libvirt/qemu live snapshot代码流程分析




上次分析了nova live snapshot的一个数据丢失问题,但对底层的实现一知半解,这篇文章是对之前的延续,继续深入的搞明白底层的技术实现和原理(本文虎头蛇尾,慎入!)。

libvirt

首先有个问题,libvirt的用途是啥?

我很早之前写过一篇文档,内容比较浅,主要说明了libvirt、qemu、kvm之间的关系,在这个问题上我的理解是,libvirt就是虚拟化适配层,adapter,适配各种底层虚拟化技术,官方声明支持的有:KVMQEMUXenVirtuozzo,VMWare ESXLXCBHyve and more,当然主要支持的还是qemu,其他的虚拟化技术,支持的都不是很完善,比如Xen、lxc,基础功能的支持尚可,高级功能就不行了,所以应该绝大部分用户都是用的qemu虚拟化,Xen已经越来越不流行了。libvirt+LXC之前用过,也是不太好用,毕竟现在都是用docker了,LXC也已经成了昨日黄花。VMWare ESX这个没用过,不发表意见,不过看nova代码里面都是实现的ESX driver,而不是用libvirt driver,我估计也不是很完善,毕竟VMWare本身客户端或者API接口做的就很不错了,用户也没必要再套一层libvirt了。libvirt接口本身是C的,为了方便使用,还提供了多种语言的SDK,比如python、java等,当前最常用的应该就是python了。

libvirt是开源的,所以目前是OpenStack等开源IaaS云平台甚至闭源自研云平台的虚拟化接口适配层的首先方案,libvirt比OpenStack项目要早很多,一直以来都是redhat的主场,libvirt官方网站上有更多信息:https://libvirt.org/

代码流程

基于master版本分析(HEAD commit:07adbd4b1f82a9f09584dfa5fb6ca9063bd24bd0)。

上篇文章提到nova里面调用的是dev.rebase(),实际是通过python-libvirt接口(对libvirt api的python封装)调用的是libvirt的virDomainBlockRebase接口,源码位于src\libvirt_domain.c:

上面的注释很清楚,其实API接口说明文档就是根据这里的注释生成的:https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainBlockRebase

nova里面调用blockRebase参数是:dev.rebase(disk_delta, copy=True, reuse_ext=True, shallow=True),最终调用的python libvirt参数是self._guest._domain.blockRebase(self._disk, base, self.REBASE_DEFAULT_BANDWIDTH, flags=flags),对应的libvirt api的flag是:libvirt.VIR_DOMAIN_BLOCK_REBASE_SHALLOW、libvirt.VIR_DOMAIN_BLOCK_REBASE_REUSE_EXT、libvirt.VIR_DOMAIN_BLOCK_REBASE_COPY,self._disk就是系统盘(vda)的文件路径,base就是snapshot文件路径,之后就是带宽限制(nova默认是0不限制)和flag参数。

因此这个接口的大致功能就是,把vda系统盘的数据rebase到snapshot文件上,之后可以通过virDomainGetBlockJobInfo接口查询rebase job的进度,nova里面也是这么做的,只不过之前的接口有bug,导致job进度有误报,还没开始就认为已经结束了。文档里有这段话:When @flags contains VIR_DOMAIN_BLOCK_REBASE_COPY,
this command is shorthand for virDomainBlockCopy(),也就是说virDomainBlockCopy这个接口可以实现同样的功能,怪不得我搜索virsh命令没找到blockrebase子命令,只有blockcopy和blockpull这俩。因此参考这个接口的文档更详细:https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainBlockCopy

接下来的代码是在qemu driver目录下,src\qemu\qemu_driver.c里面(在这个文件的最后,有个qemuRegister函数,这个就是注册qemu driver的入口,qemuHypervisorDriver这个结构体定义了各种libvirt API的qemu driver实现函数,有兴趣可以自己看下):

之后的代码流程比较长,就不一一分析了,总体来说就是为准备发送json消息给qemu monitor unix domain socket准备json消息内容,大致流程如下:

