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

香港服务器中的 CentOS 7:我如何调教 CFS,把高并发下的 CPU 利用率榨干

发布人:Minchunlin 发布时间:2025-08-22 09:31 阅读量:955


“凌晨 2 点,香港荃湾机房第 3 排的那台服务器风扇声比地铁还响。Nginx 的 502 像烟花一样蹦出来,监控上 CPU 占用只有 55%——可延迟却炸了。我当时就知道,问题不在‘CPU 不够’,而在‘CPU 不会用’。第二天早上,我把咖啡放在机柜门口,开始了一场和 CFS(Completely Fair Scheduler)的较量。”

现场环境 & 问题画像

硬件(香港机房,机柜内实际部署)

  • 机型:1U 定制服务器
  • CPU:2 × Intel Xeon Silver 4210R(10C/20T)= 20 物理核 / 40 逻辑核
  • 内存:128 GB DDR4(双路 NUMA,节点间延迟 ~85ns)
  • 磁盘:2 × NVMe 1.92TB(RAID1)
  • 网络:双口 10GbE(bonding active-backup)

系统与内核

  • OS:CentOS 7.9(3.10.0-1160.*.el7.x86_64,红帽长期支持内核,带大量回溯补丁)
  • 调度器:CFS(SCHED_OTHER),系统默认开启 irqbalance

业务

  • 北向流量:Nginx + Envoy
  • 计算:Java(Spring Boot)+ Go 微服务(大量短任务,QPS 峰值 > 12w/s,P99 延迟目标 < 20ms)
  • 容器:部分进程以 systemd 管理,部分以 Docker 运行(默认 cgroup v1)

症状

  • 峰值时 CPU 总利用率 ~55%–60%,但 run queue 层层排队,P95/P99 延迟抬头
  • vmstat 可见 r(可运行队列)飙升,mpstat -P ALL 显示部分核被打爆而其余核闲着
  • perf stat/pidstat -w 显示 cpu-migrations 偏高、context switches 频繁

直觉告诉我:任务在核之间来回搬家(迁移),缓存全废;CFS 的时间片与唤醒抢占策略,对这类“短小高密度任务”也不够友好。

先测一轮“基线”

# 基线度量(持续 5 分钟抽样)
lscpu
numactl --hardware
vmstat 1
mpstat -P ALL 1
pidstat -wtl 1
perf stat -e context-switches,cpu-migrations,sched:sched_switch -a --timeout 30000

基线摘录(峰值期)

指标 数值(基线)
CPU 总利用率 56%
run queue 平均(vmstat r) 18–28
context-switches ~ 2.3M / 30s
cpu-migrations ~ 420k / 30s
P95 / P99(业务) 28ms / 65ms
单核不均衡 个别逻辑核 > 95%,多数 30–40%

CFS 关键点(CentOS 7 口径)

为了“对症下药”,我只盯与 迁移、时间片、唤醒抢占 相关的 CFS 调优点:

  • kernel.sched_migration_cost_ns:迁移成本。越大越“护犊子”,任务更愿意留在当前 CPU,减少 cache miss。
  • kernel.sched_autogroup_enabled:交互式自动分组。服务器上通常关掉,避免“人为组队”导致不必要的隔离。
  • kernel.numa_balancing:自动 NUMA 负载均衡。对低延迟、固定核/内存亲和的服务,常关闭,控制跨节点迁移。
  • kernel.sched_latency_ns:目标调度周期(任务少于阈值时),决定每轮公平调度的“总窗口”。
  • kernel.sched_min_granularity_ns:最小时间片;小到一定程度会引发过度切换。
  • kernel.sched_wakeup_granularity_ns:唤醒抢占阈值,新唤醒任务若“更紧急”,能否抢走 CPU。

另外两块:

  • CFS 带宽控制(cgroup:cpu.cfs_quota_us/cpu.cfs_period_us)防止被“节流”;
  • CPU 亲和/隔离(cpuset、isolcpus、nohz_full)减少跨核干扰。

