上一篇 下一篇 分享链接 返回 返回顶部

如何在香港服务器的 CentOS 中优化 KVM 的 vCPU Pinning,提高虚拟机稳定性?

发布人:Minchunlin 发布时间:2025-09-04 09:50 阅读量:1003


夜里 2 点半,香港葵涌机房的监控面板上有台支付风控 VM 的延迟曲线忽高忽低,跨境流量高峰还没退去,业务侧在钉钉里不断 @我。经验告诉我:这种“偶发抖动”十有八九是 CPU 调度迁移+NUMA 远程内存访问 搞的鬼。那一夜,我把整柜机器的 CPU 拿起“梳子”重新梳了一遍,从 BIOS 到内核,再到 libvirt 逐层 pin 定位,第二天早上 6 点,抖动消失,P95 延迟收敛 37%——这篇文章就按我那晚的操作轨迹来写,尽可能把坑点和细节都摆上台面。

一、环境说明与目标

目标:通过合理的 vCPU pinning(绑定物理核)、NUMA 对齐、线程与中断亲和,降低 VM 延迟抖动与上下文切换,提升负载稳定性。

现场硬件与软件(示例)

组件 型号/版本 备注
机型 1U 双路服务器(Dell R650 同档级) 香港机房,双电源
CPU 2 × Intel Xeon Silver 4214R(12C/24T) 共 24 物理核 / 48 线程,HT 开启
内存 256 GB(8 × 32 GB,NUMA 均衡) 两路平均分布
磁盘 2 × NVMe(OS/日志),2 × SATA SSD(数据) 虚拟化主机与 VM 数据分区
网卡 Intel X710 10GbE 双口(驱动 i40e) 上联 ToR 低延迟交换机
操作系统 CentOS 7.9 生产稳定优先
内核 5.4.x(ELRepo kernel-lt) 为了更好的网卡/存储栈与调度器
虚拟化 qemu-kvm-ev 2.12 / libvirt 4.x Virt SIG/ovirt 源(版本随仓库略有差异)

为什么不是内核 3.10?

X710、NVMe、CFS/调度的成熟度与 5.x 内核差距明显。香港机房网络 jitter 敏感,升级到 ELRepo 的 LTS 5.4 带来更稳的驱动与调度表现。

二、基线观测:别着急 pin,先量一遍

命令集合(先拿“体温”再下药):

# 拿硬件与拓扑
lscpu -e
numactl --hardware
hwloc-ls --whole-system --no-io  # 如未安装可跳过

