栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Libvirt同步机制 —— 设计原理

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Libvirt同步机制 —— 设计原理

文章目录
  • 背景
  • VM同步原理
  • 任务同步原理
    • 同步任务
    • 异步任务
    • 嵌套异步任务

背景
  • 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的定义。下面我们以迁移任务举例进行说明:
  • TODO
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/893966.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号