IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 网络协议 -> Libvirt同步机制 —— 设计原理 -> 正文阅读

[网络协议]Libvirt同步机制 —— 设计原理

背景

  • Libvirt是一个虚拟机管理的开源项目,它的实现涉及到对虚拟机任务的管理,为了提高Libvirtd服务并发执行API的效率,同时又能保证多线程访问同一个虚机数据结构的一致性,Libvirt基于不同场景设计实现了不同粒度的同步机制,本文主要分析这些同步机制的设计原理。

VM同步原理

  • virDomainObj是Libvirt对虚机数据结构的抽象,包含了通用信息(比如虚机xml加载到内存信息)和每个虚机私有的driver信息(比如qemu driver的私有qemuDomainObjPrivate)。当多个API同时发起,可能出现多线程并发修改VM信息的场景,virDomainObj的基类parent中包含一把互斥锁virMutex,多线程访问VM时通过这把锁来保证数据同步。下图是一个示例,解释两个Libvirt的API qemuDomainGetInfo并发执行时的加锁顺序。
    请添加图片描述
  • 如上图所示,thread1是Libvirtd服务收到客户端的rpc调用后fork的线程,开始执行qemuDomainGetInfo,这个API的功能是获取虚机的内存、CPU运行状态等信息,这些信息在VM的结构体中都有保存,因此API的主要逻辑就是查询VM的数据结构。
  • 步骤如下:
  1. 通过qemuDomainObjFromDomain接口获取vm结构体,过程中对virDomainObj.parent加锁
  2. 获取虚机总内存:virDomainObj.def->mem.total_memory
  3. 获取虚机状态:virDomainObj.state.state
  4. 获取proc文件系统下虚机Qemu进程运行时间
  5. 通过virDomainObjEndAPI释放virDomainObj.parent锁
  • 以上步骤中,所有Libvirt API都必须执行1、5这两部,保证所有API对VM数据结构的视图一致。
  • 现在假设Libvirtd服务在API执行过程中又收到了客户端的rpc调用消息,针对同一个虚机开始执行qemuDomainGetInfo,由于Libvirt API编程规范中要求在修改VM信息前必须先获取virDomainObj.parent锁,而此时virDomainObj.parent锁被先前的API持有,因此后来的API只能等待先前API执行结束,释放锁之后才能开始。

任务同步原理

  • VM同步保证了虚机信息在多线程修改时保持一致,但有的时候,针对虚机的一些操作是比较耗时的,并且时间开销不在对数据结构的修改上,而是其它动作,比如执行QMP、QGA等命令或者执行迁移、备份这种操作。这个时候VM同步的接口会显得粒度太大,需要允许在以上这些操作的同时,其它API也能并发执行。因此,Libvirt设计了针对虚机的任务同步机制和接口并定义了一系列同步任务,对于同一个虚机而言,同一时间只允许有其中一个任务在执行。

同步任务

  • 我们以查询虚机内存统计信息接口qemuDomainMemoryStats举例,解释一个简单的任务同步,如下图所示:
    请添加图片描述- VM的内存统计接口核心实现是通过QMP的命令query-balloon查询内存信息。这个接口不涉及任何对Libvirt VM信息的修改,执行过程中允许其它API修改VM的信息,在执行过程中如果继续持有VM的大锁,会导致其它API无法并发执行。看一下API整个执行步骤:
  1. 通过qemuDomainObjFromDomain接口获取vm结构体,过程中对virDomainObj.parent加锁。
  2. 通过调用qemuDomainObjBeginJob(driver, vm, QEMU_JOB_QUERY)标记VM正在执行查询任务。查询任务QEMU_JOB_QUERY会被设置到VM的私有结构体obj->privateData->job.active中。
  3. 调用qemuDomainObjEnterMonitor为执行qmp命令做准备,因为执行qmp命令并不会对VM信息做修改,所以在执行qmp命令完成之前,允许其它虚机对虚机VM信息进行修改,qemuDomainObjEnterMonitor的核心动作就是释放virDomainObj.parent这把锁。
  4. 执行qmp命令query-balloon查询虚机内存信息。
  5. 完成qmp命令后,API需要修改VM信息了,因此调用qemuDomainObjExitMonitor,里面的核心动作是对virDomainObj.parent加锁。一旦拿到这把锁,其它针对这个VM的API就再也无法对VM信息进行修改了。因为Libvirt API编程规程要求其它API修改VM信息前需要拿到virDomainObj.parent才可以。
  6. API的核心逻辑在步骤4已经完成,查询的任务也该结束了,因此调用qemuDomainObjEndJob标记VM结束任务。函数的核心动作是将VM的任务设置为QEMU_JOB_NONE,表明当前VM没有任务在执行。
  • 我们假设上述步骤4在Thread 2中被执行时,此使针对VM新来了一个API调用,fork了一个新的线程Thread 1,同样要求查询内存统计信息,我们分析Libvirt的如何利用同步机制实现对数据保护的同时允许最大限度并发执行:
  1. Thread 1按照正常流程通过qemuDomainObjFromDomain接口获取vm结构体,过程中对virDomainObj.parent加锁。因为Thread 2此时正在执行qmp命令,将锁释放了,因此Thread 1可以轻易持有该锁。
  2. Thread 1 通过调用qemuDomainObjBeginJob(driver, vm, QEMU_JOB_QUERY)期望标记VM正在执行查询任务,但此时发现VM上已经有一个查询任务了,因此会Thread 1会等在priv->job.cond这个信号量上,同时释放自己拿到的virDomainObj.parent,如果等待QEMU_JOB_WAIT_TIME(30s)信号量还没有准备号,就会唤醒,然后重新查询一次VM上是否有任务。
  3. 当Thead 2执行完qmp命令后,在调用qemuDomainObjExitMonitor时会尝试拿virDomainObj.parent锁,此时因为Thread 1在睡眠,Libvirt编程规则时要求将virDomainObj.parent锁释放的,因此Thread 2可以轻易拿到。
  4. 当Thread 2执行完qemuDomainObjExitMonitor,会继续调用qemuDomainObjEndJob标记VM结束任务,这个时候它会将VM的任务设置为QEMU_JOB_NONE,同时通过调用virCondBroadcast(&priv->job.cond)将所有等待在该信号量上的线程唤醒,这里就包括Thread 1。
  5. Thread 1被唤醒后,继续检查VM上是否有任务,发现任务已经被设置为QEMU_JOB_NONE,因此会结束等待,再次标记VM当前的任务为QEMU_JOB_QUERY。这样后来的线程又会继续等待当前任务的结束。

