QEMU-KVM 的缓存机制的概念很多,Linux/KVM I/O 软件栈的层次也很多,网上介绍其缓存机制的文章很多。边学习变总结一下。本文结合 Ceph 在 QEMU/KVM 虚机中的使用,总结一下两者结合时缓存的各种选项和原理。
先以客户机(Guest OS) 中的应用写本地磁盘为例进行介绍。客户机的本地磁盘,其实是 KVM 主机上的一个镜像文件虚拟出来的,因此,客户机中的应用写其本地磁盘,其实就是写到KVM主机的本地文件内,这些文件是保存在 KVM 主机本地磁盘上。
先来看看 I/O 协议栈的层次和各层次上的缓存情况。
熟悉 Linux Kernel 的人都知道在内核的存储体系中主要有两种缓存,一是 Page Cache,二是 Buffer Cache。Page Cache 是在 Linux IO 栈中为文件系统服务的缓存,而 Buffer Cache 是处于更下层的 Block Device 层,由于应用大部分使用的存储数据都是基于文件系统,因此 Buffer Cache 实际上只是引用了 Page Cache 的数据,而只有在直接使用块设备跳过文件系统时,Buffer Cache 才真正掌握缓存。关于 Page Cache 和 Buffer Cache 更多的讨论参加 What is the major difference between the buffer cache and the page cache? 。
这些 Cache 都由内核中专门的数据回写线程负责来刷新到块设备中,应用可以使用如 fsync(2), fdatasync(2) 之类的系统调用来完成强制执行对某个文件数据的回写。像数据一致性要求高的应用如 MySQL 这类数据库服务通常有自己的日志用来保证事务的原子性,日志的数据被要求每次事务完成前通过 fsync(2) 这类系统调用强制写到块设备上,否则可能在系统崩溃后造成数据的不一致。而 fsync(2) 的实现取决于文件系统,文件系统会将要求数据从缓存中强制写到持久设备中。
( 来源 )
主要组成部分:
Page Cache 是客户机和主机操作系统维护的用来提高存储 I/O 性能的缓存,它是 Linux 虚拟文件系统缓存的一部分,位于操作系统内存中,它是易失性的,因此,在操作系统奔溃或者系统掉电时,这些数据会消失。数据是否写入 Page cache 可以被控制。当会写入 page cache 时,当数据被写入 page cache 后,应用就认为写入完成了,随后的读操作也会从 page cache 中读取数据,这样性能会提高。可以使用 fsync 来将数据从 page cache 中拷贝到持久存储。
在 KVM 环境中,host os 和 guest os 都有 page cache,因此,最好是能绕过一个来提高性能。
该缓存的特点是读的时候,操作系统先检查页缓存里面是否有需要的数据,如果没有就从设备读取,返回给用户的同时,加到缓存一份;写的时候,直接写到缓存去,再由后台的进程定期涮到磁盘去。这样的机制看起来非常的好,在实践中也效果很好。
考虑到其易失性,需要考虑它的大小,特别是在 KVM 主机上。现在 KVM 主机的内存可以很大。其内存越大, 那么在 Page cache 中还没有 flush 到磁盘(虚拟或者物理的)的脏数据就越多,其丢失的后果就越严重。默认的话,Linux 2.6.32 在脏数据达到内存的 10% 的时候会自动开始 flush。
这是磁盘的 write cache,它会提高数据到存储的写性能。写到 disk write cache 后,写操作会被认为完成了,即使数据还没真正被写入物理磁盘。这样,如果 disk write cache 没有备份电池的话,断电将导致尚未写入物理磁盘的数据丢失。要强制数据被写入磁盘,应用可以通过操作系统可以发出 fsync 命令。因此,disk write cache 会提到写I/O 性能,但是,需要确保应用和存储栈会将数据写入磁盘中。如果 disk write cache 被关闭,那么写性能将下降,但是断电时数据丢失将会避免。
在 libvirt xml 中使用 'cache' 参数来指定driver的缓存模式,比如:
<disk type='file' device='disk'> <driver name='qemu' type='raw' cache='writeback'/>
QEMU/KVM 支持如下这些缓存模式作为 ‘cache’ 的可选值:
缓存模式 | 说明 | GUEST OS Page cache | Host OS Page cache | Disk write cache | 数据安全性 |
Cache=none | 客户机的I/O 不会被缓存到 host OS page cache,而是会放在 disk write cache。 在大 I/O 需求时使用这种模式 基本上这是最优模式,而且是支持实时迁移的唯一模式 | Enable | Disable | Enable | 不安全. Only ensured when cache is battery-backed or fsync |
cache = unsafe | 跟 writeback 类似,只是会忽略 GUEST OS 的 flush 操作,完全由 HOST OS 控制 flush | E | E | E | 最不安全,只有在特定的场合才会使用 |
Cache=writeback | I/O 写到 HOST OS Page cache 就算成功,支持 GUEST OS flush 操作。效率最快,但是也最不安全 | E | E | E | 不安全. (only for temporary data where potential data loss is not a concern ) |
Cache=writethrough | I/O 会被缓存在host OS page cache 便于以后的读,但是它会被写入物理存储才算成功。 这是较慢的模式。 最好是用在小规模的有低 I/O 需求客户机的场景中 当不需要支持实时迁移时,如果不支持writeback 则可用 | E | E | D | 安全 |
Cache=DirectSync | 跟 writethrough 类似,只是不写入 HOST OS Page cache | E | D | D | 同 ”O_SYNC“,对一些数据库应用来说,往往会直接使用这种模式,直接将数据写到数据盘 |
cache=default | 使用各种driver 类型的默认cache 模式 qcow2:默认 writeback |
看看性能比较:
基本结论:
上面的基本结论中,writethrough 是最安全的,但是效率也是最低的。它将数据放在 HOST Page Cache 中,一方面来支持读缓存,另一方面,在每一个 write 操作后,都执行 fsync,确保数据被写入物理存储。只有在数据被写入磁盘后,写操作才会标记为成功。这种模式下,客户机的 virtual storage adapter 会被通知不会使用 writeback 模式,因此,它不会主动发送 fsync 命令,因为它是重复的,不需要的。
那还有没有什么办法使它在保持数据可靠性的同时,使它的效率提高一些呢?答案是 KVM Write barrier 功能。新的 KVM 版本中,启用了 “barrier-passing” 功能,它能保证在不管是用什么缓存模式下,将客户机上应用写入的数据 100% 写入持久存储。
好吧,这真是个神器。。那它是如何实现的呢?以 fio 工具为例,在支持 write barrier 的客户机操作系统上,在使用 direct 和 sync 参数的情况下,会使用这种模式。它在写入部分数据以后,会使得操作系统发出一个 fdatasync 命令,这样 QEMU-KVM 就会将缓存中的数据 flush 到物理磁盘上。
基本过程:
看起来和 writethrough 差不多是吧。但是它的效率比 writethrough 高。两者的区别在于,writethrough 是每次 write 都会发 fsync,而 barrier-passing 是在若干个写操作或者一个会话之后发 fdatasync 命令,因此其效率更高。
也可以看到,使用它是有条件的:
也可以看到,应用在需要的时候发出 flush 指令是关键。一方面,Cache 都由内核中专门的数据回写线程负责来刷新到块设备中;另一方面,应用可以使用如 fsync(2), fdatasync(2) 之类的系统调用来完成强制执行对某个文件数据的回写。像数据一致性要求高的应用如 MySQL 这类数据库服务通常有自己的日志用来保证事务的原子性,日志的数据被要求每次事务完成前通过 fsync(2) 这类系统调用强制写到块设备上,否则可能在系统崩溃后造成数据的不一致。而 fsync(2) 的实现取决于文件系统,文件系统会将要求数据从缓存中强制写到持久设备中。类似地,支持 librbd 的QEMU 在适当的时候也会发出 flush 指令。
考虑到 KVM write barrier 的原理和 KVM 各种缓存模式的原理,显而易见,writeback + barrier 的方式下,可以实现 效率最高+数据安全 这种最优效果。
RBDCache 是 Ceph 的块存储接口实现库 Librbd 用来在客户端侧缓存数据的目的,它主要提供了 读数据 缓存, 写数据汇聚写回 的目的,用来提高顺序读写的性能。需要说明的是,Ceph 既支持以内核模块的方式来实现对 Linux 动态增加块设备,也支持以 QEMU Block Driver 的形式给使用 QEMU 虚拟机增加虚拟块设备,而且两者使用不同的库,前者是内核模块的形式,后者是普通的用户态库,本文讨论的 RBDCache 针对后者,前者使用内核的 Page Cache 达到目的。
从这个栈可以看出来,RBDCache 类似于磁盘的 write cache。它应该有三个功能:
因此,需要注意的是,理论上,RBDCache 对顺序写的效率提升应该非常有帮助,而对随机写的效率提升应该没那么大,其原因应该是后者合并写操作的效率没前者高(也就是能够合并的写操作的百分比比较少)。具体效果待测试。
在使用 QEMU 实现的 VM 来使用 RBD 块设备,那么 Linux Kernel 中的块设备驱动是 virtio_blk,它会对块设备各种请求封装成一个消息通过 virtio 框架提供的队列发送到 QEMU 的 IO 线程,QEMU 收到请求后会转给相应的 QEMU Block Driver 来完成请求。当 QEMU Block Driver 是 RBD 时,缓存就会交给 Librbd 自身去维护,也就是一直所说的 RBDCache;用户在使用本地文件或者 Host 提供的 LVM 分区时,跟 RBDCache 同样性质的缓存包括了 Guest Cache 和 Host Page Cache,见本文第一部分的描述。
在 ceph.conf 中,设置 rbd cache = true 即可以启用 RBDCache。它有以下几个主要的配置参数:
配置项 | 含义 | 默认值 |
rbd cache | 是否启用 RBDCache | true,启用 |
rbd_cache_size | Librbd 能使用的最大缓存大小 | 32 MiB |
rbd_cache_max_dirty | 缓存中允许脏数据的最大值,用来控制回写大小,不能超过 rbd_cache_size | 24 MiB |
rbd_cache_target_dirty | 开始执行回写过程的脏数据大小,不能超过 rbd_cache_max_dirty | 16MiB |
rbd_cache_max_dirty_age | 缓存中单个脏数据最大的存在时间,避免可能的脏数据因为迟迟未达到开始回写的要求而长时间存在 | 1 秒 |
可见,默认情况下:
也能看出,RBDCache 从空间和时间来方面,在效率和数据有效性之间做平衡。
有两种类型的 flush:
关于第二种 flush,这里的一个问题是,什么时候会有这种主动 flush 指定发出。有问题说,”QEMU 作为最终使用 Librbd 中 RBDCache 的用户,它在 VM 关闭、QEMU 支持的热迁移操作或者 RBD 块设备卸载时都会调用 QEMU Block Driver 的 Flush 接口“。同时,一些对数据的安全性敏感的应用也可以通过操作系统在需要的时候发出 flush 指定,比如一些数据库系统,你可以使用 fio 工具的 fdatasync 参数在指定的写入操作后发出 fdatasync 指令。具体效果还待测试。
librados 的 flush API:
CEPH_RADOS_API int rados_aio_flush(rados_ioctx_t io) Block until all pending writes in an io context are safe This is not equivalent to calling rados_aio_wait_for_safe() on all write completions, since this waits for the associated callbacks to complete as well. Parameters io - the context to flush
因为 RBDCache 是利用内存来缓存数据,因此数据也是易失性的。那么,最安全的是,设置 rbd_cache_max_dirty = 0,就是不缓存数据,相当于 writethrough 的效果。很明显,这没有实现 RBDCache 的目的。
另外,Ceph 还提供 rbd_cache_writethrough_until_flush 选项,它使得 RBDCache 在收到第一个 flush 指令之前,使用 writethrough 模式,透传数据,避免数据丢失;在收到第一个 flush 指令后,开始 writeback 模式,通过 KVM barrier 功能来保证数据的可靠性。
各种配置下的Ceph RBD 缓存效果:
配置 | rbd_cache_writethrough_until_flush 的值 | 缓存效果 |
rbd cache = false | N/A | 没有读写缓存,等同于 directsync |
rbd cache = true rbd_cache_max_dirty = 0 | N/A | 只有读缓存,没有写缓存,等同于 writethrough |
rbd cache = true rbd_cache_max_dirty > 0 “cache=writeback” | True | 在收到 QEMU 发出的第一个 flush 前, 使用 writethrough 模式;收到后,使用 writeback 模式 |
rbd cache = true rbd_cache_max_dirty > 0 “cache=writeback” | False | 一直使用 writeback 模式,QEMU 会在特定时候发出 flush,可能会导致数据丢失 |
rbd cache = true rbd_cache_max_dirty > 0 “cache=none” | True | 一直使用 writethrough 模式,没有写缓存,只有读缓存 |
rbd cache = true rbd_cache_max_dirty > 0 “cache=writeback” | False | 一直使用 writeback 模式,QEMU 会发出 flush 使缓存数据写入Ceph 集群 |
参考资料:
解析Ceph: RBDCache 背后的世界
http://docs.ceph.com/docs/hammer/rbd/rbd-config-ref/
KVM storage performance and cache settings on Red Hat Enterprise Linux 6.2
http://linux.die.net/man/1/fio
http://xfs.org/index.php/XFS_FAQ#Write_barrier_support.
https://libvirt.org/formatdomain.html
https://www.quora.com/What-is-the-major-difference-between-the-buffer-cache-and-the-page-cache