如何在香港服务器的 CentOS 中启用 NUMA Balancing,提升大内存数据库访问性能?

香港柴湾机房监控面板上,主库的 P95 延迟从 12ms 蹿到了 40ms,业务方已经@我“确认是不是存储抖了”。我站在 42U 机柜前,一边用手摸着风口的热浪,一边盯着 numastat 的“numa_miss/numa_foreign”在缓慢爬升——这不是磁盘,是跨 NUMA 远程内存命中把我们拖慢了。那一夜的目标很明确:打开并调优内核的自动 NUMA Balancing(AutoNUMA),把大内存数据库的访问拉回正轨。
1. 现场环境与硬件拓扑
为了让你能对号入座,先把我的“病人档案”给全:
| 项 | 规格 |
|---|---|
| 机房 | 香港(HKG),国际线路直连 |
| 服务器 | Supermicro 2U(双路) |
| CPU | Intel Xeon E5-2690 v4 ×2(每颗 14C/28T,2.6GHz,Broadwell-EP) |
| 内存 | 512GB DDR4-2400(每路 256GB,均衡插槽) |
| 磁盘 | Intel P4510 NVMe 2TB ×2(RAID1 at mdadm) |
| 网卡 | Intel X710 10GbE(双口,多队列) |
| 操作系统 | CentOS 7.9(kernel 3.10.0-1160.*.el7.x86_64) |
| 数据库 | MySQL 8.0(InnoDB,Buffer Pool 320GB,redo on NVMe) |
| 负载 | 读写混合(OLTP 70/30),连接 5k~12k 波动 |
拓扑与NUMA:
# CPU与NUMA节点:
lscpu | egrep 'Socket|NUMA|Thread|Core'
numactl --hardware
# NIC/ NVMe 属于哪个 NUMA 节点(很关键):
cat /sys/class/net/eth0/device/numa_node
cat /sys/block/nvme0n1/device/numa_node
# NUMA 距离矩阵(越小越近):
numactl -H
我的机器是典型 2 节点 NUMA:node0 绑定 CPU#0、部分内存和一张 X710 口,node1 绑定 CPU#1、剩余内存和另一张口;NVMe 在 node0 下挂。数据库主进程主要在 node0 上跑,但内存占用逐步偏向 node1,远程访问拉高了延迟。
2. 目标与思路
目标:降低远程内存访问比例(numa_miss/numa_foreign),缩短内存访问延迟,平衡各 NUMA 节点的内存与 CPU 亲和,提升 TPS 与尾延迟指标。
手段:开启并调优 AutoNUMA,配合中断亲和、HugePages 策略、I/O 队列绑定、数据库进程初始布局,减少跨节点访问。
经验:CentOS 7 的内核早已有 AutoNUMA,但 不少线下镜像/自定义内核参数默认关掉了,或者扫描参数不合适,导致“开启≠有效”。
3. 快速体检:你现在是开着还是关着?
# 是否开启(1=开,0=关)
cat /proc/sys/kernel/numa_balancing
# 也可以:
sysctl kernel.numa_balancing
# 看开机参数是否强制禁用过:
cat /proc/cmdline | tr ' ' '\n' | egrep -i 'numa'
如果看到 kernel.numa_balancing = 0,或 cmdline 里有 numa_balancing=disable、numa=off,那你就是关着。
另外确认 BIOS Node Interleaving:要关闭,否则系统根本看不到 NUMA 节点,AutoNUMA 也就无从谈起。
4. 启用与持久化
4.1 临时启用(立即生效)
# 立即开启
echo 1 > /proc/sys/kernel/numa_balancing
# 或
sysctl -w kernel.numa_balancing=1
4.2 永久启用(重启后仍然生效)
方法 A:sysctl 持久化
cat >/etc/sysctl.d/99-numa.conf <<'EOF'
kernel.numa_balancing=1
# 扫描参数(后文解释为什么这样设)
kernel.numa_balancing_scan_delay_ms=1000
kernel.numa_balancing_scan_period_min_ms=60000
kernel.numa_balancing_scan_period_max_ms=300000
kernel.numa_balancing_scan_size_mb=256
EOF
sysctl --system
方法 B:GRUB 开机参数兜底
如果你曾用 numa_balancing=disable 关过,建议明确写回 enable:
# /etc/default/grub 增加(或替换):
GRUB_CMDLINE_LINUX="crashkernel=auto rhgb quiet numa_balancing=enable"
grub2-mkconfig -o /boot/grub2/grub.cfg
# UEFI 机器是:
# grub2-mkconfig -o /boot/efi/EFI/centos/grub.cfg
两种方式可以并用:sysctl 控制运行时,GRUB 确保不会被内核参数“先天禁用”。
5. 扫描参数怎么设?(避免“扫描风暴”)
AutoNUMA 的原理是周期性扫描 & 迁移页框,参数太激进会有额外开销,太保守又起不到效果。我的大内存 OLTP 上,以下组合更均衡:
kernel.numa_balancing_scan_delay_ms=1000
启动延迟:别让刚启动就猛扫,留 1s 缓冲。
kernel.numa_balancing_scan_period_min_ms=60000
最小扫描周期 60s,避免“持续扫”。
kernel.numa_balancing_scan_period_max_ms=300000
最大 300s(5 分钟),稳定期降低扫描频率。
kernel.numa_balancing_scan_size_mb=256
每次扫描 256MB,大内存时别太小,否则热页识别慢。
这些在 CentOS 7 都是 /proc/sys/kernel/ 下的可调项,随时改随时见效。
6. 数据库与 NUMA 的协同策略
6.1 先把“第一触摸”做好
Linux 是 first-touch 分配:哪个 CPU 第一次触摸页,就把这块内存分配到那个 CPU 所在的 NUMA 节点。
做法:在启动数据库前,尽量让主进程/预热脚本在预期的节点上运行,减少一开始就“偏科”。
例:MySQL 启动前做一次 Buffer Pool 预热脚本(模拟热点访问):
# 启动后(或热身脚本里):
mysql -uroot -p'***' -e "SET GLOBAL innodb_buffer_pool_dump_now=OFF; \
SET GLOBAL innodb_buffer_pool_load_abort=OFF; \
-- 自定义热表扫描或应用预热逻辑"
6.2 结合 numactl 的“轻干预”
AutoNUMA 开着的情况下,不建议硬绑死(--membind/--cpunodebind),但可以做轻度引导:
数据库服务(systemd unit)里,用 numactl --preferred 让分配优先落在目标节点(比如 NVMe 和主网卡所在的 node)。
或者在第一次冷启动时用 --interleave=all 进行均匀铺底,再交给 AutoNUMA 慢慢收敛热点。
示例(MySQL systemd 单元):
# /etc/systemd/system/mysqld.service.d/numa.conf
[Service]
# 冷启动时可选:ExecStartPre=/usr/bin/numactl --interleave=all /usr/sbin/mysqld --validate-config
ExecStart=
ExecStart=/usr/bin/numactl --preferred=0 /usr/sbin/mysqld
这套组合避免了“把热点页全撒开”的极端,同时给 AutoNUMA 足够信号去迁移真正的热点。
6.3 HugePages 与 THP
Transparent Huge Pages (THP):建议设为 madvise,交给数据库自己决定哪些内存用大页,避免后台 THP 合并带来的抖动。
HugeTLB 静态大页:MySQL/Oracle/PG 都可以使用(各有参数),但需要你评估是否会与 AutoNUMA 的迁移目标冲突。我的实践是 OLTP 场景优先 THP=madvice,必要时为 redo/特定内存池开静态大页。
# 关闭 THP=always,改为 madvise
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
echo madvise > /sys/kernel/mm/transparent_hugepage/defrag
# 持久化:
cat >/etc/tuned/numa-db/tuned.conf <<'EOF'
[main]
include=latency-performance
[vm]
transparent_hugepages=madvise
EOF
tuned-adm profile numa-db
6.4 其他内存/回收策略
# 降低跨节点回收/抖动概率
sysctl -w vm.zone_reclaim_mode=0
# 适当降低 swap 倾向
sysctl -w vm.swappiness=10
# 避免 KSM 干扰
systemctl disable --now ksmtuned || true
7. IRQ 与 I/O 的 NUMA 亲和
AutoNUMA 把线程和页迁好了,但中断/队列还在“异地”,一样会多走一跳。
步骤:
确认队列归属:
grep . /sys/class/net/eth0/queues/rx-*/rps_cpus | head
cat /proc/interrupts | egrep "eth0|mlx|ixgbe|i40e"
cat /sys/class/net/eth0/device/numa_node
为中断绑核(与数据库主工作核同 NUMA 节点):
# 举例把 eth0 的中断绑在 node0 对应的 CPU 集合(如 0-13,28-41 为 node0)
for irq in $(grep eth0 /proc/interrupts | awk -F: '{print $1}'); do
echo 0,0000ffff >> /proc/irq/$irq/smp_affinity # 示例位掩码,请按实际核数计算
done
NVMe 队列亲和(mq):
cat /sys/block/nvme0n1/queue/rq_affinity
echo 2 > /sys/block/nvme0n1/queue/rq_affinity # 2=严格按 NUMA 分配
提醒:irqbalance 会自动分配,但不一定NUMA最优。对低延迟数据库主机,我更倾向手工固定关键队列。
8. 基线与对比测试
8.1 测试方法
业务在线流量 + 旁路 sysbench 压测验证(限流到不影响线上 SLA)。
采集指标:
- TPS / QPS
- P95 / P99 latency
- numastat 的 numa_miss、numa_foreign、local_node 命中比
- perf stat 的 LLC miss、LLC load latency(可选)
- vmstat 1 的 cs、us、sy 变化
- iostat -x 与 sar -n DEV(确认不是 I/O 或网络瓶颈)
8.2 三阶段对比(线上实测节选)
| 场景 | AutoNUMA | 扫描参数 | THP | IRQ/NVMe 亲和 | TPS (mean) | P95 (ms) | 远程命中占比 |
|---|---|---|---|---|---|---|---|
| A. 初始(问题现场) | 关 | - | always | 默认 | 142k | 40.8 | 18.6% |
| B. 开启默认 | 开 | 默认 | always | 默认 | 151k | 31.2 | 12.4% |
| C. 调优后(上线) | 开 | delay=1s, min=60s, max=300s, size=256MB | madvise | 绑至 node0 | 167k | 21.7 | 6.9% |
说明:
A→B:仅开启 AutoNUMA 就能感知到尾延下降,但仍有扫描抖动与 THP 干扰。
B→C:调参 + THP 改为 madvise + 中断/队列亲和后,P95 进一步下降 ~30%,远程命中降到 <7%,TPS 提升 ~10.6%。
9. 线上变更步骤(我那晚的实际顺序)
只读影子实例先演练(相同硬件/拓扑):确认内核参数和扫描频率不会带来明显 CPU 抖动。
业务低峰窗口:
- 开启 AutoNUMA(临时)→ 观察 10 分钟。
- 套用扫描参数 → 观察 numastat 与延迟曲线。
- 调整 THP 为 madvise,监看 mysqld RSS 与 minor/major fault。
- 绑定 NIC/NVMe 亲和到 node0,观察 softirq 分布与 si 占比。
- 确认无回退需求后:sysctl 持久化 + GRUB 兜底。
- 更新运维 Runbook 与标准镜像(避免下次新机再踩)。
10. 排障与坑位记录
(坑1)Node Interleaving 在 BIOS 被打开
现象:numactl -H 只有 1 个 node;AutoNUMA 无用。
处理:关掉 Node Interleaving,重启生效。
(坑2)systemd 或 cgroup 把数据库 CPU 绑死
现象:AutoNUMA 想迁移线程,但 cpuset 限制阻碍效果。
处理:放宽 CPUAffinity 或只做“优先”绑定(--preferred),让调度器有腾挪空间。
(坑3)THP=always 导致周期性抖动
现象:P95 每隔几分钟突刺,/proc/vmstat 中 THP 合并活动上升。
处理:改 madvise,或按数据库官方建议用静态大页。
(坑4)虚拟化环境误判
现象:KVM Guest 内看见的 NUMA 与宿主不一致;迁移无效。
处理:确保宿主正确设置 vNUMA 拓扑,并与 Guest CPU/内存亲和一致;否则 Guest 内调优收益有限。
(坑5)扫描参数过于激进
现象:sy 占比上升,kswapd/numa_balancing 相关开销显著。
处理:把 min_period 拉长、size_mb 适度增大,减少频繁小块扫描。
(坑6)误把性能问题归咎于磁盘
现象:iostat 干净,但延迟高。
处理:先看 numastat 与 perf,很多时候是内存跨节点造成的“像 I/O 一样的延迟”。
11. 我常用的一组“值班手顺”命令(可当清单)
# 1) NUMA 拓扑
lscpu | egrep 'Socket|NUMA|Thread|Core'
numactl -H
# 2) AutoNUMA 状态与参数
sysctl kernel.numa_balancing
sysctl kernel.numa_balancing_scan_delay_ms \
kernel.numa_balancing_scan_period_min_ms \
kernel.numa_balancing_scan_period_max_ms \
kernel.numa_balancing_scan_size_mb
# 3) 访问统计(观察10~30分钟趋势)
numastat -p $(pidof mysqld) # 或整个系统 numastat
vmstat 1
iostat -x 1
sar -n DEV 1
# 4) 中断与队列
cat /proc/interrupts | egrep 'eth|ixgbe|i40e|mlx'
cat /sys/class/net/eth0/device/numa_node
cat /sys/block/nvme0n1/queue/rq_affinity
# 5) 快速调参(在线试探)
sysctl -w kernel.numa_balancing=1
sysctl -w kernel.numa_balancing_scan_delay_ms=1000
sysctl -w kernel.numa_balancing_scan_period_min_ms=60000
sysctl -w kernel.numa_balancing_scan_period_max_ms=300000
sysctl -w kernel.numa_balancing_scan_size_mb=256
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
echo 2 > /sys/block/nvme0n1/queue/rq_affinity
12. 标准化落地(模板配置)
/etc/sysctl.d/99-numa.conf
kernel.numa_balancing=1
kernel.numa_balancing_scan_delay_ms=1000
kernel.numa_balancing_scan_period_min_ms=60000
kernel.numa_balancing_scan_period_max_ms=300000
kernel.numa_balancing_scan_size_mb=256
vm.zone_reclaim_mode=0
vm.swappiness=10
/etc/tuned/numa-db/tuned.conf
[main]
include=latency-performance
[vm]
transparent_hugepages=madvise
/etc/systemd/system/mysqld.service.d/numa.conf
[Service]
# 引导性优先,不做硬绑死
ExecStart=
ExecStart=/usr/bin/numactl --preferred=0 /usr/sbin/mysqld
13. 从 40ms 回到 20ms
当 AutoNUMA 的曲线稳定下来、numa_foreign 比例降到 7% 以下时,监控面板的 P95 曲线像被按了下去的弹簧,缓慢回落到了 20ms 出头。业务群里静了几分钟,产品经理发了个“已恢复,辛苦了”。
我收拾好工具包,回头看那台 2U 机器,风扇还在稳定地呼呼转。调内核参数这件事听起来冷冰冰,但它真的能在凌晨两点,帮你把一个大内存数据库从“跨节点的泥沼”里拽出来。
如果你也在香港或别的机房,被尾延迟追着跑,不妨从 AutoNUMA + 亲和 + THP 策略 这三个点下手。别急着一次“拉满”,先让系统说话,再顺着数据把旋钮拧到位。这,就是我那一夜学到的东西。
附:小 FAQ
Q:开启 AutoNUMA 是否总是有利?
A:不一定。极端低并发、强绑定、或实时性系统可能更适合固定亲和。先 A/B 对比再决定。
Q:多于两路的 NUMA(4 路/8 路)怎么办?
A:思路相同,但更要重视 中断亲和与 I/O 拓扑,以及把数据库的热写线程集中到“离存储近”的节点。
Q:为什么我开了反而更慢?
A:常见原因:扫描过于频繁、THP=always、BIOS Node Interleaving 开着、数据库被 cgroup 绑死、网卡/磁盘在另一个节点但中断没绑回。