src\qemu\qemu_driver.c:qemuDomainBlockCopyCommon –> src\qemu\qemu_monitor.c:qemuMonitorDriveMirror –> src\qemu\qemu_monitor_json.c:qemuMonitorJSONDriveMirror –> src\qemu\qemu_monitor_json.c:qemuMonitorJSONMakeCommand –> src\qemu\qemu_monitor_json.c:qemuMonitorJSONCommand –> src\qemu\qemu_monitor_json.c:qemuMonitorJSONCommandWithFd –> src\qemu\qemu_monitor.c:qemuMonitorSend,这个函数里面的内容比较简单,但我没有调试,没看明白具体在哪里发送的json请求给qemu monitor,因为实在没调用几个函数,用排除法应该是调用的qemuMonitorUpdateWatch,但是看了这个方法的实现,也没啥内容,看来只能加断点单步调试才能搞清楚了。这部分代码在另外一篇文章里调试了下,大概了解了相关流程,是在qemuMonitorIO这个回调里面执行的,发现monitor socket可写了,就调用qemuMonitorIOWrite把消息写到qemu monitor socket,读也是类似流程,qemuMonitorUpdateWatch只是更新monitor事件回调的监听事件列表。

网上搜了下,最终发送给qemu monitor的json字符串应该是类似(使用的命令是:virsh blockcopy rhel7f vda —dest /var/lib/libvirt/images/f.img,可以通过blockjob命令查询job进度情况:virsh blockjob rhel7f vda):

看起来跟qemu-guest-agent的命令很像,这是因为他们都遵循相同的qmp协议

virsh命令验证:

 

小结

libvirt代码毕竟只是个适配层,类似命令传递通道的作用,没有libvirt也可以用qemu命令行启动虚拟机,通过monitor传递各种命令,但是接口不太友好,不利于编程,上层服务调用比较繁琐,libvirt则很好的解决这种问题,并且还把各种虚拟化底层统一进行了API封装,还用XML定义各种虚拟机参数,比较人性化,用户体验很好,所以它流行起来了。接下来就是分析qemu代码流程了,这部分比较复杂,我也只是略懂皮毛,尽量理出来相关代码流程吧。

qemu

基于master版本分析,HEAD commit:2babfe0c9241c239272a03fec785165a50e8288c。

qemu的用途就不多说了,配合内核态的kvm模拟各种硬件设备(CPU、内存除外),可以说实现了主板的功能,BIOS则是由另外的组件实现的(如seabios),qemu也是使用相关组件而已。qemu官网:https://www.qemu.org/,这个项目应该基本上也是redhat的主场。

qemu编译方法:https://wiki.qemu.org/Hosts/Linux

qapi和qmp、hmp的关系

qapi是底层C接口,而qmp则是封装好的json格式的协议,hmp是在qmp之上提供的方便人类使用的协议,调用关系是hmp->qmp->qapi,官方说明中提到上层服务(比如libvirt)使用的应该是qmp接口。

QAPI介绍:https://wiki.qemu.org/Features/QAPI

QMP介绍:https://wiki.qemu.org/QMP

HMP介绍(只有寥寥几句,还是TODO状态):https://wiki.qemu.org/ToDo/HMP

QAPI源代码生成相关介绍:源码库doc目录下docs/devel/qapi-code-gen.txt或者https://people.cs.clemson.edu/~ccorsi/kyouko/docs/qapi-code-gen.txt

其实我对这部分自动生成代码的功能挺感兴趣的,感觉是个很神奇的功能,很早之前还研究过,当时水平太差,没整明白,一脸懵逼,后面有空再研究研究。

QAPI代码是根据定义好的模板(schema,qapi目录下的那些json文件)来生成的,尤其是一些.h文件中定义的结构体、枚举类型,都是自动生成的(包含.h文件本身),生成的工具都在qemu根目录的scripts目录下,qapi*.py,使用方法参考上面的链接。

如果你不想每个源码文件都人肉生成,可以按照上面的qemu编译方法,直接在编译过程中生成相关源码文件即可。

qemu live block operations

相关介绍位于源码库的docs/interop/live-block-operations.rst文本文件中,这里有个在线版本:https://kashyapc.fedorapeople.org/virt/qemu/live-block-operations.html

