背景
XX 团队发现,当多个虚拟机(子机)部署在同一台 S6 标卡母机上时,即使 IO 压力未达到物理机瓶颈,多子机同时下发 IO 会导致性能下降。
SPDK 侧定位发现,4 个核(线程)中仅有 2 个核处理大量 IO 请求,而另外 2 个核几乎空闲。问题的直接原因在 SPDK 单核性能达到瓶颈,最终导致子机 IO 性能劣化。
问题现象和初步分析
XX 团队使用的子机挂载的数据盘为 2 队列盘,初步怀疑子机仅通过盘的第一个队列下发 IO,导致多个盘的 IO 集中到 SPDK 的单个核上。简化模型如下:
- SPDK 使用 2 个核(CPU 0 和 CPU 1)。
- 所有盘的第一个队列(mq0)均映射到 SPDK 的 CPU 0 ,第二个队列(mq1)映射到 CPU 1。
- 多个子机的 IO 请求均通过 mq0 下发,导致 CPU 0 过载,而 CPU 1 空闲。
Linux 多队列(Multi-Queue)机制
Linux Kernel 会为硬盘的每个队列(mq)各自分配一个 cpu_list,如下所示: mq0 绑定到 CPU 0-7,mq1 绑定到 CPU 8-15 。运行于指定 CPU 的进程提交 IO 请求就会发送到 CPU 关联的 mq 上。
ubuntu@VM-16-202-ubuntu:~$ cat /sys/block/vda/mq/0/cpu_list
0, 1, 2, 3, 4, 5, 6, 7
ubuntu@VM-16-202-ubuntu:~$ cat /sys/block/vda/mq/1/cpu_list
8, 9, 10, 11, 12, 13, 14, 15
因此,首先解决 IO 请求不均问题的方案是尝试将 XX 团队的子机中的一半业务进程/线程绑定到 mq1 对应的 CPU 上。
然而,在 XX 团队的子机上查看 cpu_list 却得到了以下结果:只有 CPU 1 分配给了 mq1 。这明显不符合预期,并且在这种情况下也不可能通过将多个线程绑定到单个 CPU1 上来解决问题。
[root@VM-0-3-centos ~]# cat /sys/block/vda/mq/0/cpu_list
0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15
[root@VM-0-3-centos ~]# cat /sys/block/vda/mq/1/cpu_list
1
问题深入定位
不同母机机型的影响
很自然的,我们首先怀疑是 Centos 子机内核有 bug,但中间有一个小插曲:XX 团队的同学发现在 S5 母机上的子机,mq 对应的 cpu_list 却又是均匀的,如下所示。(这里有一个隐藏点,CentOS 上的 mq 对应的 cpu_list 和我前面贴的 ubuntu 子机的 cpu_list 不一样, CentOS 的某个 mq 对应的 cpu 是间隔的,ubuntu 的某个 mq 对应的 cpu 是连续的。)
[root@VM-0-19-centos ~]# cat /sys/block/vda/mq/0/cpu_list
0, 2, 4, 6, 8, 10, 12, 14
[root@VM-0-19-centos ~]# cat /sys/block/vda/mq/1/cpu_list
1, 3, 5, 7, 9, 11, 13, 15
不同的母机机型会导致相同的子机内核产生不同的 cpu_list 分配逻辑,这是一个很有意思的点,难道和虚拟化还相关?从这一点出发,对比两种机型上的子机的 xml 配置,发现了一个可疑的点:两者对子机 CPU 拓扑结构配置不同,同样 16 核的子机,S6 机型配置了超线程。进一步,通过在 S6 机型上将配置从 8 核 2 超线程模式修改成 16 独立核模式,重启子机后发现盘队列的 cpu_list 也就变均匀了。
# S6 机型
<topology sockets='1' cores='8' threads='2'/>
# S5 机型
<topology sockets='1' cores='16' threads='1'/>
这表明 问题与超线程(Hyper-Threading)相关,而非虚拟化(QEMU)本身的问题。进一步将 bug 范围缩小到子机内核为硬盘队列映射 cpu 的处理函数中。
Linux Kernel 多队列映射逻辑分析
CentOS 7 的长期支持(LTS)内核基于 Linux 3.10,而 Multi-Queue(blk-mq)特性在 3.13 版本引入。对比 Linux Upstream 3.13 和 CentOS 定制内核 为盘队列映射 cpu 的处理函数实现,发现了关键差异。
Linux Kernel Upstream 3.13 逻辑
首先可以看到blk_mq_update_queue_map()
函数中确实感知了超线程,一开始就怀疑是超线程处理逻辑有问题,该函数的主要逻辑如下:
- 遍历所有 online 的 cpu,并统计所有 CPU (
nr_cpus
)和非超线程 CPU (nr_uniq_cpus
)的数量; map
数组用于记录 cpu 核到队列的映射;- 若 队列数 > CPU 数 或 无超线程,则直接将 CPU 按顺序映射到盘的队列上;
- 当存在超线程核时,使用
nr_uniq_cpus
作为参数去计算 CPU 到队列号的映射关系,并且将同一个超线程的 cpu 映射到第一个超线程核已经映射的队列号上。例如,CPU 0 和 CPU 1 互为超线程,首先 CPU 0 会通过cpu_to_queue_index
计算出来映射到 mq0,继续循环到 CPU 1,发现该 CPU 对应于超线程 CPU 0,因此执行map[i] = map[first_sibling];
将当前 CPU 1 也映射到 mq0;
在详细分析 blk_mq_update_queue_map
函数的逻辑后,我们发现就算存在超线程,CPU 在多队列间应该也是均衡的。并且在不存在超线程的情况下,最后的结果应该也不是 CentOS 上 cpu_list 分配的表现,也应该是前 8 个 CPU 映射到 mq0,后 8 个 CPU 映射到 mq1(这里回到了我们最前面的发现,正常情况下,ubuntu 和 CentOS 不同的 cpu 分配表现)。
CentOS 定制内核逻辑
那么只有一种可能,CentOS 没有采用 Linux upstream queue map 的方法。我在 Github 上找到了以下代码,最后找 XX 团队的同学确认,他们使用的内核源码中的函数实现确实和这段代码一致:
- 首先为每个队列分配一个 CPU;
- 然后感知超线程,将线程核分配给同一个队列;
这段代码有一个明显的问题,在盘的队列数为 2 时(简化一下条件),并且当 CPU 按顺序互为超线程时(如 CPU 0/1、CPU 2/3…),除前 2 个 CPU 外,其余 CPU 均被映射到同一队列(mq0),导致 mq1 仅绑定到单个 CPU(如 CPU 1)。
static int cpu_to_queue_index(unsigned int nr_queues, const int cpu,
const struct cpumask *online_mask)
{
// ....
return cpu % nr_queues;
}
int blk_mq_map_queues(struct blk_mq_tag_set *set)
{
// ...
for_each_possible_cpu(cpu) {
/*
* First do sequential mapping between CPUs and queues.
* In case we still have CPUs to map, and we have some number of
* threads per cores then map sibling threads to the same queue for
* performace optimizations.
*/
if (cpu < nr_queues) {
map[cpu] = cpu_to_queue_index(nr_queues, cpu, online_mask);
} else {
first_sibling = get_first_sibling(cpu);
if (first_sibling == cpu)
map[cpu] = cpu_to_queue_index(nr_queues, cpu, online_mask);
else
map[cpu] = map[first_sibling];
}
}
return 0;
}
解决方案
问题基本定位,主要问题在于 CentOS kernel 感知了超线程,但是又没有像 upstream 代码感知超线程核数量来建立 CPU 到队列的映射。
- 短期规避
- 在 S6 机型上,将子机配置改为 独立核模式(
threads='1'
),避免超线程影响。 - 长期修复
修改 CentOS 内核的
blk_mq_map_queues()
,使其与 Linux Upstream 逻辑一致,基于nr_uniq_cpus
计算队列映射,确保超线程核均衡分布。总结
从多盘性能问题定位到 SPDK 核间压力不均,再子机中盘队列映射的 CPU 异常;从怀疑虚拟化的影响,到进一步分析 Linux Kernel 感知超线程核的优化,为多队列盘分配 CPU 的逻辑;最终定位到 CentOS 内核在某些超线程场景下多队列盘 CPU 映射异常的原因。
Comments | NOTHING