云硬盘快照数据丢失问题测试

云硬盘快照数据丢失问题测试

问题复现

  1. 虚机A挂载了一块云硬盘disk A,在挂载状态下对该硬盘中写入数据,如echo > test.txt helloworld
  2. 创建云硬盘快照,并基于该快照创建一块新的云硬盘disk B,将disk B挂载到虚机B上;
  3. 对比发现,disk B中的数据与disk A中不一致,disk B中的test.txt文件内容为空。存在数据丢失问题。

文件写入的过程中,包括两个主要的步骤:
1. 从虚机内存将数据写到虚机disk;
2. 从虚机disk将数据写到存储设备。

在复现问题的过程中,发现使用echo > test.txt helloworld写数据时,会出现数据不一致问题;而使用vim方式写入数据,则没有出现数据不一致问题。因此猜测可能是echo和vim对读写缓存的操作机制不同导致的这个差异。为了进一步验证这个猜想,进行了以下测试。

虚拟机数据落盘测试

vim的源码太长,没时间逐一分析它和echo在源码级别上读写操作的区别。但是在vim源码中找到如下信息:

When {flags} contains “s” then fsync() is called after writing
the file. This flushes the file to disk, if possible. This
takes more time but avoids losing the file if the system
crashes.
When {flags} does not contain “S” or “s” then fsync() is
called if the ‘fsync’ option is set.
When {flags} contains “S” then fsync() is not called, even
when ‘fsync’ is set.

从函数说明中可以看到,fsync函数触发了数据落盘的动作。也就是说,echo之所以丢失数据,有可能是因为没有进行fsync()操作。又做了一次测试,这次在使用echo > test.txt helloworld之后,又调用了sync指令手动刷盘。检查发现,这次没有丢数据,证明确实是因为没有刷盘导致了数据丢失。

应用测试

那对于应用程序而言,如何避免出现这种情况?这里用python进行了测试。

首先,我们通过如下代码写入数据,测试发现,下述代码丢数据了:

with open('test.txt', 'w') as fp:
    fp.write('hello world\n')

改进代码,手动调用os.fsync刷盘。测试发现,这次没有丢数据:

import os

with open('test.txt', 'w') as d_file:    
    fp.write('hello world\n')
    fp.flush()
    os.fsync(fp)

对于大应用而言,修改代码太麻烦了。经过测试发现,执行完Python代码之后,直接调用sync指令刷盘,然后再创建快照,数据一样不会丢失。

问题总结

一般来说,虚机内部的写io,只是写到了操作系统的pagecache中,就返回写成功了 。此时数据并没有落盘,此时创建的快照就会存在数据丢失的问题。

要想保证创建快照的数据完整性,一个比较直白的想法就是在创建快照之前先把虚拟内部的pagecache刷到磁盘里,再进行打快照的操作。

关于快照完整性的问题,实际上社区的解决方案也是这么操作的:
- 在给虚机创建快照的时候,libvirt下发一个“刷盘”的指令给虚机
- 虚机内部需要有qemu_guest_agent,用来接收“刷盘”的指令,并执行“刷盘”的动作
- 然后,再执行创建快照的动作

对虚机打快照时(社区现有的逻辑):
- 如果是boot from volume的虚机,会调用cinder的snapshot接口,对虚机上的所有volume打快照(包括系统盘,数据盘),且会调用libvirt的quiesce接口,可以保证数据完整性
- 如果是boot from image的虚机,只会对系统盘打快照,是否会调用libvirt的quiesce接口取决于镜像是否有属性”os_require_quiesce”
- 调用libvirt的quiesce接口需要虚机的镜像安装有qemu-guest-agent(libvirt发送指令,qemu-guess-agent接收指令执行相关fsfreeze的动作)

通过cinder单独对云硬盘打快照(社区现有的逻辑):
- 如果是未挂载的云硬盘,创建快照的时候不存在数据丢失的问题
- 如果是已挂载的云硬盘,存在数据丢失的问题(没有主动的去调用libvirt的quiesce接口)

disk cache分析

一开始的时候提到,从虚机disk将数据写到物理磁盘,也同样有可能丢失数据,这种情况其实和普通的硬盘读写类似。数据写到硬盘里,返回写成功,并不意味着数据真正写到硬盘的存储介质上了。数据有可能还在硬盘的cache中。硬盘自身一般都会有少量的cache,cache里的数据有可能会掉电丢失(有些硬盘的cache是非易失性的,掉电数据不会丢失)。有些场景下希望将硬盘cache里的数据刷到硬盘真正的存储介质上,避免突然掉电或者crash导致硬盘cache里的数据丢失,比如日志文件系统(xfs、ext4)的journal。因此在应用开发中,重要的数据都应该有一个操作将硬盘cache里的数据刷到硬盘的存储介质上。