调优策略一览(先给结论,再讲过程)

方向 取值(我最后落地) 目的
迁移 kernel.sched_migration_cost_ns 500000 → 5000000 明显提高迁移成本,减少任务“串门”
分组 kernel.sched_autogroup_enabled 0 服务器场景禁用自动分组
NUMA kernel.numa_balancing 0 避免自动跨节点迁移
时间片 kernel.sched_latency_ns 24000000 → 12000000 将调度周期适度缩短,提升响应性
时间片下限 kernel.sched_min_granularity_ns 3000000 → 4500000 提高最小片,减少短任务过度切换
唤醒抢占 kernel.sched_wakeup_granularity_ns 4000000 → 8000000 降低“轻易抢占”,稳住在核执行
CFS 带宽 cpu.cfs_quota_us -1(不限)或充足配额 避免容器/服务被节流
亲和/隔离 cpuset/isolcpus,nohz_full,rcu_nocbs 绑定/隔离关键核 降干扰、提缓存命中
频率 CPU governor performance 低延迟优先,锁频不降频

注:初始值可能随内核小版本略有差异;我的原始默认值如上左列(CentOS 7.9 常见情况)。

Step 0:安全网与回滚

所有变更写进独立文件:/etc/sysctl.d/90-cfs.conf,可一键回滚。

GRUB 变更写成单独条目,保留上一版内核与参数。

变更前后做 10–15 分钟 A/B 记录(相同负载),确保可对比。

Step 1:锁定 CPU 性能状态(别让频率来捣乱)

yum install -y kernel-tools tuned
cpupower frequency-info
# 切到性能模式
cpupower frequency-set -g performance

# tuned 保底
tuned-adm profile latency-performance

坑 1:在这台服务器 BIOS 里默认是“Balanced”。不改 BIOS,内核 governor 也可能被平台策略“拉回”。

处理:BIOS 设为 Performance,关 C 状态深度(或拉低),并确认 cpupower 输出稳定。

Step 2:把“乱跑”的任务摁住——迁移与自动分组

cat >/etc/sysctl.d/90-cfs.conf <<'EOF'
kernel.sched_migration_cost_ns = 5000000
kernel.sched_autogroup_enabled = 0
kernel.numa_balancing = 0
EOF
sysctl --system

为什么这么设?

  • 我们的短任务(Go/Java 线程)命中 L1/L2 cache 很关键。一旦跨核/跨 NUMA,延迟飙升。
  • 提高 sched_migration_cost_ns 后,CFS 更愿意让任务在原核继续跑,缓存“热起来”。
  • 禁用 autogroup,避免 tty/会话“自动分家”,让 CFS 面向真实优先级和负载调度。
  • 关闭自动 NUMA,配合 cpuset 手工定向(下一步)更可靠。

Step 3:时间片与唤醒抢占的再平衡

cat >>/etc/sysctl.d/90-cfs.conf <<'EOF'
kernel.sched_latency_ns = 12000000
kernel.sched_min_granularity_ns = 4500000
kernel.sched_wakeup_granularity_ns = 8000000
EOF
sysctl --system

取值思路(经验法)

我把 sched_latency_ns 从 24ms 拉到 12ms:对“并发中等但短任务密集”的场景,缩短调度窗口有助降低尾延迟。

min_granularity 提升到 4.5ms:避免被切得太碎(碎片化切换=上下文切换/缓存抖动)。

wakeup_granularity 设为 8ms:让“刚被唤醒的小任务”不那么容易抢走 CPU,提升在核连续度。

坑 2:把 sched_latency_ns 一味调小,反而会造成切换风暴;我试过 8ms,context-switches 直接高出 20%+。

折中:12ms + 4.5ms 的下限,在我们这批服务的曲线最平。

Step 4:CFS 带宽(cgroup)——别被“无形的限流器”卡喉咙

容器或 systemd slice 下,若 cpu.cfs_quota_us 太小,会出现节流(throttling),表现为“CPU 看似不高,但延迟高”。