我就不翻译了,水平不行,大家直接看原文吧,这里大概讲讲里面说了些啥,也就是翻译下文档的开头一段算是摘要的部分:

QEMU Block Layer currently (as of QEMU 2.9) supports four major kinds of live block device jobs — stream, commit, mirror, and backup. These can be used to manipulate disk image chains to accomplish certain tasks, namely: live copy data from backing files into overlays; shorten long disk image chains by merging data from overlays into backing files; live synchronize data from a disk image chain (including current active disk) to another target image; point-in-time (and incremental) backups of a block device. Below is a description of the said block (QMP) primitives, and some (non-exhaustive list of) examples to illustrate their use.

QEMU块设备层当前(2.9版本)支持4种类型的在线块设备操作:stream、commit、mirror(nova live snapshot使用的是这个)、backup,这些功能分别可用来操作磁盘镜像链来完成某些任务如:从backing file live copy磁盘镜像数据到overlays、通过合并overlays到backing file来收缩镜像链的长度、live synchronize磁盘镜像数据到另外一个镜像(包含正在使用的磁盘)、制作块设备在某一时间点的备份(增量快照)。下面是对这些块设备操作方法(基于QMP命令)的描述,以及一些简要示例来说明它们的用法。

backing file:镜像的基础数据部分,打个比喻可以看作是只读的live CD启动的系统,所有数据都保存在overlay里面,overlay与backing file的数据块一一映射,但overlay一般具备稀疏文件特性,保证没有写入过的0数据块不占用实际存储空间,从而节约物理磁盘使用,当用户要修改backing file中的数据时,立即把数据copy一份到overlay中,然后根据用户下发的文件操作进行正常的数据修改,这一过程对用户透明,一旦backing file中的数据块copy到overlay之后,后续就直接从overlay这边进行相关读取、更新操作,不再跟backing file有关联。

这也是qcow2镜像的专有特性,qcow2的意义就是qemu copy on write格式的镜像(第二版本),copy on write的大概原理上面那段已经简要说明了。nova中的backing file一般是保存在instances目录下的_base目录里,而overlay部分则是instance目录下的disk文件,backing file一般是raw格式的,而overlay一般是qcow2格式,不过好像qcow2格式也支持作为backing file,但nova里是强制转换成raw再做backing file的(原因没搞清楚)。可以通过qemu-img info命令查看镜像属性信息,类似:

具体原理解释和相关qmp操作命令就不多说了,上面的链接或者源码库的doc里解释的比我清楚,自己看就好了。下面进行代码流程分析,我也属于只知其然不知其所以然的水平,大家凑合看吧(注意上面说的qapi代码生成过程,否则看不到相关源码,./configure&&make之后生成的相关源码文件大概如下,仅供参考,可能有遗漏)。

qemu是一个独立的进程,要编译成二进制可执行文件,肯定要有main函数才行,因此如果不知道入口在哪儿,可以用笨方法:

当然还可以把qemu编译成debug模式二进制,用gdb调试,就能很容易的找到入口函数的位置。

其实我是通过搜索“drive-mirror”命令反推的代码流程,因为毕竟qemu项目这么大,我不知道该怎么看(也就是说下面的代码流程分析是反向分析的,但效果应该一样,并且比较快速),C代码的函数开头和结尾有很多初始化变量,copy字符串,指针地址空间管理等准备操作,还有参数检查等逻辑,因此一般主流程都在函数的中间部分。

接下来的代码流程大致是(为啥不写了?因为关键的block数据处理流程我也没研究过,不懂啊。。。留给牛人们分析吧):

blockjob.c:void block_job_start  –>  block.c:void bdrv_coroutine_enter –> util/async.c:void aio_co_enter –> util/qemu-coroutine.c:void qemu_aio_coroutine_enter –> util/coroutine-sigaltstack.c: CoroutineAction qemu_coroutine_switch

这部分核心代码,应该必须要对块设备底层实现非常清楚才行,而这部分我的基础基本是0,后续有机会要补上(其实何止这部分,整个内核态部分都差不多是0,没做过内核相关的工作)。

底层虚拟化技术3大核心:计算、存储、网络,都是要恶补的。

能看到最后的都是真爱码农,给大家留点福利(参考资料):