# 线程迁移与中断分布
pidstat -wt 1 10
grep . /proc/irq/*/smp_affinity_list | head
cat /proc/interrupts | egrep "eth|ens|enp|x710|nvme"

# 延迟与抖动(宿主与 VM 内分别测)
cyclictest -p95 -m -D 30s
stress-ng --cpu 8 --timeout 60s
netperf -H <peer_ip> -t TCP_RR -l 30

常见“异常体征”:

  • pidstat -wt 看到 qemu-system-x86_64 的线程(vCPU、iothread、emulator)在不同 PSR 来回跳。
  • /proc/interrupts 显示 X710 的中断跨多个 NUMA 节点乱跑。
  • cyclictest 的 max latency 偶发尖峰(> 1~2ms,甚至十几毫秒)。
  • VM 内 mpstat -P ALL 1 出现间歇性的 run queue 抬头。

我的基线(示例)

指标 调优前
VM P95 RPC 延迟 7.9 ms
cyclictest max 2.3 ms
vCPU 平均迁移/30s 180+ 次
远程 NUMA 内存占比 ~22%
ksoftirqd 占用峰值 80%+(偶发)

三、原理与策略:pin 的对象不止是 vCPU

要 pin 的线程族:

  • vCPU 线程(每个 vCPU 一个)
  • emulator 线程(QEMU 主线程)
  • iothread(磁盘/网络 IO 线程,配置后出现)
  • vhost-net kthread(内核线程,处理 virtio-net,加速通道)

配套要 pin 的还有:

  • IRQ(网卡/存储中断)亲和到对应 NUMA 节点
  • 内存(numatune)尽量在本地 NUMA
  • HugePages(减少缺页与 TLB 抖动)

宿主要留“家务核”:ksoftirqd、kswapd、ssh、监控代理等需要独立小核区,别和 VM 抢。

四、规划 CPU 编排:先画一张“拓扑草图”

典型双路 + HT 的 lscpu 片段(示例):

CPU(s):              48
Thread(s) per core:  2
Core(s) per socket:  12
Socket(s):           2
NUMA node0 CPU(s):   0-11,24-35
NUMA node1 CPU(s):   12-23,36-47

解读:011 为 socket0 的物理核,2435 为其超线程;1223 为 socket1 物理核,3647 为其超线程。

经验:同一物理核的兄弟线程(超线程)编号差 24(=每路的线程数)。

我的分区策略(示例):

  • 宿主家务核:CPU 0-1, 24-25(同一物理核的俩线程)
  • VM-A(支付风控):绑在 NUMA0:选 CPU 4-7, 28-31(4 物理核 ×2 线程 = 8 vCPU)
  • VM-B(风控特征计算):也在 NUMA0:CPU 8-11, 32-35
  • VM-C(日志/队列):放 NUMA1:CPU 12-15, 36-39
  • …(按业务重要性与资源需求继续分)

为什么 HT 也要算进去?

对延迟敏感场景,优先给 vCPU 绑定到“不同物理核的第一个线程”;如果需要更多 vCPU,再考虑用到该物理核的超线程位。但不要让两个高负载 vCPU 挤在同一物理核的两个超线程上。

五、系统级准备:从 BIOS 到内核

BIOS(供应商 UI 略有不同)

  • Power/Performance:Performance(固定高性能 P-State)
  • C-States:只保留 C0/C1,关闭深度 C-State(C6/C7)
  • Hyper-Threading:开启(可控使用)
  • NUMA:开启
  • VT-x/VT-d:开启
  • 关自动风扇曲线省电策略(避免温度抖动触发降频)

内核与 Tuned 配置(CentOS 7)

# 升级到 ELRepo LTS 内核(谨慎:生产变更走变更流程)
yum install -y https://www.elrepo.org/elrepo-release-7.el7.elrepo.noarch.rpm
yum --enablerepo=elrepo-kernel install -y kernel-lt kernel-lt-tools

# 配置默认内核并重启
grub2-set-default 0
reboot

GRUB 内核参数(/etc/default/grub 的 GRUB_CMDLINE_LINUX 追加):

intel_pstate=disable processor.max_cstate=1 idle=poll
isolcpus=4-23,28-47 nohz_full=4-23,28-47 rcu_nocbs=4-23,28-47
  • isolcpus/nohz_full/rcu_nocbs:把预留给 VM 的核“隔离”出一般调度与 RCU 回调,减少干扰。
  • idle=poll 与 max_cstate=1:牺牲功耗换取一致性。高热环境注意温控。
  • intel_pstate=disable:配合 cpufreq performance 固定频率,避免频率抖动。
# 生成并应用 grub
grub2-mkconfig -o /boot/grub2/grub.cfg

# 性能调优 profile
yum install -y tuned kernel-tools
tuned-adm profile latency-performance
cpupower frequency-set -g performance

# HugePages(1G 大页示例,先小规模验证)
echo "vm.nr_hugepages=64" >> /etc/sysctl.d/99-hugepages.conf
sysctl --system
mkdir -p /dev/hugepages
mount -t hugetlbfs nodev /dev/hugepages

IRQBalance 与中断亲和

关闭 irqbalance(或限制其不要动隔离核):

# 禁止自动搬运中断,避免与我们手工亲和策略冲突
systemctl stop irqbalance
systemctl disable irqbalance

将 X710 中断绑到 宿主家务核(例如 0-1,24-25),为 vhost/VM 留出干净的核区:

# 查找网卡中断号
grep -i "ixgbe\|i40e\|x710\|ens\|enp" /proc/interrupts

# 写 smp_affinity_list(示例把中断固定在 0-1)
echo 0-1 > /proc/irq/<irq_id>/smp_affinity_list

如果使用 DPDK/OVS-DPDK 路线,亲和策略另算;本文以内核 vhost-net 为例。

六、libvirt / QEMU:把线程和内存都“拴”在对的位置

关键点:

  • cpu mode="host-passthrough":让 VM 获得接近裸机的指令集特性。
  • cputune:vcpupin 绑定每个 vCPU;emulatorpin 绑定 QEMU 主线程;iothreadpin 绑定 IO 线程。
  • numatune:内存绑定到本地 NUMA;mode="strict" 防止越界。
  • iothreads:为磁盘/网络单独线程提高并发可控性。
  • 网卡:virtio + vhost=on + mq(多队列)并与队列数匹配 vCPU。

VM 示例(节选 XML):

<domain type='kvm'>
  <name>vm-pay-risk</name>
  <vcpu placement='static'>8</vcpu>
  <cpu mode='host-passthrough'>
    <topology sockets='1' cores='4' threads='2'/>
  </cpu>

  <iothreads>2</iothreads>

  <cputune>
    <!-- vCPU 0..7 绑到 NUMA0 的 4 个物理核及其超线程:4,28 / 5,29 / 6,30 / 7,31 -->
    <vcpupin vcpu='0' cpuset='4'/>
    <vcpupin vcpu='1' cpuset='28'/>
    <vcpupin vcpu='2' cpuset='5'/>
    <vcpupin vcpu='3' cpuset='29'/>
    <vcpupin vcpu='4' cpuset='6'/>
    <vcpupin vcpu='5' cpuset='30'/>
    <vcpupin vcpu='6' cpuset='7'/>
    <vcpupin vcpu='7' cpuset='31'/>

    <!-- QEMU 主线程放在宿主家务核,避免抢 VM 的核 -->
    <emulatorpin cpuset='0-1'/>

    <!-- IO 线程分别固定 -->
    <iothreadpin iothread='1' cpuset='2'/>
    <iothreadpin iothread='2' cpuset='3'/>
  </cputune>

  <numatune>
    <memory mode='strict' nodeset='0'/>
    <memnode cellid='0' mode='strict' nodeset='0'/>
  </numatune>

  <memoryBacking>
    <hugepages>
      <page size='1048576' unit='KiB'/>
    </hugepages>
  </memoryBacking>

  <devices>
    <disk type='file' device='disk'>
      <driver name='qemu' type='qcow2' io='threads' iothread='1'/>
      <source file='/vmstore/pay-risk.qcow2'/>
      <target dev='vda' bus='virtio'/>
    </disk>

    <interface type='bridge'>
      <source bridge='br0'/>
      <model type='virtio'/>
      <driver name='vhost' queues='4'/> <!-- 与 vCPU 数匹配或取其倍数 -->
    </interface>
  </devices>
</domain>

版本差异提醒:较新的 libvirt 支持 <interface><tuning><vhost><threads> 为 vhost 线程单独亲和;旧版可在 VM 启动后用 ps -eLo pid,psr,comm | grep vhost- 找到 kthread,再用 taskset -pc 绑定(持久化需自写 udev/systemd 单元)。

临时 pin 与在线校验:

# 在线修改某 vCPU pin(重启不保留)
virsh vcpupin vm-pay-risk 0 4
virsh emulatorpin vm-pay-risk 0-1
virsh iothreadpin vm-pay-risk 1 2

# 查看线程与亲和
virsh vcpupin vm-pay-risk
virsh emulatorpin vm-pay-risk
virsh iothreadinfo vm-pay-risk

七、网络与存储协同:把数据路径也对齐

virtio-net 多队列:driver queues=<n> 与 vCPU 或 NIC RSS 队列数一致;ethtool -l 查看物理队列,ethtool -L 调整。

RPS/XPS:宿主 /sys/class/net/ethX/queues/rx-*/rps_cpus 与 /tx-*/xps_cpus 配置到 宿主家务核 或对应 VM 的 NUMA 核。

