QEMU/KVM + Ceph Librbd 性能

2022年10月24日 Mark Nelson (nhm)

简介

Ceph 团队最近被问到,使用 librbd 测试过的性能最高的 QEMU/KVM 配置是什么。虽然过去已经收集过虚拟机基准测试数据,但没有来自高性能配置的最新数据。通常 Ceph 工程师会尝试通过消除堆栈更高层级的瓶颈来隔离特定组件的性能。这可能意味着使用同步 IO 通过 librbd 隔离测试单个 OSD 的延迟,或者使用大量客户端和高 IO 深度向裸机上的 OSD 集群发送大量的 IO。在本例中,请求是驱动一个由 librbd 支持的单个 QEMU/KVM 虚拟机,并使用高并发 IO,看看它能达到多快的速度。请继续阅读,了解 QEMU/KVM 在利用 Ceph 的 librbd 驱动程序时能够达到的速度。

致谢

感谢 Red Hat 和 Samsung 向 Ceph 社区提供用于本次测试的硬件。感谢 Adam Emerson 和 Ceph 团队中所有致力于客户端性能改进的人,正是他们的工作使这些结果成为可能。最后,感谢 QEMU 维护者 Stefan Hajnoczi 提供有关 QEMU/KVM 的专业知识并审查了本文的草稿。

集群设置

节点10 x Dell PowerEdge R6515
CPU1 x AMD EPYC 7742 64C/128T
内存128GiB DDR4
网络1 x 100GbE Mellanox ConnectX-6
NVMe6 x 4TB Samsung PM983
操作系统版本CentOS Stream release 8
Ceph 版本Pacific V16.2.9(从源代码构建)
qemu-kvm 版本qemu-kvm-6.2.0-20.module_el8.7.0+1218+f626c2ff.1

所有节点都位于同一 Juniper QFX5200 交换机上,并通过单个 100GbE QSFP28 链路连接。虽然该集群有 10 个节点,但在确定最终配置之前,评估了各种配置。最终使用了 5 个节点作为 OSD 主机,总共拥有 30 个基于 NVMe 的 OSD。此配置的预期聚合性能约为 1M 随机读 IOPS 和至少 250K 随机写 IOPS(经过 3 倍复制后),这应该足以测试单个 VM 的 QEMU/KVM 性能。集群中的其余节点之一被用作 VM 主机。在配置 VM 之前,使用 CBT 构建了几个测试集群,并使用 fio 的 librbd 引擎运行了一个测试工作负载,以获得基线结果。

基准测试

CBT 被配置为使用一些修改后的设置来部署 Ceph,而不是默认设置。主要是在集群级别禁用了 rbd 缓存 (1),每个 OSD 获得了 8GB 的 OSD 内存目标,并且 msgr V1 被使用,并且在初始测试中禁用了 cephx(但 cephx 在使用 msgr V2 的安全模式下启用用于后续测试)。集群创建后,CBT 被配置为使用 fio 和 librbd 引擎创建一个 6TB RBD 卷,然后使用 iodepth=128 进行 16KB 随机读取 5 分钟。由于使用 CBT 重新创建集群和运行多个基准测试非常容易,因此测试了几个不同的集群大小,以获得 librbd 引擎和基于 kernel-rbd 的 libaio 引擎的基线结果。

  1. 禁用集群级别的 RBD 缓存将受到使用 librbd 引擎的 fio 的尊重,但不会受到 QEMU/KVM 的 librbd 驱动程序的尊重。相反,必须通过 qemu-kvm 的驱动部分显式传递 cache=none。

当从单个 OSD 读取时,Kernel-RBD 表现非常好,但在完整的 30 OSD 配置中,Librbd 实现了最高的性能,略高于 122K IOPS。Librbd 和 kernel-rbd 在 5 OSD 设置上的表现几乎一样好。尽管如此,还是对 5 节点、30 OSD 配置进行了进一步的测试。此设置更好地模拟了用户可能在小型但现实的 NVMe 支持的 Ceph 集群上看到的情况。

VM 部署

