Cache 的一些基本概念
Cache 最先指的是, 用于平衡 CPU 和内存之间的速度, 后来, 进一步发展为 一种技术, 用在速度相差较大的两种硬件之间, 用于协调两者数据传输速度差异的结构.
本文主要指的是协调内存和硬盘速度的 Cache.
在 Cache 和他的后端存储的数据同步一般有以下几种方式:
- Write through: 数据一旦在 Cache 中, 就马上同步到真实设备中, 也就是保持同步
- Write back: 见闻知意思, writeback 的意思就是数据一旦在 Cache 中, 这个操作就算完成了, 并不需要马上同步到真实设备中, 除非用户手动指定(fsync), 或者此 Cache 中得内容发生了改变
QEMU 中的 Cache 模型
由于虚拟化的关系, QEMU 中的 Cache 模型有一点复杂.
在虚拟化的世界中, 一个对象通常有两端, guest 端和 host 端, 或者称为 frontend 和 backend. 比如 vcpu 对象, 在 frontend 是一个 CPU, 在 backend 端, 它只是 一个线程, 对磁盘来讲, frontend 端看到 的是一个磁盘设备, 在 backend 端, 仅仅 是一个普通的文件而已.
所以 QEMU 中的 Cache, 就有两种情况, guest(frontend) 看到的 disk 的 Cache, 和 host(backend)看到的那个普通文件的 Cache. QEMU 需要对前者进行模拟, 对后者 需要管理. 后面会用代码详细解释 QEMU 是怎么实现的.
先遍历一下 QEMU 中 Cache 模式:
Cache Mode | Host Cache | Guest Disk Cache |
---|---|---|
none | off | on |
writethrough | on | off |
writeback | on | on |
unsafe | on | ignore |
directsync | off | off |
实现的代码分析
为了方便, 我选择使用 ide 的 device 和 raw 的磁盘格式.
host end 初始化 block 的流程
根据上面所说的, 模拟磁盘的初始化分为前端(磁盘设备)和后端(镜像文件)的初始化, 在后端的初始化中, 入口函数是 drive_init(), 到最后的 qemu_open() 终止. 上图 中红色得模块就是 Cache 初始化最重要的两个地方.
简单的分析一下流程.
接下来的流程稍微复杂一点:
blockdev_init -> bdrv_open -> bdrv_file_open -> raw_open -> raw_open_common -> raw_parse_flags -> qemu_open
首先是从 blockdev_init 到 raw_open 的流程, 简单分析如下:
最后剩下核心的 raw_open 函数, 这个函数和 host side 的 Cache 相关, 通过上面 的分析, 已经比较简单了.
guest end cache 的设置
在上面的的 bdrv_open_common() 函数中, 会设置 BlockDriverState->enable_write_cache 成员, 这个成员表示 guest 默认是否启用 writeback 的 Cache. 接下来会看到, guest 获取设备寄存器的时候, 会相应地用这个值填充寄存器的位, 下面以 IDE 硬盘来做例子.
guest 在初始访问 IDE 设备时, 会发送 IDENTIFY DRIVE (0xec) 指令, 设备收到这个 指令后, 需要返回一个 512 字节的信息, 包括设备的状态, 扇区数目, 等等的信息.
Cache 的工作模式
首先简单分析一下 guest 到 QEMU 的 I/O 执行路径: 下面的函数 bdrv_co_do_writev(bdrv_co_do_rw), 很好的阐释了 QEMU 怎么模拟 磁盘的 cache
那么对于 guest side, 如果磁盘有 cache, 那么 guest 是如何保证同步到磁盘上的:
guest 会根据磁盘的 cache 情况指定相应的调度策略, 来把数据从内存同步到磁盘 中, 或者用户手动指定 fsync, 这时也会发生 flush.
关于刷新的机制, 没有什么好说的, 无非就是调用系统调用 fdatasync, 如果系统不支持 fdatasync, 那么调用 fsync.
测试
使用 ubuntu-13.04 server 版本, 内核版本号 3.8.0-19. 24 cores, 20GB的 memory.
PS: 这里只是针对行的比较几种 cache 模式的性能, 不能作为专业的测试结果.
FIO 配置:
[test] rw=rw rwmixread=50 ioengine=libaio iodepth=1 direct=1 bs=4k filename=/dev/sdb runtime=180 group_reporting=1
Cache Mode | Read IOPS | Write IOPS |
---|---|---|
none | 293/235/218/200/200 | 292/234/215/199/200 |
writethrough | 5/36/72/93/113 | 5/35/73/93/113 |
writeback | 1896/1973/1944/1627/2027 | 1894/1979/1947/1627/2023 |
writethough 的性能相当糟糕, 写的性能我还能理解, 读的性能完全不能理解, 仔细 想了一下, 应该是”混合读写”导致的, “写”拖慢了”读”, 我随后单独做了一个随机读 的测试, IOPS 达到 四千多, 这次应该是正常的.
结论
通过以上的分析, writethrough 的性能很差, 因为它几乎相当于 writeback + flush, writeback 的性能最好, 但是在掉电/迁移的情况下不能保证数据安全, none 的读性能 很差, 因为他完全绕过了 kernel 的 buffer.
总的来说, 选择 cache 模式的时候, 至少要考虑以下几种情况:
- 你的存储是本地还是分布式的, 具体是那种存储, 支不支持 Direct IO(FUSE?), 一般来说网络文件系统使用 writethrough cache 性能很糟糕
- 是否需要迁移, 有的 cache 模式迁移会导致数据丢失.