本系列文章会总结 QEMU/KVM 和 Ceph 之间的整合:
(1)QEMU-KVM 和 Ceph RBD 的 缓存机制总结
(2)QEMU 的 RBD 驱动
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) 的实现取决于文件系统,文件系统会将要求数据从缓存中强制写到持久设备中。
仔细地看一下 fsync 函数(来源):
fsync() transfers ("flushes") all modified in-core data of (i.e., modified buffer cache pages for) the file referred to by the file descriptor fd to the disk device (or other permanent storage device) so that all changed information can be retrieved even after the system crashed or was rebooted. This includes writing through or flushing a disk cache if present. The call blocks until the device reports that the transfer has completed. It also flushes metadata information associated with the file (see stat(2)).
它会 flush 系统中所有的cache,包括 Page cache 和 Disk write cache 以及 RBDCache,将数据放入持久存储。也就说说,操作系统在 flush Page cache 的时候, RBDCache 也会被 flush。
(来源)
主要组成部分:
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 被关闭,那么写性能将下降,但是断电时数据丢失将会避免。
从该过程可以看出:
QEMU-KVM Linux 支持关闭和开启任一一个 Page cache,也就是说有四种组合模式,分别会带来不同的效果。在各种I/O的过程中,最好是绕过一个或者两个 Page cache。
写 I/O 过程比较复杂,本文其余部分会详细阐述。从 1.3 表格总结,基本上
关于 guest os page cache,看起来它主要是作为读缓存,而对于写,没有一种模式是以写入它作为写入结束标志的。
在 libvirt xml 中使用 'cache' 参数来指定driver的缓存模式,比如:
<disk type='file' device='disk'> #对于 type,'file' 表示是 host 上的文件,'network' 表示通过网络访问,比如Ceph <driver name='qemu' type='raw' cache='writeback'/>
QEMU/KVM 支持如下这些缓存模式作为 ‘cache’ 的可选值:
缓存模式 | 说明 | GUEST OS Page cache | Host OS Page cache | Disk write cache | 被认为数据写入成功 | 数据安全性 | |
cache = unsafe | 跟 writeback 类似,只是会忽略 GUEST OS 的 flush 操作,完全由 HOST OS 控制 flush |
|
E | E | Host page cache | 最不安全,只有在特定的场合才会使用 | |
Cache=writeback | I/O 写到 HOST OS Page cache 就算成功,支持 GUEST OS flush 操作。 效率最快,但是也最不安全 |
Bypass(?不确定) | E | E | Host Page Cache | 不安全. (only for temporary data where potential data loss is not a concern ) | |
Cache=none | 客户机的I/O 不会被缓存到 page cache,而是会放在 disk write cache。 这种模式对写效率比较好,因为是写到 disk cache,但是读效率不高,因为没有放到 page cache。因此,可以在大 I/O 写需求时使用这种模式。
也就是常说的 O_DIRECT I/O 模式 |
Bypass | Bypass | E | Disk write cache | 不安全. 如果要保证安全的话,需要disk cache备份电池或者电容,或者使用 fync | |
Cache=writethrough | I/O 数据会被同步写入 Host Page cache 和 disk。相当于每写一次,就会 flush 一次,将 page cache 中的数据写入持久存储。 这种模式,会将数据放入 Page cache,因此便于将来的读;而绕过 disk write cache,会导致写效率较低。因此,这是较慢的模式,适合于写I/O不大,但是读I/O相对较大的情况,最好是用在小规模的有低 I/O 需求客户机的场景中。 也就是常说的 O_SYNC I/O 模式 |
E | E | Bypass | disk | 安全 | |
Cache=DirectSync | 跟 writethrough 类似,只是不写入 HOST OS Page cache 也就是常说的 O_DIRECT & O_SYNC 模式 |
Bypass | Bypass | Bypass | disk | 同 ”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 指令。
以 fio 为例,设置有两个参数时,会有 flush 指令发出:
fsync=int How many I/Os to perform before issuing an fsync(2) of dirty data. If 0, don't sync. Default: 0. fdatasync=int Like fsync, but uses fdatasync(2) instead to only sync the data parts of the file. Default: 0.
需要注意的是,频繁的发送(int 值设置的比较小),会影响 IOPS 的值。为了测得最大的IOPS,可以在测试准备阶段发一个sync,然后再收集阶段就不发sync,完全由 RBDCache 自己的机制去 flush;或者需要的话,把 int 值设得比较大,来模拟一些应用场景。
考虑到 KVM write barrier 的原理和 KVM 各种缓存模式的原理,显而易见,writeback + barrier 的方式下,可以实现 效率最高+数据安全 这种最优效果。
Linux 系统中被用于 Page cache 的主内存可以通过 free -m 命令来查看:
root@controller:~# free -m total used free shared buffers cached Mem: 4867 3586 1280 0 193 557 -/+ buffers/cache: 2834 2032 Swap: 2022 0 2022
写 Page cache:
(1)当数据被写时,通常情况下,它首先会被写入 page cache,被当作一个 dirty page 来管理。’dirty‘ 的意思是,数据还保存在 page cache 中,还需要被写入底下的持久存储。dirty pages 中的内容会被系统周期性地、以及使用诸如 sync 或者 fsync 的系统调用来写入持久存储。
root@controller:~# cat /proc/meminfo | grep Dirty Dirty: 148 kB root@controller:~# sync root@controller:~# cat /proc/meminfo | grep Dirty Dirty: 0 kB
sync writes any data buffered in memory out to disk. This can include (but is not limited to) modified superblocks, modified inodes, and delayed reads and writes.
(2)当数据被从持久性存储读出时,它也会被写入 page cache。因此,当连续两次读时,第二次会比第一次快,因为第一次读后把数据写入了 page cache,第二次就直接从这里读了。完整过程如下:
需要注意的是,需要结合应用的需要,来决定是否在写数据时一并写入 page cache。
QEMU 能够支持将主机的一个块设备映射给客户机,但是从 0.15 版本开始,就不再需要先将一个 Ceph volume 映射到主机再给客户机了。现在,QEMU 可以直接通过 librbd 来象 virtual block device 一样访问 Ceph image。这既提高了性能,也使得可以使用 RBDCache 了。
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 | 0.87 版本开始:true,启用 0.87 版本之前:false,禁用 |
rbd_cache_size | Librbd 能使用的最大缓存大小 | 32 MiB |
rbd_cache_max_dirty | 缓存中允许脏数据的最大值,用来控制回写大小,不能超过 rbd_cache_size。超过的话,应用的写入应该会被阻塞, 这时候IOPS就会下降 |
24 MiB |
rbd_cache_target_dirty | 开始执行回写过程的脏数据大小,不能超过 rbd_cache_max_dirty | 16MiB |
rbd_cache_max_dirty_age | 缓存中单个脏数据最大的存在时间,避免可能的脏数据因为迟迟未达到开始回写的要求而长时间存在 | 1 秒 |
可见,默认情况下:
也能看出,RBDCache 从空间和时间来方面,在效率和数据有效性之间做平衡。
几个重要的注意事项:
(1)QEMU 和 ceph 配置项的相互覆盖问题
http://ceph.com/docs/master/rbd/qemu-rbd/#qemu-cache-options
优先级:QEMU 命令行中的配置 > Ceph 文件中的显式配置 > QEMU 配置 > Ceph 默认配置
(2)在启用 RBDCache 时,必须在 QEMU 中配置 ”cache=writeback”,否则可能会导致数据丢失。在使用文件系统的情况下,这可能会导致文件系统损坏。
Important
If you set rbd_cache=true, you must set cache=writeback or risk data loss. Without cache=writeback, QEMU will not send flush requests to librbd. If QEMU exits uncleanly in this configuration, filesystems on top of rbd can be corrupted.
http://ceph.com/docs/master/rbd/qemu-rbd/#running-qemu-with-rbd
(3)使用 raw 格式的 Ceph 卷设备 “ <driver name='qemu' type='raw' cache='writeback'/>“
http://ceph.com/docs/master/rbd/qemu-rbd/#creating-images-with-qemu
理论上,你可以使用其他 QEMU 支持的格式比如 qcow2 或者 vmdk,但是它们会带来 overhead
The raw data format is really the only sensible format option to use with RBD. Technically, you could use other QEMU-supported formats (such as qcow2 or vmdk), but doing so would add additional overhead, and would also render the volume unsafe for virtual machine live migration when caching (see below) is enabled.
(4)在新版本的 Ceph 中(将来的版本,尚不知版本号),Ceph 配置项 rbd cache 将会被删除,RBDCache 是否开启将由 QEMU 配置项决定。
也就是说,如果 QEMU 中设置 cache 为 ‘none’ 的话, RBDCache 将不会被使用;设置为 ‘writeback’ 的话,RBDCache 将会被启用。参考链接:ceph : [client] rbd cache = true override qemu cache=none|writeback。
(5)对 Nova 来说,不设置 disk_cachemode 值的话,默认的 driver 的 cache 模式是 ‘none’。但是,在不支持 ‘none’ 模式的存储系统上,会改为使用 ‘writethrough’ 模式。(来源)
def disk_cachemode(self): if self._disk_cachemode is None: # We prefer 'none' for consistent performance, host crash # safety & migration correctness by avoiding host page cache. # Some filesystems (eg GlusterFS via FUSE) don't support # O_DIRECT though. For those we fallback to 'writethrough' # which gives host crash safety, and is safe for migration # provided the filesystem is cache coherant (cluster filesystems # typically are, but things like NFS are not). self._disk_cachemode = "none" if not self._supports_direct_io(FLAGS.instances_path): self._disk_cachemode = "writethrough" return self._disk_cachemode
有两种类型的 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
下面是一个 Linux 系统上文件操作的伪代码(来源)。可见,该程序知道只有在 fdatasync 执行成功后,数据才算写入成功。
#include "stdlib.h" /* for exit */ #include "unistd.h" /* for write fdatasync*/ #include "fcntl.h" /* for open */ int main(void){ int fd; if((fd=open("/home/zzx/test.file",O_WRONLY|O_APPEND|O_DSYNC))<0){ exit(1); } char buff[]="abcdef"; if(write(fd,buff,6)!= 6){ exit(2); } if(fdatasync(fd)==-1){ exit(3); } exit(0); }
因为 RBDCache 是利用内存来缓存数据,因此数据也是易失性的。那么,最安全的是,设置 rbd_cache_max_dirty = 0,就是不缓存数据,相当于 writethrough 的效果。很明显,这没有实现 RBDCache 的目的。
另外,Ceph 还提供 rbd_cache_writethrough_until_flush 选项,它使得 RBDCache 在收到第一个 flush 指令之前,使用 writethrough 模式,透传数据,避免数据丢失;在收到第一个 flush 指令后,开始 writeback 模式,通过 KVM barrier 功能来保证数据的可靠性。
该选项的含义:
This option enables the cache for reads but does writethrough until we observe a FLUSH command come through, which implies that the guest OS is issuing barriers. This doesn't guarantee they are doing it properly, of course, but it means they are at least trying. Once we see a flush, we infer that writeback is safe.
该选项的默认值到底是 true 还是 false 比较坑爹:
因此,你在使用不同版本的 librbd 情况下使用默认配置时,其 IOPS 性能是有很大的区别的:
在实现上,
(1)收到第一个flush 之前,相当于 rbd_cache_max_dirty 被设置为0 了:
uint64_t init_max_dirty = cct->_conf->rbd_cache_max_dirty; if (cct->_conf->rbd_cache_writethrough_until_flush) init_max_dirty = 0;
(2)收到第一个 flush 之后,就转为 writeback 了
if (object_cacher && cct->_conf->rbd_cache_writethrough_until_flush) { md_lock.get_read(); bool flushed_before = flush_encountered; md_lock.put_read(); uint64_t max_dirty = cct->_conf->rbd_cache_max_dirty;
各种配置下的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
https://bugs.launchpad.net/charms/+source/nova-compute/+bug/1412856
http://duartes.org/gustavo/blog/post/page-cache-the-affair-between-memory-and-files/