一旦知道该怎么做,部署和启动带有 RBD 的 QEMU 镜像就相当简单。

1. 下载镜像。

使用带有 root 密码和公共密钥注入的 CentOS8 Stream qcow2 镜像,以便于访问

wget https://cloud.centos.org/centos/8-stream/x86_64/images/CentOS-Stream-GenericCloud-8-20220913.0.x86_64.qcow2
virt-sysprep -a ~/CentOS-Stream-GenericCloud-8-20220913.0.x86_64.qcow2 --root-password password:123456 --ssh-inject root:file:/home/nhm/.ssh/id_rsa.pub 

2. 创建、初始化并设置 LibVirt 身份验证以获取 RBD 镜像池

用于存储 RBD 镜像。

sudo /usr/local/bin/ceph osd pool create libvirt-pool 
sudo /usr/local/bin/rbd pool init libvirt-pool
sudo /usr/local/bin/ceph auth get-or-create client.libvirt mon 'profile rbd' osd 'profile rbd pool=libvirt-pool'  

3. 将 qcow2 镜像转换为 Ceph RBD 镜像并调整大小

调整大小以便为基准测试留出一些空间!

qemu-img convert -f qcow2 -O raw ./CentOS-Stream-GenericCloud-8-20220913.0.x86_64.qcow2 rbd:libvirt-pool/CentOS8 
qemu-img resize rbd:libvirt-pool/CentOS8 6000G 

4. 完成 VM 设置并预填充基准测试数据

最后,从 RBD 启动 VM,登录,设置分区,并预填充一个 FIO 文件进行测试。在本例中,仅使用 20GB 的镜像部分,但不用担心。稍后将提供有关在真实 XFS 文件系统上使用更大(2TB)文件的结果。

/usr/libexec/qemu-kvm -m 16384 -smp 16,sockets=1,cores=16,threads=1 -drive format=raw,file=rbd:libvirt-pool/CentOS8 -net nic -net user,hostfwd=tcp::2222-:22
ssh -p 2222 root@localhost
sudo yum install fio
cfdisk /dev/sda # Create a 2TB partition here (maximum size due to the partition type for image, oh well)
fio --ioengine=libaio --rw=write --numjobs=1 --bs=4M --iodepth=128 --size=20G --name=/dev/sda2 

基准测试 VM

1. 默认情况

众所周知,qemu 设备(如 ide 和 virtio-blk)之间存在相当大的性能差异。如果 QEMU 配置为使用其默认设置,性能如何?

/usr/libexec/qemu-kvm -m 16384 -smp 16,sockets=1,cores=16,threads=1 -drive format=raw,file=rbd:libvirt-pool/CentOS8 -net nic -net user,hostfwd=tcp::2222-:22

fio --ioengine=libaio --direct=1 --bs=16384 --iodepth=128 --rw=randread --norandommap --size=20G --numjobs=1 --runtime=300 --time_based --name=/dev/sda2 
read: IOPS=2484, BW=38.8MiB/s (40.7MB/s)(11.4GiB/300001msec) 

非常糟糕!virtio-blk 表现如何?

2. 使用 virtio-blk-pci

/usr/libexec/qemu-kvm -m 16384 -smp 16,sockets=1,cores=16,threads=1 -drive format=raw,id=rbd0,if=none,file=rbd:libvirt-pool/CentOS8 -device virtio-blk-pci,drive=rbd0,id=virtioblk0 -net nic -net user,hostfwd=tcp::2222-:22

fio --ioengine=libaio --direct=1 --bs=16384 --iodepth=128 --rw=randread --norandommap --size=20G --numjobs=1 --runtime=300 --time_based --name=/dev/vda2
read: IOPS=24.9k, BW=390MiB/s (409MB/s)(114GiB/300005msec) 

很大的改进。现在是时候添加一个单独的 iothread 了。

3. 添加一个单独的 IO 线程