排查

# 容器
docker inspect <container> | jq '.[0].HostConfig.NanoCpus, .[0].HostConfig.CPUQuota, .[0].HostConfig.CPUPeriod'
# 或直接看 cgroup
cat /sys/fs/cgroup/cpu/docker/<id>/cpu.stat

# systemd slice
systemctl show <svc>.service | egrep 'CPUQuotaPerSecUSec|CPUAccounting'

处理

对核心服务:放开限流 CPUQuotaPerSecUSec=0(systemd)或 Docker 配置 --cpus/--cpu-quota=-1。

若必须限流:把 cpu.cfs_period_us 保持默认 100000,cpu.cfs_quota_us ≥ period × 逻辑核数 的 0.8–1.0;或给稳定余量(例如 40 线程给 380000–400000)。

Step 5:亲和与隔离——给关键线程“保留车道”

方案 A:cpuset(在线变更,风险低)

# 预留 NUMA0 的 8 个逻辑核给网关与网卡中断
mkdir -p /sys/fs/cgroup/cpuset/gateway
echo 0 > /sys/fs/cgroup/cpuset/gateway/cpuset.mems
echo 0-7 > /sys/fs/cgroup/cpuset/gateway/cpuset.cpus
echo 1 > /sys/fs/cgroup/cpuset/gateway/cpuset.cpu_exclusive

# 迁移进程(示例:envoy)
echo $(pidof envoy) > /sys/fs/cgroup/cpuset/gateway/tasks

同理为计算服务划出 16–24 个逻辑核,保持 同 NUMA 节点。

方案 B:内核隔离(需改 GRUB,风险高但收益稳定)

/etc/default/grub 增加:

GRUB_CMDLINE_LINUX="... isolcpus=8-31 nohz_full=8-31 rcu_nocbs=8-31"

然后:

grub2-mkconfig -o /boot/grub2/grub.cfg
reboot

把 8–31 号核“拉出”一般调度循环(user-space 负载独享),与 cpuset 配合使用。

nohz_full 减少时钟中断打扰,rcu_nocbs 把 RCU 回调移出这些核。

坑 3:隔离过多核导致系统守护/中断找不到“落脚点”。务必留一组核给系统与中断(例如 0–7)。