NVMe/SSD 中断:同样绑到本地 NUMA 的宿主家务核,避免跨节点。

八、验证与对比:用数据说话

调优后指标(同上业务窗口测 30 分钟)

指标 调优前 调优后
VM P95 RPC 延迟 7.9 ms 4.9 ms
cyclictest max 2.3 ms 0.72 ms
vCPU 平均迁移/30s 180+ 次 ≤ 5 次(主要为维护/抢占)
远程 NUMA 内存占比 ~22% < 3%
ksoftirqd 占用峰值 80%+(偶发) < 25%

在线观察点:

htop(打开 CPU 编号)看 vCPU 是否稳定停在规划的核上。

numastat -p <qemu-pid> 检查内存是否稳在对应 node。

/proc/interrupts 中断是否集中在规划核。

业务曲线是否去掉“锯齿”。

九、踩坑记录与快速解法

只 pin 了 vCPU,忘了 emulator/iothread

现象:偶发长尾仍在。

解决:<emulatorpin>、<iothreadpin> 都要配;磁盘 io='threads' 并分配 iothreads。

irqbalance 把中断“搬”走

现象:中断亲和失效,一会儿在 node0 一会儿在 node1。

解决:停掉 irqbalance,或 IRQBALANCE_BANNED_CPUS 限制(CentOS 7 版本不同配置项名不同,保守做法是停服务 + 手工亲和)。

