一、问题初现:虚拟机IO卡成PPT
最近有朋友找我吐槽,说他们的虚拟机跑了几个数据库实例,平时慢点也就算了,现在直接卡成PPT,拷贝个文件都要等半天。用iostat一看,svctm(服务时间)和wa(等待时间)都高得离谱。更奇怪的是,宿主机上的其他虚拟机表现正常,就那几台用RBD块存储的虚拟机在遭殃。这种时候,很多人会第一时间怀疑是Ceph集群出了问题,但经过观察CPU和网络都很平静。那问题到底出在哪呢?今天就陪大家从最底层的krbd驱动开始,一路排查到QEMU的配置,看看能不能把这只“慢乌龟”揪出来。
二、先看看存储端是不是背锅侠
在怀疑驱动和QEMU之前,咱们得先确认Ceph这边是不是真的健康。毕竟如果OSD都在抖动,那后面调什么都是白搭。
2.1 检查Ceph集群健康状态
首先到Ceph管理节点上跑一下健康检查,一眼就能看出集群有没有红色警报。
# 查看Ceph集群整体状态,注意health字段
ceph -s
如果输出里面有HEALTH_WARN甚至HEALTH_ERR,那就得先处理降级的PG或者慢请求。之前遇到过一次osd_pending长时间不恢复,导致IO卡顿严重,后来发现是一块网卡松动。
2.2 查看RBD镜像的数据分布
光看健康状态还不够,咱们还得看看具体是哪个RBD镜像在拖后腿。用下面的命令能列出所有rbd镜像和它们的特性:
# 列出pool里的rbd镜像
rbd ls -p mypool
# 查看某个镜像的详细信息,包括大小、特性、条带大小
rbd info -p mypool --image vm-disk-01
这里有个小知识点:rbd info里如果看到features包含 crush-simple或者layering这类,没关系,但要注意老版本内核可能不支持某些新特性,导致性能回退。
2.3 确认OSD性能
最后,可以看下OSD的延迟统计。用ceph osd perf能快速定位是哪个OSD响应慢。
# 查看所有OSD的提交与应用延迟
ceph osd perf
如果某个OSD的commit_latency常年超过几十毫秒,基本可以断定磁盘或网络有问题。我遇到过一台机器上的SATA盘被同事误插到RAID卡上,性能差得离谱,后来把盘挪到直通控制器上,延迟直接掉了十几倍。
这一通操作下来,如果Ceph本身没问题,那就是时候怀疑虚拟机这一侧的“翻译官”了——也就是krbd驱动。
三、揪出krbd驱动的问题
3.1 krbd是什么
简单说,krbd是Linux内核里自带的RBD客户端驱动。它负责把Ceph里的RBD镜像映射成一块本地块设备,比如/dev/rbd0。QEMU和虚拟机里的操作系统去读写这块设备时,实际上是通过krbd向Ceph集群发送请求。如果这个驱动配置不合理,就算Ceph再快,IO也得排队等。
3.2 查看队列深度和配置
先看看当前rbd设备的队列设置。内核版本3.10里krbd默认的队列深度可能很浅。用cat命令直接看:
# 查看rbd0的请求队列长度
cat /sys/block/rbd0/queue/nr_requests
# 查看调度器
cat /sys/block/rbd0/queue/scheduler
很多老机器上nr_requests只有32,调度器还是cfq。如果虚拟机里并发读写高,队列一下就满了,请求只能排队等,延迟自然飙升。这时候有两个调整方向:
- 把队列深度调大,比如到128或256。
- 将调度器改成
none或noop,因为krbd底层已经做了合并和排序,内核再跑一遍cfq反而多此一举。
看怎么修改:
# 临时调整nr_requests到128
echo 128 > /sys/block/rbd0/queue/nr_requests
# 设置调度器为none(永不排队,适合SSD和RBD这种智能设备)
echo none > /sys/block/rbd0/queue/scheduler
注意,这些修改重启后会失效。要想永久生效,可以写个udev规则,或者放到开机脚本里。我们后面有例子。
3.3 调整krbd的读写请求大小
krbd还有一个隐藏参数叫rbd_order,它决定了每次下发到底层多少个字节。如果这个值太小,大IO会被拆成很多小请求,增加Ceph端的处理次数;如果太大,又会占用太多内存。一般建议在rbd create的时候设置条带大小,然后在客户端挂载时也可以调整。
比如我们挂载rbd设备时,可以加上rbd_order选项,其中rbd_order是2的幂次,默认22代表4MB条带。如果你虚拟机主要跑随机小IO,可以把条带设小点,比如rbd_order=18(256KB)。如果是顺序大IO,4MB反而更好。
在QEMU配置里,我们通常不是直接用krbd,而是用librbd。但如果你是在宿主机上直接mount这个rbd设备给虚拟机磁盘文件用,那就可以调整挂载参数。看一个挂载示例:
# 挂载rbd0到/mnt/data,指定rbd_order为18(256KB),并开启noatime
mount -t rbd -o rbd_pool=mypool,rbd_name=vm-disk-01,rbd_order=18,noatime /dev/rbd0 /mnt/data
不过这里要提醒一句,krbd的参数调整对性能的影响不是线性的,需要结合你的IO特征反复测试。之前我见过一个团队把nr_requests调到1024,结果内存占用暴涨,还触发了内核警告,最后稳在256才舒服。
3.4 检查网络对krbd的影响
krbd本身是内核态的,它跟Ceph通信走的是TCP连接。如果宿主机和Ceph集群之间的网络有丢包,那延迟会成倍增加。可以用ping和ss检查连接情况。
# 测试到Ceph Monitor的延迟和丢包
ping -c 100 -i 0.2 ceph-mon-01
# 查看与Ceph OSD之间的TCP连接状态
ss -s
如果ping有丢包,或者ss输出显示大量SYN_RECV,那说明网络栈或者交换机背板有问题。此外,还要看下网卡有没有开中断合并(coalescing),有时候合并太狠,小包延迟也会很大。
四、QEMU配置也有大学问
再往上层走,就是虚拟机与QEMU的交互层了。QEMU通过librbd直接跟Ceph通信,跟krbd不是一条路。这里面的配置项又多又隐蔽,稍微不注意就掉坑里。
4.1 虚拟机磁盘的cache模式
libvirt中定义虚拟机磁盘时,有一个cache属性,常见的有none、writeback、writethrough。很多人图省事直接设成writeback,觉得能提升性能。但问题来了,如果宿主机突然断电,writeback模式下的数据可能还没刷到Ceph里,一旦丢盘,你哭都来不及。对于虚拟机里的数据库这种重要数据,建议老老实实设为none,让Ceph自己的缓存机制来保证一致性。
来看一个libvirt磁盘配置片段:
<disk type='network' device='disk'>
<driver name='qemu' type='raw' cache='none' io='native'/>
<source protocol='rbd' name='mypool/vm-disk-01'>
<host name='192.168.1.10' port='6789'/>
</source>
<target dev='vda' bus='virtio'/>
</disk>
注意这里io='native'表示QEMU直接使用Linux原生AIO,如果改成threads则绕道线程池,性能和稳定性都会下降。
4.2 IO线程与virtio队列
现代虚拟化都支持多队列virtio。如果虚拟机内核支持,并且QEMU开启了多队列,那么IO请求可以分散到多个CPU核心上处理。反之,所有IO都由一个线程串行执行,再快的磁盘也会被卡住。
检查虚拟机里是否有多队列:
# 在虚拟机内部查看vda的中断号分配
cat /proc/interrupts | grep vda
如果只有一个中断号在处理,说明virtio队列数只有一个。可以在宿主机上修改虚拟机配置,增加 virtio 设备的队列数。下面是一个libvirt接口配置的例子(注意要改成你的实际网络接口):
<interface type='bridge'>
<source bridge='br-vm'/>
<model type='virtio'/>
<driver name='vhost' queues='4'>
<host mq='on'/>
<guest mq='on'/>
</driver>
</interface>
同时,虚拟机内的优化也很关键。比如在虚拟机里使用ethtool -L组合接收队列,并开启devm_poll,这样性能会有明显改善。但虚拟机里的配置不在本文范围,这里先不展开。
4.3 块设备的多队列支持
配置文件里除了网络,磁盘也可以开多队列。在libvirt中给磁盘的<driver>增加queues属性:
<disk type='network' device='disk'>
<driver name='qemu' type='raw' cache='none' io='native' queues='2'/>
<source protocol='rbd' name='mypool/vm-disk-02'>
<host name='192.168.1.11' port='6789'/>
</source>
<target dev='vdb' bus='virtio'/>
</disk>
这里queues='2'表示使用两个IO线程处理该磁盘的请求。如果你有4个CPU核心,建议设成4,提高并行度。但也要小心,队列数超过核数反而会导致上下文切换开销变大。
另外,QEMU的iothread机制可以将IO处理线程绑定到指定CPU,从而避免与虚拟CPU争抢资源。看一个完整的libvirt配置:
<iothreads>2</iothreads>
<vcpu placement='static'>4</vcpu>
<cputune>
<vcpupin vcpu='0' cpuset='0'/>
<vcpupin vcpu='1' cpuset='1'/>
<vcpupin vcpu='2' cpuset='2'/>
<vcpupin vcpu='3' cpuset='3'/>
<iothreadpin iothread='1' cpuset='4'/>
<iothreadpin iothread='2' cpuset='5'/>
</cputune>
注意:如果你的系统不支持iothread,会直接报错。所以要先确认你的QEMU版本和libvirt支持情况。
4.4 别忘了虚拟机内部的IO调度器
即使宿主机这边做得很完美,虚拟机内部的操作系统如果还在用老的调度器,照样会拖后腿。我们可以在虚拟机里把磁盘调度器改成none,减少无谓的合并工作。
在虚拟机内执行:
# 查看当前调度器
cat /sys/block/vda/queue/scheduler
# 临时设置为none
echo none > /sys/block/vda/queue/scheduler
# 如果想永久生效,可以加到rc.local或systemd服务里
五、综合优化案例:一个真实排障过程
理论说了这么多,不如看一个实际案例。假设我们有一台宿主机,配置了4核8GB,跑了两台虚拟机,都使用RBD块存储。用户反馈其中一台VM在高峰时段IO延迟高达300ms,于是我按下面的步骤走了一遍。
5.1 环境描述
- 宿主机:CentOS 7.9,内核3.10.x,qemu-kvm 1.5.3,libvirt 2.0.0
- Ceph集群:Luminous版本,3个OSD节点,万兆网卡
- 虚拟机:2 vCPU,4GB内存,系统盘是RBD镜像,数据盘也是RBD镜像
5.2 一步步排查
先检查Ceph状态,ceph -s输出一切正常,没有PG异常。接着用ceph osd perf发现latency都在10ms以内,排除存储端。
然后到宿主机上看rbd设备。因为虚拟机数据盘是直接映射的,不是通过krbd,所以宿主机上没有kbrd设备。但虚拟机的系统盘是模板克隆的,实际上也是直接给QEMU使用。我们需要看QEMU的IO线程和缓存模式。
用virsh edit 虚拟机名修改配置,把磁盘驱动改成如下:
<driver name='qemu' type='raw' cache='none' io='native' queues='2'/>
同时给虚拟机增加两个iothread,并绑定到CPU4和CPU5上,避免和vCPU抢资源。修改虚拟机内部的/etc/fstab,加上noatime挂载参数,减少写操作。
重启虚拟机后,用virsh vcpuinfo和virsh iothreadinfo确认配置生效。再看延迟,fio测试结果:
# 在虚拟机内运行fio随机写测试,时间60秒,队列深度32
fio --name=randwrite --rw=randwrite --bs=4k --direct=1 --ioengine=libaio --iodepth=32 --numjobs=4 --runtime=60 --group_reporting
优化前平均延迟约200ms,优化后平均延迟约20ms,效果非常明显。
5.3 最终配置对比
我们整理一下优化前后的关键变更:
| 项目 | 优化前 | 优化后 |
|---|---|---|
| cache模式 | writeback | none |
| io模式 | threads | native |
| 磁盘队列数 | 1 | 2 |
| iothread | 无 | 绑定到独立CPU |
| 挂载参数 | 默认 | noatime |
这里要注意,cache=none虽然会牺牲一点宿主机的写缓存,但换来了数据安全性和稳定性。如果你的业务允许丢数据,理论上可以选writeback,但强烈不推荐在关键业务上这么做。
六、注意事项与避坑指南
调优过程中有几个坑需要特别留意。
- 不要一上来就疯狂调大队列。队列深度增大意味着内存中排队的请求增多,可能造成内存压力,尤其是虚拟机数量多的时候。建议从64开始,用fio压测,逐步增加。
- 修改QEMU配置前一定要备份。用
virsh dumpxml导出当前配置,防止改坏后无法恢复。 - 检查你的内核版本是否支持某些特性。比如iothread在老的qemu-kvm上可能没有,需要升级。
- 注意Ceph版本与内核的兼容性。老内核的krbd不支持新的Ceph特性,会导致挂载失败或性能降级,建议把宿主机内核升级到4.x以上。
- ceph的
rbd cache配置也可能产生影响。如果你用的是librbd,需要在ceph.conf里调rbd_cache = true和rbd_cache_size,但要注意缓存一致性,如果虚拟机里跑了数据库,建议谨慎开启。
七、总结
绕了一大圈,可以发现虚拟机IO延迟高并不一定就是Ceph集群的锅。从krbd驱动到QEMU配置,每一层都可能成为瓶颈。我们通过检查Ceph健康状态、调整krbd的队列深度和请求大小、优化QEMU的缓存模式和IO线程,成功地让延迟从200ms降到了20ms。这次排障也告诉我们,遇到问题不要急着改代码,先理清数据链路,一层一层排除,才能精准定位。希望这篇文章能帮到你,下次遇到类似问题,心里就能有个谱了。
评论
围绕“虚拟机使用RBD块存储时IO延迟居高不下,从krbd驱动到QEMU配置深入排查性能瓶颈问题”参与讨论