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

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

发布人:Minchunlin 发布时间:2025-09-03 10:45 阅读量:889


那天凌晨 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. 标准化落地清单(可抄作业)

  1. BIOS:NPS=4;Determinism=Performance;CPB=按需;CPPC=On
  2. 内核:6.6 + amd_pstate=active(或 5.4 + performance)
  3. sysctl:关闭 numa_balancing,提高 sched_migration_cost_ns
  4. THP:never;预分配 HugeTLB
  5. L3/CCX 分组:用 lstopo 做表;服务一 L3、一碗饭
  6. IRQ/RPS/XPS:与服务同 L3;关闭/钳制 irqbalance
  7. tuned:自定义 epyc-ccx-lowlatency
  8. 验收: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 实际输出帮你改成可直接套用的版本

目录结构
全文