介绍
最近,很多人都会问:如何设置 QEMU/KVM 以获取最高的性能?
虽然,过去很多人都在测试或者使用 Ceph 过程中进行调优设置以获取最佳性能,但到目前而言,还没有一个最优的最新数据。通常,我们在使用 Ceph 的时候,有经验的工程师往往会通过消除系统高级别的性能瓶颈来优化。
这可能意味着可能会通过 librbd 与同步 IO 隔离测试单个OSD的延迟,或者使用大量有高 IO 深度的客户端在裸机上的 OSD 集群上产生大量IO。在这种情况下,请求是用大量并发 IO 驱动由 librbd 支持的单个 QEMU/KVM,并查看其速度。
下文,我们将了解 QEMU/KVM 在使用 Ceph 的 librbd 驱动程序时的执行速度。
集群设置
所有节点都位于同一个 Juniper QFX5200 交换机上,并通过单个 100GbE QSFP28 链路连接。虽然集群有 10 个节点,但在决定最终设置之前我们也评估了各种配置。最终使用 5 个节点作为 OSD 主机,总共有 30 个 NVMe 支持的 OSD。
此设置的预期总体性能约为 1M 随机读取 IOPS 和至少 250K 随机写入 IOPS(在 3 副本的场景下),这足以测试单个 VM 的 QEMU/KVM 性能。集群中剩余的一个节点用作 VM 客户端主机。不过,在配置 VM 之前,使用 CBT (https://github.com/ceph/cbt/) 构建了几个测试集群,并使用 fio 的 librbd 引擎运行测试工作负载以获得基线结果。
基线测试
CBT 的配置为匹配 Ceph 环境修改了一些,而不是使用默认的配置。首先,禁用了 rbd 缓存 (1),每个 OSD 被分配了一个 8GB 的 OSD 内存 traget,并且在初始测试中禁用了 cephx ,并且使用 msgr V1(但在以后的测试中使用安全模式下的 msgr V2 启用了 cephx)。创建集群后,CBT 配置为使用带有 librbd 引擎的 fio 创建一个 6TB RBD 卷,然后通过 iodepth=128 的 fio 执行 16KB 随机读取 5 分钟。由于使用 CBT 重新创建集群和运行多个基线测试非常简单,因此,下面测试了几种不同的集群大小以获得 librbd 引擎和基于 kernel-rbd 的 libaio 引擎的基线结果。
在集群级别禁用 RBD 缓存将对使用 librbd 引擎的 fio 有效,但不会对 QEMU/KVM 的 librbd 驱动程序有效。相反,cache=none 必须通过 qemu-kvm 的驱动部分显式传递。
Kernel-RBD 在从单个 OSD 读取时表现非常出色,但 Librbd 在完整的 30 个 OSD Ceph 集群中以略高于 122K IOPS 的速度实现了最高性能。librbd 和 kernel-rbd 在 5 OSD Ceph 集群上的表现几乎一样。
尽管如此,在 5 个节点、30 个 OSD Ceph 集群上我们执行了进一步的测试。此场景更好地模仿了用户在小规模但更贴近真实环境并且配置 NVMe 的 Ceph 集群上可能看到的结果。
虚拟机部署
使用 RBD 部署和引导 QEMU 虚拟机镜像相当简单了。
1. 下载镜像
使用了 CentOS8 Stream qcow2 映像,并注入了 root 密码和公钥以便于访问:
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. RBD 镜像池创建、初始化和设置 LibVirt 身份验证
这用于存储 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. 完成设置虚拟机并预填充基准数据
最后,从 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. 默认情况
众所周知,在 ide 和 virtio-blk 等 qemu 设备之间存在相当显着的性能差异。QEMU/KVM 配置为使用其默认值的性能如何?
/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 (https://github.com/markhpc/uwpmp/) ) 分析了 QEMU/KVM。有各种各样的问题,包括不匹配的 debug symbols 和其他烦人的问题。为了防止结果不标准,我们进行了数十次测试。最终推动测试继续的是对 QEMU 调用 librbd 缓存层的 wallclock 配置文件调整。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 缓存带来了相当大的性能改进,但幸运的是这还没有结束。在运行 wallclock profiler 时,我们注意到不仅有大量时间花在 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 编写并由 Jason Dillaman 在 RBD 中实现的 boost::asio IO 路径返工有关。可以设置 LD_LIBRARY_PATH 来告诉 qemu-kvm 使用编译 Ceph (v16.2.9) 时安装在 /usr/local 中的新版本 librbd。
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 测试要快一些!
更大镜像的测试
到目前为止,只有一个直接位于 RBD 块设备上的小型 (20G) 数据集用于测试。为了使这些测试更真实,更接近我们的基线测试,可以安装一个 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 randread 工作量怎么样?
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 (https://github.com/markhpc/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 加密
到目前为止,这些测试都使用了完全禁用 CephX 的 Msgr V1。这不是运行真实集群的一种非常现实的方式。唯一应该像这样配置集群的情况是,网络以及 Ceph 客户端是完全可靠信任的。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%,但是这次(反向)wallclock 配置文件看起来有点不同:
+ 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 都会遍历每个段并依次解密各个缓冲区(每个段可能有多个!)。IE 的伪代码看起来像这样:
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
看来,在这个处理器上,当我们只处理 16K 块并利用一个完整的核心来进行 AES 解密时,我们应该能够达到接近 4GB/s(256K 块/秒)的速度。即使是 1-8K 的块处理速度也很快,但非常小的块对解密性能有重大影响。
这可能有助于解释启用安全模式时的性能损失。如果 3 个 IO 线程中的每一个都在 libssl 函数中花费 14-20% 的时间(并非所有都显示在上面的配置文件片段中),那么根据 openssl 速度测试,预期应该可以实现 120-130K IOPS。如果可以减少 msgr 线程之间的争用,那么额外的 msgr 线程可能能够以增加 CPU 使用率为代价来提高性能。
结论
这篇文章介绍了如何为 VM 存储调整以及优化 QEMU/KVM 和 librbd 的性能。
对于 16K 的 IO,qemu+librbd 经过仔细调优后,可以从单个 VM 实现 64-67K 的随机写入 IOPS 和 123K 的随机读取 IOPS。即使在使用 libssl 的 AES-NI 支持时,在 Ceph 中启用 128 位在线 AES 加密也会对性能产生显着影响(30% 以上)。
在加密和未加密的情况下,性能似乎主要受到饱和 msgr 线程的限制。有一些迹象表明,线程之间的争用可能在限制单客户端性能方面发挥了作用。在启用AES加密的情况下,帧段的分解和解密顺序可能会影响性能。似乎没有证据表明 virtio-blk-pci 达到了极限。
*原文链接:https://ceph.io/en/news/blog/2022/qemu-kvm-tuning/