/usr/libexec/qemu-kvm -m 16384 -smp 16,sockets=1,cores=16,threads=1 -drive format=raw,id=rbd0,if=none,file=rbd:libvirt-pool/CentOS8 -object iothread,id=iothread0 -device virtio-blk-pci,iothread=iothread0,drive=rbd0,id=virtioblk0 -net nic -net user,hostfwd=tcp::2222-:22

fio --ioengine=libaio --direct=1 --bs=16384 --iodepth=128 --rw=randread --norandommap --size=20G --numjobs=1 --runtime=300 --time_based --name=/dev/vda2 
read: IOPS=26.0k, BW=407MiB/s (426MB/s)(119GiB/300005msec) 

更好,但仍然很慢。此时,我使用 (uwpmp) 对 QEMU/KVM 进行了分析。存在各种问题,包括不匹配的调试符号和其他令人讨厌的问题。在获得任何运气之前,进行了数十次测试。最终推动测试前进的事情是基于壁钟剖析的观察,即 QEMU 正在调用 librbd 的缓存层。然后我记起 QEMU 中的 librbd 驱动程序会覆盖 Ceph 全局配置中设置的任何内容。要在 QEMU/KVM 中禁用 RBD 缓存(这在快速集群上很重要),必须在 qemu-kvm 的驱动配置中显式设置 cache=none。

4. 禁用 LibRBD 驱动程序缓存

/usr/libexec/qemu-kvm -m 16384 -smp 16,sockets=1,cores=16,threads=1 -drive format=raw,id=rbd0,if=none,cache=none,file=rbd:libvirt-pool/CentOS8 -object iothread,id=iothread0 -device virtio-blk-pci,iothread=iothread0,drive=rbd0,id=virtioblk0 -net nic -net user,hostfwd=tcp::2222-:22

fio --ioengine=libaio --direct=1 --bs=16384 --iodepth=128 --rw=randread --norandommap --size=20G --numjobs=1 --runtime=300 --time_based --name=/dev/vda2 
read: IOPS=53.5k, BW=836MiB/s (876MB/s)(245GiB/300003msec) 

禁用 LibRBD 缓存带来了一个很大的胜利,但幸运的是这并不是结局。在运行壁钟剖析器时,我注意到不仅花费了大量时间在 rbd 缓存中,还在 libc 内存分配例程中。Ceph 的内存模型通常涉及创建许多小的临时对象,这些对象会碎片化内存并对内存分配器造成极大的负担。TCMalloc(和 JEMalloc)往往比 libc malloc 更好地处理 Ceph 的行为。幸运的是,可以通过 LD_PRELOAD 指令注入 TCMalloc。

5. 将内存分配器切换到 TCMalloc

LD_PRELOAD="/usr/lib64/libtcmalloc.so" /usr/libexec/qemu-kvm -m 16384 -smp 16,sockets=1,cores=16,threads=1 -drive format=raw,id=rbd0,if=none,cache=none,file=rbd:libvirt-pool/CentOS8 -device virtio-blk-pci,drive=rbd0,id=virtioblk0 -net nic -net user,hostfwd=tcp::2222-:22

fio --ioengine=libaio --direct=1 --bs=16384 --iodepth=128 --rw=randread --norandommap --size=20G --numjobs=1 --runtime=300 --time_based --name=/dev/vda2 
read: IOPS=80.0k, BW=1250MiB/s (1311MB/s)(366GiB/300003msec) 

另一个很大的胜利,但还能更快吗?

6. 使用新版本的 LibRBD

到目前为止,CentOS Stream8 附带的系统 librbd 版本一直用于此测试。它非常旧,并且自那以来已经进行了值得注意的改进。这些主要与 Adam Emerson 编写的 boost::asio IO 路径重构以及 Jason Dillaman 在 RBD 中实现的有关。

LD_LIBRARY_PATH=/usr/local/lib:/usr/local/lib64
LD_PRELOAD="/usr/lib64/libtcmalloc.so" /usr/libexec/qemu-kvm -m 16384 -smp 16,sockets=1,cores=16,threads=1 -drive format=raw,id=rbd0,if=none,cache=none,aio=native,file=rbd:libvirt-pool/CentOS8 -object iothread,id=iothread0 -device virtio-blk-pci,iothread=iothread0,drive=rbd0,id=virtioblk0 -net nic -net user,hostfwd=tcp::2222-:22