nohz_full/isolcpus 配过猛,宿主家务核不够

现象:ksoftirqd 飙升,管理操作掉队。

解决:至少留 2~4 个逻辑核 给宿主家务;磁盘/网络中断也集中到这几颗核。

NUMA 与 HugePages 不匹配

现象:numastat -p 里 remote 分配升高。

解决:numatune mode='strict' + 预留足够同 NUMA 的大页;不够就降低 VM 内存或扩大大页。

Docker/容器与 libvirt cgroup 冲突

现象:/sys/fs/cgroup/cpuset 下容器/VM 互相“踩界”。

解决:给容器单独划分 cpuset,避免进入 VM 的隔离核区。

开启 HT 但把两个重负载 vCPU 绑到同一物理核

现象:吞吐看似够、抖动仍大。

解决:对延迟敏感 VM,优先使用不同物理核的“第一个线程”;超线程只作补位。

live migration 需求与静态 pin 冲突

现象:迁移后目标宿主没有同样核位。

解决:在主机群层面制定统一 CPU 编号规划与“核位契约”,或迁移后自动化重 pin。

十、可复用的小工具

1)一键生成 vcpupin 片段(bash)

#!/usr/bin/env bash
# gen_vcpupin.sh: 传入起始物理核列表,为每个物理核生成两条(含超线程)
# 用法:./gen_vcpupin.sh 4 5 6 7
set -e
vcpu=0
for pcore in "$@"; do
  ht=$((pcore+24))   # 按示例拓扑,超线程=物理核+24
  echo "<vcpupin vcpu='$vcpu' cpuset='$pcore'/>"; vcpu=$((vcpu+1))
  echo "<vcpupin vcpu='$vcpu' cpuset='$ht'/>";    vcpu=$((vcpu+1))
done

2)绑定 vhost kthread(旧 libvirt 环境)

#!/usr/bin/env bash
# pin_vhost.sh vmname cpulist
VM=$1; CPUS=$2
PID=$(pgrep -f "qemu-system.*-name $VM" | head -1)
ps -eLo pid,psr,comm | awk -v p=$PID '$0 ~ p && $3 ~ /^vhost-/{print $1}' | \
while read -r T; do
  taskset -pc $CPUS $T
done

十一、回滚与变更窗口建议

GRUB 内核参数、HugePages、nohz_full/isolcpus 等属于系统级变更,建议业务低谷、分批次套用,有控制的灰度。

libvirt XML 可先 virsh define 到一台影子 VM 验证,再推广到核心业务。

保留可回滚的内核条目;grub2-reboot 1 指定下次仅一次性回到旧内核。

关键指标纳入看板:vCPU 迁移率、NUMA 远程比例、网络/存储中断分布、P95/P99 延迟。

十二、结尾:黎明前的那杯咖啡

当我在机房门口捧着便利店的热美式,看着业务群里“抖动消失”的反馈,脑子里浮现的是一张张 CPU 编号表。Pinning 看似“粗暴”,但它让复杂系统的 不确定性 变得可控:vCPU 不再漫游,内存不再远行,中断不再四处惊扰。
如果你的 VM 也在香港这个对网络时延极为敏感的环境中承载关键流量,别指望默认调度器能替你做完所有权衡——把核位安排清楚,比什么都重要。

附:操作清单(Checklist)

  •  BIOS:Performance、C-state≤C1、HT On、NUMA On
  •  内核:ELRepo 5.4、isolcpus/nohz_full/rcu_nocbs、performance governor
  •  HugePages:预留并与 NUMA 对齐
  •  IRQ:X710/NVMe 中断亲和到宿主家务核
  •  libvirt:host-passthrough、vcpupin、emulatorpin、iothreadpin、numatune strict
  •  virtio-net:vhost=on、多队列与 vCPU/RSS 匹配
  •  验证:numastat -p、/proc/interrupts、pidstat -wt、业务 P95/P99 曲线

只要思路和手法跑通,你的 VM 延迟曲线也能像那天清晨的海风一样,稳。

目录结构
全文