坑 4:irqbalance 会把 NIC 中断均匀打到所有核,和隔离策略打架。解决:保留 irqbalance,但配置 IRQ affinity(/proc/irq/*/smp_affinity_list)只落在“系统核”。

Step 6:系统服务与应用的落位

systemd 服务绑定(示例:Java 网关)

/etc/systemd/system/gateway.service.d/cpu.conf:

[Service]
CPUAffinity=0-7
TasksMax=infinity
Nice=-5

重载:

systemctl daemon-reload && systemctl restart gateway

Java/Go 应用

Java:-XX:ActiveProcessorCount=<核数> 配合亲和;线程池核心数 = 逻辑核 × 0.8–1.2,避免 2×核数的盲目配置。

Go:GOMAXPROCS 与绑定核一致,压测下微调。

验证与对比(真实采样数据)

同一时段同一压测负载(15 分钟),仅开启上文调优项

指标 基线 调优后 变化
CPU 总利用率 56% 81% ↑ 25pp
run queue(vmstat r) 18–28 9–14 ↓ ~45%
context-switches(/30s) ~2.3M ~1.7M ↓ 26%
cpu-migrations(/30s) ~420k ~130k ↓ 69%
P95 / P99(ms) 28 / 65 17 / 31 P95 ↓ 39%,P99 ↓ 52%
单核不均衡 明显 显著改善 -

抽样命令

pidstat -wtl 1 | awk '{if($1 ~ /^[0-9]+$/) print}' | head
perf stat -e context-switches,cpu-migrations -a --timeout 60000
mpstat -P ALL 1 60

现象非常直观:迁移次数大幅下降,缓存命中率稳定;队列长度回落,尾延迟显著改善,CPU 使用率被“榨出来”了。

现场遇到的坑 & 处理手记

Docker 默认 CPU 配置“悄悄限流”

现象:容器内 cpu.stat 出现 nr_throttled 快速累加。

处理:为核心容器取消配额或把 quota 提到足够(如 400000us/100000us≈4 核等效),并统一到 cpuset 管控。

自动 NUMA 迁移“帮倒忙”

现象:numastat 看到远端内存访问增多,P99 上扬。

处理:kernel.numa_balancing=0 + cpuset 固定在同一节点;必要时使用 numactl --membind 启动应用。

irqbalance 与隔离核冲突

现象:被隔离核仍出现中断负载。

处理:为关键网卡手工设置 smp_affinity_list 到 0–3(系统核),irqbalance 仍开但不碰隔离范围。

时间片调太小导致“切换风暴”

现象:context-switches 飙升,P95 反而变差。

处理:把 sched_latency_ns 拉回 12ms,min_granularity 提高到 4.5ms,wakeup_granularity 提升到 8ms,稳定了。

混用 RT/Deadline 线程影响 CFS

现象:某组件擅自把线程设为 SCHED_RR,CFS 任务饥饿。

处理:统一禁止 RT 策略(除音视频/硬实时),systemd 单元 CPUSchedulingPolicy= 保持默认;必要时把该类线程也定向到系统核。

运维落地清单(可直接复用)

/etc/sysctl.d/90-cfs.conf

kernel.sched_migration_cost_ns = 5000000
kernel.sched_autogroup_enabled = 0
kernel.numa_balancing = 0
kernel.sched_latency_ns = 12000000
kernel.sched_min_granularity_ns = 4500000
kernel.sched_wakeup_granularity_ns = 8000000

GRUB(可选,高级隔离)

GRUB_CMDLINE_LINUX="... isolcpus=8-31 nohz_full=8-31 rcu_nocbs=8-31"

systemd(示例)

/etc/systemd/system/gateway.service.d/cpu.conf

[Service]
CPUAffinity=0-7
TasksMax=infinity
Nice=-5

容器(示例)

# 计算服务容器:不节流 + 绑定核(与 cpuset 搭配)
docker run --cpuset-cpus="8-31" --cpu-quota=-1 ...

观测(放在自动化脚本里)

vmstat 1 | ts
mpstat -P ALL 1 | ts
perf stat -e context-switches,cpu-migrations -a --timeout 60000 | ts
cat /proc/schedstat | head -n 20

适用性与风险提示

本文调优 针对“短任务高并发 + 低延迟敏感”的在线服务。

批处理/吞吐优先型(如大批量离线计算),min_granularity 未必需要拉高,甚至可以增大 sched_latency_ns 来提高吞吐。

GRUB 隔离属侵入式操作,需变更窗口 + 回滚预案。

NUMA 绑定若与内存压力不匹配,可能触发 OOM/kswapd 抖动,务必结合 numastat 和 sar -B 观察。

结尾:那一杯凉掉的咖啡

“那天晚上 10 点,我在机房外的过道坐了很久。监控曲线像被熨斗抚平,P99 从 60 多毫秒落到 30 出头。那杯咖啡已经凉透,但我知道,CFS 这道‘隐形的墙’被推开了一道缝。
后来新业务再上量,我不再一上来就加机器,而是先看迁移、时间片、亲和与配额。我更确信:CPU 不止要多,更要会用。”

TL;DR(复盘要点)

  • 先量化:迁移、上下文切换、run queue、P95/P99。
  • 控迁移:sched_migration_cost_ns=5ms,关 autogroup 与自动 NUMA。
  • 稳时间片:latency=12ms、min_granularity=4.5ms、wakeup=8ms。
  • 查节流:CFS 带宽别卡喉咙。
  • 亲和/隔离:给关键线程“专用车道”。
  • 结果验证与回滚预案不可少。

如果你也在香港机房盯着同样的图,这套打法值得一试;当然,参数不是教条,数据才是你最好的指南针。

目录结构
全文