异步任务

  • Libvirt引入同步任务后,可以针对同一个VM并发执行数据结构的修改和非数据结构修改的动作。但如果两个任务,都对VM的数据结构没有修改,并且允许任务并发执行,这个时候,利用现有的同步任务机制就没法实现了,因为同步任务机制要求只要VM上有一个线程在执行任务,其它线程就无法执行任务。还有一种情况是,如果VM当前在执行一个任务,并且这个任务错误了,永远无法结束,这时必须要发起另外一个任务来终止出错的任务,如果按照Libvirt提供的上述同步机制,是做不到的,因为后面一个任务必须等到前一个任务结束才能发起。综上原因,Libvirt在同步任务中又特别定义一个QEMU_JOB_ASYNC任务,引入了异步任务的概念,需要注意的是,异步任务和所有其它同步任务是互斥的,即针对同一个VM,同一时间还是只能有一个任务,无论是异步任务还是同步任务。除非其中一个异步任务允许其它任务并发执行,才可能被允许同时执行。
  • 我们以快照和迁移两个任务举例,来说明异步任务的使用规则,如下图所示:
    请添加图片描述
  • 首先原理上,Qemu不允许针对一个虚机同时执行快照和迁移,因此这两个任务肯定不能并发执行,但他们是通过异步任务来保持执行顺序的。我们先分析快照的步骤:
  1. 通过qemuDomainObjFromDomain接口获取vm结构体,过程中对virDomainObj.parent加锁。
  2. 调用qemuDomainObjBeginAsyncJob,标记同步任务是QEMU_JOB_ASYNC,异步任务是QEMU_ASYNC_JOB_SNAPSHOT,这里会调用qemuDomainObjCanSetJob判断VM是否有正在执行的同步任务,如果有,则异步任务必须等待当前的同步任务完成。
  3. 标记VM正在执行快照任务之后,便是快照的核心实现,调用qmp命令发起快照。
  4. 快照执行完成后,调用qemuDomainObjEndAsyncJob标记VM已经结束快照这个异步任务。同时唤醒所有等待在异步任务信号量priv->job.asyncCond上的所有异步任务。
  • 假设在Qemu执行快照过程中,虚机发起了迁移,快照在Thread 2中执行,迁移在Thread 1中执行,我们继续分析迁移步骤,解释两者如何保证顺序的:
  1. Thread 2调用qemuDomainObjFromDomain接口获取vm结构体,过程中对virDomainObj.parent加锁。此时虚机正在Qemu中做快照,virDomainObj.parent释放了,因此可以正常获取。
  2. Thread 2调用qemuDomainObjBeginAsyncJob,打算标记同步任务为QEMU_JOB_ASYNC,异步任务QEMU_ASYNC_JOB_MIGRATION_OUT,此时通过调用qemuDomainNestedJobAllowed判断VM上是否有正在执行的异步任务,如果有异步任务,并且这个异步任务不允许其它异步任务并发执行,那么要发起的异步任务就必须等待当前异步任务结束。显然当前VM存在快照这个异步任务,并且不允许迁移异步任务并发执行,因此迁移任务必须等待。睡在priv->job.asyncCond信号量上。
  3. 当快照任务结束后,会唤醒睡在priv->job.asyncCond信号量上的线程,这是迁移线程会被唤醒,开始执行迁移的核心流程。
  • 总结上面的步骤,快照和迁移被定义为了异步任务,并且通过异步任务的机制保证两者顺序执行,但这个效果同步任务机制也可以实现,这里为什么要这样做呢?因为Libvirt将快照和迁移定义为异步任务,是保留了其它任务并发执行的机会,这里的其它任务包括查询任务、销毁任务和终止任务。这样查询、销毁和终止任务永远可以在异步任务执行的同时并发执行。这个机制通过priv->job->mask来实现。我们可以跟踪快照函数qemuSnapshotCreateXML,当它执行完qemuDomainObjBeginAsyncJob之后,还会调用qemuDomainObjSetAsyncJobMask来设置允许并发执行的任务。qemuDomainObjSetAsyncJobMask函数默认会把QEMU_JOB_DESTROY设置为快照允许的任务。从而保证快照可以被终止。另外一个允许两个异步任务并发执行的场景,出现在备份任务上,备份任务(QEMU_JOB_SUSPEND)允许其它任务比如虚机挂起(QEMU_JOB_SUSPEND)、虚机修改(QEMU_JOB_MODIFY)任务并发执行。详细流程可以参考qemuDomainBackupBegin API。

