RBDCache 是Ceph的块存储接口实现库 Librbd 的用来在客户端侧缓存数据的目的,它主要提供了读数据缓存,写数据汇聚写回的目的,用来提高顺序读写的性能。需要说明的是,Ceph 既支持以内核模块的方式来实现对 Linux 动态增加块设备,也支持以 QEMU Block Driver 的形式给使用 QEMU 虚拟机增加虚拟块设备,而且两者使用不同的库,前者是内核模块的形式,后者是普通的用户态库,本文讨论的 RBDCache 针对后者,前者使用内核的 Page Cache 达到目的。更多关于 Librbd 的情况参见。
Contents []
RBDCache 的实现
RBDCache 目前在 Librbd(以下统指用户态库)中主要以 Object Buffer Extent 为基本单位进行缓存,一个 RBD 块设备在 Lirbd 层会以固定大小分为若干个对象,而读写请求通常会有不同的 IO 大小,每个请求的 Buffer 大小都会以 Object 为单位放到一个或多个 Object Buffer Extent 中。目前 RBDCache 只支持以内存的形式存在,因此需要提供一些策略来不断回写到 Ceph 集群来实现持久化。在 Lirbd 中有若干选项来控制 RBDCache 的大小和回写策略:
- rbd_cache_size: Librbd 能使用的最大缓存大小
- rbd_cache_max_dirty: 缓存中允许脏数据的最大值,用来控制回写大小,不能超过 rbd_cache_size
- rbd_cache_target_dirty: 开始执行回写过程的脏数据大小,不能超过 rbd_cache_max_dirty
- rbd_cache_max_dirty_age: 缓存中单个脏数据最大的存在时间,避免可能的脏数据因为迟迟未达到开始回写的要求而长时间存在
除了当满足缓存回写要求大小或者时间才会回写数据外,Librbd 提供的 Flush 接口同样能将缓存中的脏数据全部回写。
RBDCache 由于只是以内存的形式存在,因此大部分人可能会关心是否由于意外的 Kernel Crash 或者 Host 端掉电而导致潜在的数据丢失情况,那么下面就主要讨论这种情况。
Cache 在内核
熟悉 Linux Kernel 的人都知道在内核的存储体系中主要有两种缓存,一是 Page Cache,二是 Buffer Cache。Page Cache 是在 Linux IO 栈中为文件系统服务的缓存,而 Buffer Cache 是处于更下层的 Block Device 层,由于应用大部分的使用存储数据是基于文件系统,因此 Buffer Cache 实际上只是引用了 Page Cache 的数据,而只有在直接使用块设备跳过文件系统时,Page Cache 才真正掌握缓存。关于 Page Cache 和 Buffer Cache 更多的讨论参加。
这些 Cache 都由内核中专门的数据回写线程负责来刷新到块设备中,应用可以使用如 fsync(2), fdatasync(2) 之类的系统调用来完成强制执行对某个文件数据的回写。像数据一致性要求高的应用如 MySQL 这类数据库服务通常有自己的日志用来保证事务的原子性,日志的数据被要求每次事务完成前通过 fsync(2) 这类系统调用强制写到块设备上,否则可能在系统崩溃后造成数据的不一致。
而 fsync(2) 的实现取决于文件系统,文件系统会将要求数据从缓存中强制写到持久设备中。但是这里还有另外一个大“麻烦”,通常成为 Block Device Cache(块设备缓存),这类缓存并不存在归 Kernel 管理,它或许是传统磁盘上的控制器缓存,RAID 控制器缓存或者就像本文提到的 RBDCache,它主要是被块设备自己管理。
块设备缓存
传统硬件块设备提供缓存的目的与 RBDCache 的意义是一致的,它们同样面临在机器掉电情况下,存在于磁盘控制器上的缓存丢失的情况。但是现代磁盘控制器或者 RAID 卡都会配置一个小型电容用来实现在机器掉电后对缓存数据的回写,但是 Linux Kernel 无法知晓到底是否存在这类“急救”装置来实现持久性,因此,大多数文件系统在实现 fsync 这类接口时,同时会使用 Kernel Block 模块提供的 “blkdev_issue_flush” API 去给块设备发送一个 Flush Request,块设备收到 Flush Request 后就会回写自身的缓存。但是如果机器上存在电容,那么实际上 Flush Request 会大大降低文件系统的读写性能,因此,文件系统会提供如 barrier 选项来让用户选择是否需要发送 Flush Request,比如 XFS 在 mount 时就支持 “barrier=0″ 来选择不发送 Flush Request ()。
相关的文件系统对持久化的 Trick 参考。
QEMU 中的缓存
回到 RBDCache 的使用情况里,用户往往是使用 QEMU 实现的 VM 来使用 RBD 块设备,那么 Linux Kernel 中的块设备驱动是 virtio_blk。它会对块设备各种请求封装成一个消息通过 virtio 框架提供的队列发送到 QEMU 的 IO 线程,QEMU 收到请求后会转给相应的 QEMU Block Driver 来完成请求。用户在使用本地文件或者 Host 提供的 LVM 分区时,跟 RBDCache 同样性质的缓存包括了 Guest Cache 和 Host Page Cache,在本文暂且不提这种情况下的缓存,相关信息参考。
而当 QEMU Block Driver 是 RBD 时,缓存就会交给 Librbd 自身去维护,也就是一直所说的 RBDCache。用户在使用了开启 RBDCache 的 RBD 块设备 VM 时需要给 QEMU 传入 “cache=writeback” 确保 QEMU 知晓有缓存的存在,不然 QEMU 会认为后端并没有缓存而选择将 Flush Request 忽略。
QEMU 作为最终使用 Librbd 中 RBDCache 的用户,它在 VM 关闭、QEMU 支持的热迁移操作或者 RBD 块设备卸载时都会调用 QEMU Block Driver 的 Flush 接口。
RBDCache 可能造成的数据破坏
通过上面的梳理,可以发现开启 RBDCache 的 RBD 块设备实际上就是一个不带电容的磁盘,我们需要让文件系统开启 barrier 模式,幸运的是,这也是文件系统的默认情况。除此之外,因为文件系统实际上可能管理的是通过 LVM 这种逻辑卷管理工具得到的分区,因此必须确保文件系统下面的 Linux Device Mapping 层也能够支持 Flush Request,LVM 在较早版本的 Kernel 中就已经支持 Flush Request,而其他 DM-* 模块可能就会忽略该请求,这就需要用户非常明确的了解。
幸运的是,rbd 会默认开启一个叫”rbd_cache_writethrough_until_flush”的一个选项,它的作用就是为了避免一些不支持 “flush” 的 VM 来使用 RBDCache,它的主要方式是在用户开启 RBDCache 的情况下,在收到来自 VM 的第一个 Flush 请求前,它是不会在逻辑上启用 Cache 的。这样就避免了旧内核不支持 Flush 的问题。