Mitaka Nova在线快照数据丢失问题及解决方法




问题背景

公司云平台架构是OpenStack+kvm,产品规划在这个月的迭代中支持云主机系统盘不停机做自定义镜像(nova live snapshot),原因有2个:一是我们已经支持了定期备份功能,如果不能做到不停机做备份,那么定期备份功能的用户体验就很差,因为极大的影响了用户的业务运行;二是做自定义镜像的时候如果能不影响业务,那也是极好的。另外一个技术原因是,从M版本之后的某个版本(Pike 16.0版本还没改,应该是master分支改的,也就是Q版本会变成默认开启)开始,live snapshot已经是nova做系统盘快照的默认方式(M版本默认还是cold snapshot),可见该功能已经非常成熟稳定。

问题症状

经过修改nova配置项[workaround]disable_libvirt_livesnapshot=False简单测试,发现nova image-create命令执行后,云主机确实不会再中断一下了,在整个快照过程中,都正常可用,卡顿现象都没发生,试着从自定义镜像创建新云主机,也可以正常进入操作系统,非常兴奋,以为果然好用。

结果有一天在这个开启了live snapshot的环境部署了一台测试机,安装了一些软件,想通过自定义镜像复制几台新的云主机,打完快照,从快照新建,启动后登陆到云主机内部,发现装好的软件不见了,镜像内容跟基础镜像一样,之后就郁闷了。

于是不甘心,在使用ceph后端做nova系统盘的环境下测试,发现没有问题,看了下nova代码的live snapshot流程,ceph后端场景下live snapshot的流程是直接调用rbd后端接口做snap,因此没有这个问题。需要注意的是,即便是ceph后端做系统盘,默认也是cold snapshot方式做自定义镜像。

看来这个问题只在本地镜像文件做系统盘的场景下才会发生。

问题定位

于是只能继续分析nova代码,老的cold snapshot流程跟之前用了很久的H版本中一样,都是先休眠虚拟机,然后通过qemu-img convert命令合并instance/UUID目录下的cow部分disk文件和_base目录下的backing file基础部分,合成一个完整的qcow2镜像(或者你在nova配置文件中指定的snapshot镜像格式),然后就可以唤醒虚拟机,最后通过glanceclient调用update image api更新镜像内容(镜像uuid在nova api服务里面已经调用glance api生成了,提前预留好)。这个过程中,从虚拟机休眠到唤醒期间,都是停服的,虽然唤醒后虚拟机里面的CPU状态、内存数据都还在,但毕竟是无法响应外部请求的,对用户影响很大。

而live snapshot在线快照流程,则是不对虚拟机进行休眠操作,只是freeze一下虚拟机内部的文件系统(只针对系统盘,也就是vda盘),freeze文件系统这一步也不是强制的,但是最好支持,否则正在写入的或者缓存中的磁盘数据可能刷不到系统盘上,导致文件系统校验错误(windows虚拟机启动时会提示非正常关机,进入启动选项的选择模式:“安全模式”、“正常启动”等那几条选项,默认是等待30s进入正常启动模式)。

看了下代码,关键的步骤是在dev.rebase那里,这个操作前后的流程都是用qemu-img命令做一些准备和收尾工作,这部分的原理还不是特别清楚,应该是libvirt通过qemu monitor socket进行系统盘数据同步工作(我理解是类似本地文件镜像启动的虚拟机的热迁移流程),反复迭代同步数据,同步过程中记录新的脏数据,然后下个迭代把上一轮迭代过程中产生的脏数据继续同步,直到收敛到一个合适的值之后一次同步完所有脏数据(不知道会不会有短暂的vcpu pause过程,感觉应该会有)。

于是开始加断点单步调试,调试过程中,会把rebase前、后的中间过程snapshot文件(一般是instances目录下的snapshot目录,在里面会为每次snapshot过程创建一个tmp开头的临时目录)拷贝到另外的目录,然后用qemu-nbd+kpartx命令挂载到物理机上进行snapshot文件内容查看(参考:使用nbd设备挂载镜像进行内容修改),发现这个过程中,dev.rebase之后的镜像就有基础镜像创建之后新增的文件,内容也一致,然后最后一步extract_snapshot(通过qemu-img convert命令把cow和backing file合成一个完整镜像)合并完整镜像,也是没问题的,尤其是这一步最不值得怀疑,因为我本身也经常这么操作,从来没遇到过数据丢失问题。

于是去掉断点,继续做snapshot,发现基础镜像创建后新增的数据又没有了。。。

思来想去,想不到啥原因,在这里中断了3天都没思路,中间也反复断点调试了好几次,确认freeze操作都是可用的(虚拟机里面写入文件比如touch命令会卡住,virsh qemu-agent-command命令查看虚拟机freeze-status,也是freezed状态),证明不是freeze过程出现问题。还有一个明显的现象是,断点调试过程中做的自定义镜像(snapshot),都是有新增数据的,而没有加断点单步调试的时候做的snapshot,都是没有新增数据的。

晚上睡觉前整理思路,思考加断点和不加断点两种操作有啥区别,一个是权限问题,一个就是延时问题,权限问题:不加断点模式systemd启动时用的是nova用户启动nova-compute进程,而加入断点后前台启动nova-compute是在root账号下,但感觉不太可能,毕竟没报权限不足之类的错误,为了排除这种可能,断点调试启动也切换到了nova账号下,发现新增数据还是有的,非root只要加断点就有数据。那就剩下最后一种可能了,延时问题,于是在代码里加time.sleep(10),第一次不知道问题出在哪里,所以一共加了4个(上面的代码里面有注释具体位置),重点加在了dev.rebase前后,加完之后不加断点systemd启动nova-compute,执行live snapshot,之后启动虚拟机,发现数据果然都在,反复试验了好几次都在,换了几个镜像也ok,确认是延时问题导致。

于是跟几个前同事交流,看有没有啥想法,他们也没遇到过这种问题,一个是在H版本上加入的自己写的live  snapshot过程,跟官方的流程不太一样,另一个是没用过live snapshot。不过H版本的同事提醒我会不会是nova的bug。

如果是bug要想找到patch,就得先找到出现问题的位置,于是四个sleep,先保留第一个,其他去掉,发现问题还在,保留第二个,发现问题不在了,确认是在dev.rebase之后出现的问题,找到问题点之后,继续对比查看master分支,发现while not dev.is_job_complete()这行有改动,对比了M版本和master的差异,通过git blame命令发现改了2次,于是都pick过来测了下,问题解决了。

具体bug原因可以看nova/virt/libvirt/guest.py里is_job_complete这个方法的注释,大致意思就是说之前判断rebase job是否结束的代码有bug,会把还没有开始当作已经结束,并且第一次改动后的代码还不够完善,于是redhat的人又改了一次,果然还是人家redhat libvirt/qemu玩的溜啊!