嵌套异步任务

  • 除了以上同步场景,再考虑一种情况,当一个线程正在执行一个异步任务,这个过程中它不允许别的线程并发执行任务,但允许自己发起同样的任务,即所谓嵌套异步任务。这种情况下按照之前的设计,可以将自己发起的任务设置为允许异步执行的任务,但这样可能会导致其他执行相同任务的线程也被允许,因此需要设计一种同步机制只允许任务嵌套执行。这就是QEMU_JOB_ASYNC_NESTED的定义。下面我们以迁移任务举例进行说明:
    请添加图片描述
  • 首先我们分析Libvirt迁移API的实现逻辑和具体步骤,假设这在Thread 2中执行:
  1. 同样,API通过qemuDomainObjFromDomain接口获取vm结构体,过程中对virDomainObj.parent加锁。
  2. 之后,通过调用qemuMigrationJobStart标记VM的任务。目标是将同步任务标记为QEMU_JOB_ASYNC,异步任务标记为QEMU_ASYNC_JOB_MIGRATION_OUT,在这个过程中检查到VM上没有其它同步或异步任务,标记成功。
  3. 准备通过qmp命令发起迁移,因为迁移是比较耗时的任务且不涉及VM信息的修改,因此该过程需要放开VM锁,否则影响其它API的并发执行,这个动作在qemuDomainObjEnterMonitorAsync中被执行,它将VM的同步任务标记为QEMU_JOB_ASYNC_NESTED,在标记过程中,qemuDomainObjBeginNestedJob还会判断当前虚机上的异步任务和想要发起的异步任务是不是同一个,如果不是同一个则不被允许,而且也会判断发起异步任务的线程ID和正在VM上执行异步任务的线程ID是否相同,如果不相同,会又警告打印,但任务仍然被允许继续执行。qemuDomainObjEnterMonitorAsync在完成任务的标记后会释放virDomainObj.parent的锁。走完整个函数逻辑。
  4. 执行迁移的核心逻辑,通过qmp的migrate命令发起虚机迁移。
  5. 虚机迁移发起后,就是等待迁移结束,这个过程中会不断查询虚机的迁移状态并更新VM的数据结构,因此需要再次获得virDomainObj.parent这把锁,这个在qemuDomainObjExitMonitor执行,同时结束嵌套任务QEMU_JOB_ASYNC_NESTED
  6. 调用qemuMigrationSrcWaitForCompletion等待迁移任务结束,等待的过程中因为要睡眠,而Libvirtd的Monitor poll线程在更新迁移状态时又需要拿锁,因此等待的过程就是放锁,睡眠,超时唤醒,检查迁移是否结束这个循环执行。
  7. 迁移结束后,调用qemuMigrationJobFinish标记迁移异步任务完成。
  8. 整个API逻辑执行结束后,调用virDomainObjEndAPI释放VM锁。
  网络协议 最新文章
使用Easyswoole 搭建简单的Websoket服务
常见的数据通信方式有哪些?
Openssl 1024bit RSA算法---公私钥获取和处
HTTPS协议的密钥交换流程
《小白WEB安全入门》03. 漏洞篇
HttpRunner4.x 安装与使用
2021-07-04
手写RPC学习笔记
K8S高可用版本部署
mySQL计算IP地址范围
上一篇文章      下一篇文章      查看所有文章
加:2022-05-19 12:04:57  更:2022-05-19 12:05:56 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/19 9:09:36-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码