linux内核层面对硬盘内部cache的控制策略(writeback_cache_control)

  • forced cache flush:给硬盘下发bio写请求时,使用REQ_PREFLUSH标志,硬盘会先将当前cache中的数据写到存储介质上,再执行该写请求。

  • forced unit access:给硬盘下发bio写请求时,使用REQ_FUA标志,硬盘会确保这个写请求的数据真正落到存储介质上,再返回写成功,不会将当前cache中的数据刷到存储介质上。

上述两个接口需要硬盘控制器本身能够支持,比较新的硬盘应该都能支持。

fsync()

fsync()的实现取决于文件系统,大部分情况下也会用到REQ_PREFLUSH接口将数据刷到硬盘存储介质。

libvirt定义虚机disk时,cachemode的几种模式

               BDRV_O_NO_CACHE    writethrough    BDRV_O_NO_FLUSH
none                yes               no                no
directsync          yes               yes               no
writeback           no                no                no
writethrough        no                yes               no
unsafe              no                no                yes
  • 标志BDRV_O_NO_CACHE意味着不使用虚机disk后端driver的某种cache机制;
  • 标志writethrough意味着是否每次写io都需要调用后端driver的某种flush机制,将数据刷入存储介质;
  • 标志BDRV_O_NO_FLUSH意味着后端driver是否可以忽略来自虚机下发的flush指令。
# 例如:
# 
# 标志BDRV_O_NO_CACHE:
#
#   对于虚机disk的后端driver使用本地file的形式,对应的xml定义文件中形如" source file='...' " ,
#   则none或者directsync模式将不使用host的pagecahce,
#   在原理上也就是qemu在打开(open系统调用)source指定的file时,使用了O_DIRECT标志
#   
#   对于虚机disk的后端driver使用的是ceph rbd的情况,
#   则none或者directsync模式将不使用rbd cache,
#   在原理上也就是将ceph的rbd_cache选项设为false
#   
# 标志writethrough:
#    
#   对于虚机disk的后端driver使用本地file的形式,
#   则directsync或者writethrough模式具有writethrough标志,
#   也即每次写io都会调用后端driver的某种flush()机制,
#   本情形下就是调用fsync()或者fdatasync()
#   
#   对于虚机disk的后端driver使用本地file的形式,
#   则directsync或者writethrough模式具有writethrough标志,
#   也即每次写io都会调用后端driver的某种flush()机制,
#   本情形下就是调用rbd_aio_flush()或者rbd_flush()

综合下来:

  • directsync和writethrough模式每次写io都会调用后端driver的某种flush()机制,因此可以保证数据完整性, 但写io的性能会有损失;
  • none模式下相当于io数据写到host物理硬盘就返回,数据完整性(掉电数据是否丢失)由物理硬盘决定,如果物理硬盘能够保证掉电数据不丢失,则数据完整性可以保证(现在比较新的一些硬盘,采用非易失性的cache或者大电容能够确保数据掉电不丢失,具体信息可以咨询硬盘供应商),如果物理硬盘不能保证掉电数据不丢失,则数据完整性无法保证,但虚机自身可以调用fsync()等操作来保证重要数据真实落盘,写到存储介质上;
  • writeback模式相当于io数据写到host的pagecache就返回,数据完整性无法保证,但虚机自身可以调用fsync()等操作来保证重要数据真实落盘,写到存储介质上;
  • unsafe模式无法保证数据完整性

libvirt_cachemode

以上讨论都是从host层面保证虚机下发的io数据的完整性,而虚机本身是一个完整的操作系统,也是有pagecache的,很多场景下虚机内部的io只是写到虚机的pagecache就返回了,虚机pagecache中的数据由虚机的操作系统异步的刷到后端driver对应的disk上,这种情形下的io数据,如果突然掉电,无法保证数据的完整性。

参考资料

  1. https://en.wikipedia.org/wiki/Disk_buffer#READ-AHEAD
  2. https://www.kernel.org/doc/Documentation/block/writeback_cache_control.txt
  3. https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/Documentation/block/barrier.txt?id=09d60c701b64b509f328cac72970eb894f485b9e
  4. https://lwn.net/Articles/283161/
  5. https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=4fed947cb311e5aa51781d316cefca836352f6ce
展开阅读全文

没有更多推荐了,返回首页