fio --ioengine=libaio --direct=1 --bs=16384 --iodepth=128 --rw=randread --norandommap --size=20G --numjobs=1 --runtime=300 --time_based --name=/dev/vda2
read: IOPS=126k, BW=1964MiB/s (2060MB/s)(575GiB/300002msec) 

新版本的 librbd 极大地提高了性能,结果现在略快于直接使用 librbd 进行基线 fio 测试!

更大的测试

到目前为止,只使用了一个小的(20G)数据集直接在 RBD 块设备上进行测试。为了使这些测试更逼真,更接近我们的基线测试的大小,可以安装一个 XFS 文件系统并预填充 2TB 的数据(遗憾的是受到上述分区大小限制的限制)。

mkfs.xfs /dev/vda2
mount /dev/vda2 /mnt
fio --ioengine=libaio --direct=1 --rw=write --numjobs=1 --bs=4M --iodepth=16 --size=2000G --name=/mnt/foo 
write: IOPS=607, BW=2429MiB/s (2547MB/s)(2000GiB/843305msec); 0 zone resets 

不错。即使使用相当适度的 iodepth=16,fio 也能以大致 NVMe 速度填充 RBD 卷。16K 随机读取工作负载如何?

16K 随机读取

fio --ioengine=libaio --direct=1 --bs=16384 --iodepth=128 --rw=randread --norandommap --size=2000G --numjobs=1 --runtime=300 --time_based --name=/mnt/foo 
read: IOPS=123k, BW=1916MiB/s (2009MB/s)(561GiB/300002msec) 

...仅供娱乐,16K 随机写入如何?

16K 随机写入

fio --ioengine=libaio --direct=1 --bs=16384 --iodepth=128 --rw=randwrite --norandommap --size=2000G --numjobs=1 --runtime=300 --time_based --name=/mnt/foo 
write: IOPS=64.1k, BW=1001MiB/s (1050MB/s)(293GiB/300003msec); 0 zone resets 

不如本地 NVMe 驱动器快,但对于单个 VM 来说还不错。值得注意的是,在随机读取测试期间,Ceph 的所有三个异步 msgr 线程都在 100% CPU 上运行。在使用 (uwpmp) 对 qemu-kvm 进程进行分析时,测试正在运行时显示了大量工作正在进行中,并且在 librbd 侧没有明显的快速优化领域。但是,将 boost::asio 进一步深入堆栈可能会提供额外的改进。

进一步的 QEMU 优化?

在完成上述测试后,我联系了 QEMU 维护者 Stefan Hajnoczi,以了解他对结果的看法。他提供了一些额外的选项来尝试

  • 较新的 --blockdev rbd,node-name=rbd0,cache.direct=on,pool=libvirt-pool,image=CentOS8 语法省略了 'raw' 驱动程序,从而略微提高了速度...I/O 请求会直接从模拟的 virtio-blk 设备发送到 rbd 驱动程序,使用这种新的语法。
  • 使用 -M q35 获取现代机器类型。

事实证明,测试起来非常容易,我们可以使用新的语法重新启动 qemu-kvm

LD_LIBRARY_PATH=/usr/local/lib:/usr/local/lib64
LD_PRELOAD="/usr/lib64/libtcmalloc.so" /usr/libexec/qemu-kvm -m 16384 -smp 16,sockets=1,cores=16,threads=1 --blockdev rbd,node-name=rbd0,cache.direct=on,pool=libvirt-pool,image=CentOS8 -M q35 -object iothread,id=iothread0 -device virtio-blk-pci,iothread=iothread0,drive=rbd0,id=virtioblk0 -net nic -net user,hostfwd=tcp::2222-:22 

最终,使用新的语法,性能非常接近,读取速度可能略慢,写入速度可能略快,但需要进一步的测试才能确定结果是否具有统计意义。

