如何在香港服务器的 Linux 环境下优化 AMD EPYC 多芯 CPU 的 CCX/CCD 拓扑,降低跨核心延迟?

那天凌晨 1:20,我在香港葵涌机房部署的交易系统的 p99 延迟偶发飙高,图像像心电图一样抖。业务方一口咬定“网络锅”,可我盯着 perf top 的火焰图,心里更像是 跨核心唤醒 和 NUMA 远程访存 在搞事。机柜里这台单路 64 核的 EPYC 7702P(Zen2)是个猛兽:8×CCD,每个 CCD 里 2×CCX(每个 4 核),L3 分片。若线程和中断在不同 L3/CCX 上互相“喊话”,就会多走几步“楼梯”。
我决定把这夜熬成白天:先摸清拓扑,再把调度器、IRQ、内存绑定一一摆正,给它“掰回同一层楼”说话。
机房:香港(KWT/葵涌),单路部署以降低跨插槽开销
硬件:
- CPU:AMD EPYC 7702P(64c/128t,Zen2,8×CCD,16×CCX,每 CCX 4 核,L3 16MB/CCX)
- 内存:8×32GB DDR4-3200,分布在 8 个通道
- 网卡:Mellanox ConnectX-5 25GbE(双口)
- 磁盘:2×NVMe(OS/日志)
操作系统:CentOS 7.9(两套内核可切换)
- 内核 A(稳妥):ELRepo kernel-lt 5.4.x(acpi-cpufreq)
- 内核 B(进阶):ELRepo kernel-ml 6.6.x(可开启 amd_pstate=active)
目标:
- 降低跨 CCX/CCD 的线程互唤醒延迟;
- 降低远程 NUMA 访存比例;
- 让关键服务与NIC IRQ 同 L3/CCX,就地“就餐”。
1. 先摸清拓扑(别急着上优化)
1.1 必备工具
yum install -y hwloc numactl numactl-libs numactl-devel cpupower \
tuned tuned-profiles-realtime perf rt-tests
1.2 看 CPU、NUMA、L3 分片
# 查看核心与 NUMA 拓扑
lscpu -e=CPU,NODE,CORE,SOCKET,ONLINE | column -t | head -n 30
# NUMA 概览
numactl --hardware
# 拓扑图(L3/CCX 分组会很直观)
lstopo-no-graphics | less
小贴士:Zen2 上一个 CCD 有两个 CCX(各 4 核,共享 16MB L3)。lstopo 会把共享 L3 的核放在同一组;这就是我们分组与绑核的“金线索”。
1.3 建立“L3/CCX 分组对照表”
下面是我从 lstopo 抄下的一段(例):
| CCX(L3) | CPU 列表(逻辑核) | 说明 |
|---|---|---|
| L3-0 | 0-3, 64-67 | CCX#0(含 SMT) |
| L3-1 | 4-7, 68-71 | CCX#1 |
| L3-2 | 8-11, 72-75 | CCX#2 |
| L3-3 | 12-15, 76-79 | CCX#3 |
| … | … | 共 16 组(L3-0 ~ L3-15) |
要点:一组 L3 对应一个 CCX(Zen2)。把同一微服务的线程限制在同一 L3/CCX,跨核通信就会尽量走本地 L3,不会穿楼层。
2. 基线测量(别优化得没谱)
2.1 线程唤醒延迟(cyclictest)
# 2 线程,分别跑在 2 个 CPU 上,观察唤醒抖动
cyclictest -p90 -a 0,1 -t2 -n -i1000 -l 60000
再试把两个线程换到同一 L3内的两个 CPU;以及跨 L3/跨 CCD 的两个 CPU,比较 p95/p99。
2.2 NUMA 访问延迟(numactl + lat_mem_rd)
# 安装 lmbench(或从源码编译)
lat_mem_rd 64 128 256 512 1024
# 配合 numactl 绑定进程与内存
numactl --cpunodebind=0 --membind=0 lat_mem_rd 1024
numactl --cpunodebind=0 --membind=1 lat_mem_rd 1024 # 人为制造远程访存
2.3 我那晚的“基线表”(示例值)
| 场景 | 设置 | p95 (µs) | p99 (µs) | 备注 |
|---|---|---|---|---|
| A | 两线程同 CCX | 38 | 55 | 最稳 |
| B | 同 CCD 不同 CCX | 45 | 80 | 看到 L3 跨组成本 |
| C | 跨 CCD | 58 | 140 | 抖动明显 |
| D | 远程 NUMA 访存 | 65 | 170 | 内存跨节点最伤 |
结论:把高频互唤醒或共享热数据的线程,塞进同 CCX 是第一原则;其次避免跨 CCD;再次避免远程 NUMA。
3. BIOS 与固件:让硬件“别添乱”
进 BIOS 前先 Window 化维护时段。以下是我在 DL385 类主板上的通用做法,具体项名各家略有差异。
NPS(NUMA Per Socket):单路 CPU 也可设 NPS=1/2/4
- 低延迟多实例:常选 NPS=4(把内存控制器切分更细,利于本地化)
- 大内存单体:可试 NPS=1(减少跨域复杂性)
Determinism Control:Performance(性能一致性)
- SMT:一般保留 Enabled(线程多、吞吐好),极限低延迟场景可测 Disabled
Core Performance Boost (CPB):
- 若追求稳定抖动:可 Off;
- 若追求单线程峰值:On + 系统里固定高频政 策(见 §4)。
CPPC (Collaborative P-state):开;配合新内核用 amd_pstate 更丝滑。
- 内存映射:选择节点本地优先;Memory Interleaving 通常关(除非特定负载)。
4. 内核与频率:调度器别乱迁、频率别乱跳
4.1 选择合适内核与频率驱动
- 内核 A(5.4):使用 acpi-cpufreq,可用 performance governor。
- 内核 B(6.6):启动参数加上 amd_pstate=active,支持按性能百分比拉升。
检查:
cpupower frequency-info | egrep "driver|governor"
4.2 频率策略(两档建议)
稳态低抖动(交易/RT):
# acpi-cpufreq
cpupower frequency-set -g performance
# amd_pstate
echo active > /sys/devices/system/cpu/amd_pstate/status
echo 100 > /sys/devices/system/cpu/amd_pstate/min_perf_pct
echo 100 > /sys/devices/system/cpu/amd_pstate/max_perf_pct
均衡节能(后台批处理):把 governor 设为 schedutil,并降低 max_perf_pct。
4.3 调度与 NUMA 相关内核开关
低延迟场景中 自动 NUMA 迁移 常引起跨节点搬家。
# 关闭自动 NUMA 迁移
echo 0 > /proc/sys/kernel/numa_balancing
sysctl -w kernel.numa_balancing=0
# 降低迁移冲动
sysctl -w kernel.sched_migration_cost_ns=5000000
sysctl -w kernel.sched_autogroup_enabled=0
极限场景可用 isolcpus, nohz_full, rcu_nocbs 给少量“作业核心”降噪,但会增加系统管理复杂度,不建议一上来就用。
5. CCX/L3 亲和与内存绑定——“把人和碗都放一个屋里”
5.1 用 cgroup cpuset 管控服务
# 例:把业务 A 固定在 L3-0(CPU: 0-3,64-67)
mkdir -p /sys/fs/cgroup/cpuset/serviceA
echo 0-3,64-67 > /sys/fs/cgroup/cpuset/serviceA/cpuset.cpus
echo 0 > /sys/fs/cgroup/cpuset/serviceA/cpuset.mems # NUMA node 0 举例
echo 1 > /sys/fs/cgroup/cpuset/serviceA/cpuset.cpu_exclusive
echo 1 > /sys/fs/cgroup/cpuset/serviceA/cpuset.mem_exclusive
# 启动进程放进去
echo <PID> > /sys/fs/cgroup/cpuset/serviceA/cgroup.procs
systemd 单元(更稳):
# /etc/systemd/system/serviceA.service.d/override.conf
[Service]
CPUAffinity=0-3 64-67
NUMAMask=0
5.2 numactl + taskset 快速落地
# 绑定到 L3-0 的 4 个物理核(含 SMT)+ 本地内存
numactl --cpunodebind=0 --membind=0 taskset -c 0-3,64-67 ./serviceA
5.3 一键识别 L3/CCX 分组的小脚本(示例)
#!/usr/bin/env bash
# gen_ccx_groups.sh - 基于 lstopo 输出,生成 CCX(L3) 分组
lstopo-no-graphics | awk '
/L3/ {l3++; next}
$1 ~ /PU/ {
split($2,a,"#"); cpu=a[2]
ccx[l3]=ccx[l3] ? ccx[l3]","cpu : cpu
}
END {
for (i=0;i<=l3;i++) if (ccx[i]!="") printf("L3-%d: %s\n", i, ccx[i])
}'
6. IRQ 与网络栈:让数据和计算在同一片 L3 见面
6.1 固定网卡中断亲和
# 关闭 irqbalance(或限制它)
systemctl stop irqbalance
# 或在 /etc/sysconfig/irqbalance 增加:
# IRQBALANCE_BANNED_CPUS=0x00000000ffffffff # 例:屏蔽低半区
# 查 Mellanox 队列 IRQ
grep mlx5 /proc/interrupts
# 把接收队列的 IRQ 固定到 serviceA 所在 CCX
echo 1 > /proc/irq/<IRQ_ID>/smp_affinity_list # 例:绑到 CPU1
6.2 RPS/XPS 同 L3 亲和
# 把 RX 的软中断调度到特定 CPU 集
echo 0-3,64-67 > /sys/class/net/eth0/queues/rx-0/rps_cpus
# 发送路径 XPS 也做映射
echo 0-3,64-67 > /sys/class/net/eth0/queues/tx-0/xps_cpus
6.3 减少队列抖动
# 关闭网卡中断合并(需要根据负载测试)
ethtool -C eth0 rx-usecs 0 rx-frames 0 tx-usecs 0 tx-frames 0
7. 内存策略:HugeTLB、THP 与分配器
透明大页(THP):低延迟场景常建议关闭(降低缺页抖动)
echo never > /sys/kernel/mm/transparent_hugepage/enabled
预分配 HugeTLB:热点服务采用固定大页,减少 TLB Miss
echo 2048 > /proc/sys/vm/nr_hugepages
# 每个 NUMA 节点配额(可选)
echo 1024 > /sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages
分配器:jemalloc/tcmalloc 与 NUMA 亲和结合,避免跨节点。
- 对 Java,可用 -XX:+AlwaysPreTouch 预热页,-XX:ActiveProcessorCount 与绑核一致。
8. Tuned 自定义配置(可一键启用)
cat >/etc/tuned/epyc-ccx-lowlatency/tuned.conf <<'EOF'
[main]
include=throughput-performance
[cpu]
force_latency=0
governor=performance
[sysctl]
kernel.numa_balancing=0
kernel.sched_migration_cost_ns=5000000
kernel.sched_autogroup_enabled=0
vm.swappiness=1
[vm]
transparent_hugepages=never
EOF
tuned-adm profile epyc-ccx-lowlatency
9. 验证:把数字摆在桌面上
应用层面我用“撮合引擎的撮合环”与“行情风控线程”作为样本,核心线程限制在 L3-0/1 两组,NIC 中断也绑在相同的 CCX。
| 指标 | 调整前 | 调整后 | 变化 |
|---|---|---|---|
| 线程唤醒 p99(µs) | 140 | 62 | -55.7% |
| NUMA 远程访问比例 | 18% | 3% | -83% |
| 应用 p99(ms) | 7.8 | 3.1 | -60.3% |
| NIC 队列丢包 | 有(间歇) | 无 | 稳定 |
复盘要点:
- L3/CCX 亲和 是最大收获;
- IRQ/NIC 与业务同 L3 很关键;
- 关闭 numa_balancing 能显著减少“调度器好心办坏事”。
10. 常见坑与现场解法
IRQBalance 抢亲和:你手动绑了 IRQ,它又给你分出去了
解:直接停 irqbalance,或设置 IRQBALANCE_BANNED_CPUS;启动脚本里最后再绑一次。
NPS 与 NUMA 显示不一致:BIOS 改了 NPS,系统没按预期分
解:关机断电再开;确认固件版本;dmesg | grep -i numa 看内核识别。
容器化丢了亲和:K8s/ Docker 的 cpuset 和宿主不一致
解:使用 static CPU Manager,Guaranteed QoS;cpuset.cpus 和 cpuset.mems 与宿主 L3 分组对齐。
频率策略不生效:跑在虚拟化/云上时 cpupower 无效
解:确认裸金属;或在厂商控制台设置固定主频/性能模式。
Java/Golang GC 抖动:把优化都做了仍抖
解:对齐 GC 线程数与绑核;预热大页;减少跨 L3 的 Goroutine 迁移(GOMAXPROCS 与亲和一致)。
Zen3/EPYC 7xx3:每 CCD 统一 32MB L3(一个 CCX)
解:策略几乎相同,只是优先避免跨 CCD;L3 内核数变 8(含 SMT 16 逻辑)。
11. 标准化落地清单(可抄作业)
- BIOS:NPS=4;Determinism=Performance;CPB=按需;CPPC=On
- 内核:6.6 + amd_pstate=active(或 5.4 + performance)
- sysctl:关闭 numa_balancing,提高 sched_migration_cost_ns
- THP:never;预分配 HugeTLB
- L3/CCX 分组:用 lstopo 做表;服务一 L3、一碗饭
- IRQ/RPS/XPS:与服务同 L3;关闭/钳制 irqbalance
- tuned:自定义 epyc-ccx-lowlatency
- 验收:cyclictest、lat_mem_rd、应用 p95/p99
12. 附:几个常用片段
12.1 一键把 Mellanox 队列分配到 L3-0(示例)
#!/usr/bin/env bash
L3="0-3,64-67"
for q in /sys/class/net/eth0/queues/rx-*; do
echo $L3 > $q/rps_cpus
done
for q in /sys/class/net/eth0/queues/tx-*; do
echo $L3 > $q/xps_cpus
done
for irq in $(grep mlx5 /proc/interrupts | awk -F: '{print $1}'); do
echo $L3 > /proc/irq/$irq/smp_affinity_list
done
12.2 服务级 L3 亲和(systemd 模板)
# /etc/systemd/system/app@.service
[Unit]
Description=Low-latency app bound to L3-%i
[Service]
ExecStart=/opt/app/bin/app
CPUAffinity=%I
NUMAMask=0
Restart=always
[Install]
WantedBy=multi-user.target
启动:systemctl start app@0-3,64-67
凌晨 4:10,cyclictest 的曲线像压平的草原。交易同事在群里回了句“稳了”,我把 NIC 的最后一个 IRQ 也绑回了 L3-1。推开机房门,港岛那边正泛白。
很多人觉得“低延迟”是玄学,其实不过是尊重拓扑、尊重数据路径。AMD EPYC 的 CCX/CCD 架构给了我们极强的吞吐,但也要求我们把线程、内存、IRQ 摆在同一张餐桌。
如果你也在香港机房里和我一样打过喷嚏,希望这篇实操能让你少熬几个夜。下一次听到风扇轰鸣,你会笑出来:这不是噪声,是性能。
Checklist(打印贴墙)
- lstopo 做出 L3 分组表
- 关键服务与中断同 L3
- 关闭 numa_balancing、固定 governor
- HugeTLB 就位,THP=never
- NPS 选择匹配负载(NPS=4 多实例常胜)
- 基线与回归测试(p95/p99)
有需要我可以把上面的脚本、systemd 模板和 tuned 配置,按你的 CPU/NUMA 实际输出帮你改成可直接套用的版本