Msgr V2 和 AES 加密

到目前为止,这些测试都使用了 Msgr V1,并且完全禁用了 CephX。这并不是运行真实集群的非常现实的方式。只有当集群受到外界的良好保护并且客户端受到隐式信任时,才应以这种方式配置集群。Ceph 的默认身份验证模式允许进行身份验证并防止中间人攻击。Ceph 还可以以“安全”模式运行,该模式还提供基于线上的 AES-128-GCM 加密。可以通过将几个 Ceph 配置选项设置为“安全”来启用此功能

ms_client_mode = secure
ms_cluster_mode = secure
ms_service_mode = secure
ms_mon_client_mode = secure
ms_mon_cluster_mode = secure
ms_mon_service_mode = secure

这些设置对基线测试中的客户端性能有什么影响?

这里的下降似乎不太糟糕,但重复“更大的测试”部分中的 QEMU/KVM 随机读取测试,结果为大约 87K IOPS,而没有加密时为 123K IOPS。在 qemu-kvm 进程中,所有三个异步 msgr 线程仍然被卡在 100% CPU 上,但这一次,(反向)壁钟剖析看起来有点不同

+ 14.00% _aesni_ctr32_ghash_6x
|+ 14.00% aesni_gcm_decrypt
| + 14.00% aes_gcm_cipher
|  + 14.00% EVP_DecryptUpdate
|   + 14.00% ceph::crypto::onwire::AES128GCM_OnWireRxHandler::authenticated_decrypt_update(ceph::buffer::v15_2_0::list&)
|    + 14.00% ceph::msgr::v2::FrameAssembler::disasm_remaining_secure_rev1(ceph::buffer::v15_2_0::list*, ceph::buffer::v15_2_0::list&) const 

每个异步 msgr 线程至少花费 14% 的时间在 libssl 的 EVP_DecryptUpdate 函数中,作为帧组装工作的一部分。值得注意的是,即使在此 AMD Rome 处理器上,libssl 也正确使用了 AES-NI 指令。仔细查看代码,似乎对于每个帧,Ceph 会遍历每个段并按顺序解密单个缓冲区(可能每个段多个!)。例如,伪代码看起来像这样

Disassemble first segment
Disassemble remaining segments
  For each Segment in Segments:
    For each buffer in Segment:
      convert buffer to c string
      call EVP_DecryptUpdate on c string 

也许如果一次可以解密更大的数据块,就可以减少 AES-NI 开销。但是哪些大小真正重要?Openssl 提供了一个速度测试,可以帮助缩小范围

openssl speed -evp aes-128-gcm -decrypt
类型16 字节64 字节256 字节1024 字节8192 字节16384 字节
aes-128-gcm321729.96k1099093.40k2269449.47k3429211.27k3995912.87k4038464.85k

在此处理器上,我们应该能够处理高达近 4GB/s(每秒 256K 块)的速度,仅处理 16K 块并使用整个核心进行 AES 解密。即使是 1-8K 块也能快速处理,但非常小的块对解密性能有重大影响。这可能有助于解释启用安全模式后性能下降的原因。如果每个段都被分解并按顺序解密,那么可能需要进一步优化。在 msgr 线程之间减少争用也可能有助于提高单客户端性能。

结论

本文介绍了如何调整 QEMU/KVM 和 librbd 的性能以进行 VM 存储。对于 16K IO,qemu+librbd 可以在仔细调整后从单个 VM 实现 64-67K 随机写 IOPS 和 123K 随机读 IOPS。即使在使用 libssl 的 AES-NI 支持的情况下,在 Ceph 中启用 128 位在线 AES 加密也会产生显著(30% 以上)的性能影响。在加密和未加密的情况下,性能主要受饱和 msgr 线程的限制。有一些迹象表明,线程之间的争用可能在限制单客户端性能方面发挥了作用。在启用 AES 加密的情况下,性能可能受到帧段分解和顺序解密方式的影响。没有证据表明 virtio-blk-pci 已经达到其上限,这表明仍有改进的空间。感谢阅读!