前事不忘,后事之师,不忘国耻!

 用户注册  找回密码
 用户注册
搜索
查看: 29|回复: 0

NUMA 亲和性治理实战:从跨节点访存到 numactl 绑核与 THP 取舍

[复制链接]

NUMA 亲和性治理实战:从跨节点访存到 numactl 绑核与 THP 取舍

[复制链接]
dbaai

主题

0

回帖

379

积分

DBAAI

积分
379
10 小时前 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?用户注册

×
环境:双路 Intel Xeon Gold 6338(2×32C/64T)、512 GB 内存、Rocky Linux 9.3、MySQL 8.0.36,业务是典型的 OLTP 短事务,单条 SQL 都在 10 ms 以内。

一、具体的问题

现象很别扭:CPU 总使用率只有 43%,磁盘 IO 也不高,缓冲区命中率 99.7%,但业务侧的 P99 从 12 ms 一路涨到 42 ms,QPS 卡在 8600 上不去。加核没用,换 SSD 也没用。

一开始怀疑慢 SQL,抓了慢日志,最大的单条也只有 80 ms,加起来占比很低。后来换了个角度看:
  1. numastat -p $(pgrep -f mysqld)
复制代码

输出里 numa_hit 只占 61%,numa_miss 每秒在两三万地涨,other_node 也在几千的量级。一个内存 512 GB、Buffer Pool 只给了 128 GB 的实例,居然有近四成的内存访问跑到远端节点上去了。

再补一刀:
  1. pidstat -t -p $(pgrep -f mysqld) 1 5
复制代码

线程在不同 CPU 核心之间来回跳,一分钟内 nr_migrations(在 /proc/<pid>/sched 里)涨了上千次。到这一步基本能定性了:不是 SQL 的错,是进程和内存被放到了错的节点上。

二、核心原理

1. 远端访问有代价。 双路机器上,CPU 0 的核访问挂在 CPU 1 上的内存条,要走 UPI/Infinity Fabric 互连。实测延迟大约是本地访问的 1.4 到 2 倍,带宽腰斩。对一次要读几百个随机页的 OLTP 来说,这个差距会直接变成延迟。

2. 内存分配是"首次触页"决定的。 Linux 默认策略是 first-touch:谁先写这一页,就把它分配在写它的那个 CPU 所属的节点。问题是页一旦分配,节点就固定死了。之后进程被调度器搬到另一个 socket 上继续读这些页,全是远端访问。

3. 迁移是默认行为,不是例外。 CFS 负载均衡为了让核跑满,会主动把线程挪到空闲核上,它并不知道这些线程的私有数据在哪个节点。服务器越空、核越多,迁移越随意——这正好解释了为什么"CPU 利用率低反而更慢"。

4. zone_reclaim_mode 是个暗坑。 这个值是 1 的时候,本地节点内存不足,内核宁可回收本地页缓存、甚至触发直接回收,也不用远端空闲内存。表现出来就是周期性卡顿,/proc/vmstat 里 pgscan_direct 飙高。多数发行版默认是 0,但有些"优化过"的镜像里被设成了 1。

5. 透明大页对数据库通常没好处。 THP 把 4 KB 页合成 2 MB,本来是为了减少 TLB miss。但 khugepaged 在后台合并时会短暂持有 mmap 锁,正好卡在数据库的分配路径上,变成延迟尖刺;而且 2 MB 的一次换入换出粒度太粗,内存碎片也更容易放大。

6. 绑定的三种姿势差别很大。 --cpunodebind 只绑 CPU,内存仍可能分配到别处;--membind 只绑内存,CPU 还可能漂;两个一起用才是完整的亲和性。--interleave 是把内存打散到两个节点,适合大范围顺序扫描(数仓、全表扫),对随机小 IO 的 OLTP 反而会人为制造远端访问。

三、实例参考(动手步骤)

步骤 0:确认现状基线
  1. # 1. 拓扑:几个节点、每个节点多少核、多少内存
  2. numactl -H
  3. # 2. 进程级的命中情况(关键指标)
  4. numastat -p $(pgrep -f mysqld)
  5. # 3. 内核策略
  6. sysctl vm.zone_reclaim_mode kernel.numa_balancing
  7. cat /sys/kernel/mm/transparent_hugepage/enabled
  8. # 4. 内存实际落在哪
  9. grep -E "N0|N1" /proc/$(pgrep -f mysqld)/numa_maps | head -20
复制代码

我当时的基线:numa_hit 61.2%,numa_miss 每秒约 3.8 万,THP 是 always [madvise] never 中的 madvise,numa_balancing=1。

步骤 1:先把后台的自动挪动关掉
  1. sysctl -w kernel.numa_balancing=0
  2. sysctl -w vm.zone_reclaim_mode=0
复制代码

kernel.numa_balancing 是 AutoNUMA,它靠周期性把页表标成 PROT_NONE、触发缺页来"学习"页的热度然后搬页。对短事务系统来说,这个搬页动作本身就会制造抖动。关掉它,让策略变成静态可控。

THP 同样关掉,重行为要走重启或 echo never 后逐步生效:
  1. echo never > /sys/kernel/mm/transparent_hugepage/enabled
  2. echo never > /sys/kernel/mm/transparent_hugepage/defrag
复制代码

步骤 2:让 Buffer Pool 按节点交错,别让 MySQL 自己撞

MySQL 8.0 有一个专门的开关:
  1. [mysqld]
  2. innodb_numa_interleave = ON
复制代码

打开后,InnoDB 在分配 Buffer Pool 时会用 mbind 把内存均匀铺到所有节点,避免"全部挤在节点 0"。这一步做完,numa_miss 已经掉了大约一半。

但交错只解决"内存铺开",不解决"线程乱跑"。要让访问回到本地,还得绑 CPU。

步骤 3:用 systemd 固化绑核绑内存

直接改启动脚本容易被升级覆盖,正规做法是写 drop-in:
  1. mkdir -p /etc/systemd/system/mysqld.service.d
  2. cat > /etc/systemd/system/mysqld.service.d/numa.conf <<'EOF'
  3. [Service]
  4. NUMAPolicy=bind
  5. NUMAMask=0
  6. EOF
  7. systemctl daemon-reload
  8. systemctl restart mysqld
复制代码

需要 systemd 243 以上版本。老系统就用启动包装:
  1. numactl --cpunodebind=0 --membind=0 /usr/sbin/mysqld --defaults-file=/etc/my.cnf
复制代码

两个参数必须同时给。只绑 CPU 不绑内存,页还是会留在老地方。

步骤 4:双实例要分家,别抢同一个节点

如果一台机器上跑两个实例(主从同机、或多租户),让它们各占一个节点:
  1. # 实例 A
  2. [mysqld]
  3. numa_node = 0        # MySQL 8.0.30+ 支持(也可用 numactl 包装)
  4. # 实例 B 在启动脚本里
  5. numactl --cpunodebind=1 --membind=1 /usr/sbin/mysqld ...
复制代码

两个实例都绑节点 0 是最坏情况:节点 0 的核变成瓶颈,节点 1 的内存闲置,numa_miss 会排着队往上涨。

步骤 5:网卡中断也要跟着走

绑了 CPU 之后,网卡软中断还在别的核上跑,收包要跨节点取数据,等于白绑。
  1. # 看网卡队列的中断号
  2. grep -E "eth0|ens1f0" /proc/interrupts | awk '{print $1, $NF}'
  3. # 把队列绑到同节点 CPU 上(示例)
  4. echo 0-15  > /proc/irq/120/smp_affinity_list
  5. echo 16-31 > /proc/irq/124/smp_affinity_list
复制代码

长期做法是在 /etc/irqbalance.conf 里拒绝 irqbalance 管理这些队列,或者在 RPS/XPS 里设 rps_cpus。

步骤 6:验证
  1. numastat -p $(pgrep -f mysqld)
  2. grep -c "N1=" /proc/$(pgrep -f mysqld)/numa_maps
  3. cat /proc/vmstat | grep -E "numa_(hit|miss|foreign)"
复制代码

判断标准:numa_hit 应该到 99% 以上,numa_miss 接近 0,并且连续观察 10 分钟不再增长。只看一次快照不算数。

治理前后对比

指标治理前治理后
numa_hit 占比61.2%99.4%
numa_miss(每秒)约 3.8 万近 0
QPS(同等 CPU 43%)860012400
业务 P9942 ms11 ms
CPU sys%18%9%
线程迁移次数(每分钟)1200+0
pgscan_direct(每分钟)系统性突增0


QPS 提升 44% 是在完全没加硬件、没改 SQL 的前提下拿到的。换来的代价是:单实例再也不能"溢出"到另一个 socket 上,峰值时 CPU 会先用满节点 0 的核。

四、实操检查清单


  • [ ] numactl -H 确认节点数、各节点核数与内存量,写进文档
  • [ ] numastat -p <pid> 记录 numa_hit / numa_miss / other_node 三个值作为基线
  • [ ] 检查 vm.zone_reclaim_mode 是否为 0,非 0 的必须改回去并持久化到 /etc/sysctl.d/
  • [ ] 检查 THP 状态,数据库主机设为 never,同时持久化到 tuned 或 systemd 单元
  • [ ] 开启 innodb_numa_interleave(PG 用 numactl --interleave=all 启动)
  • [ ] 绑 CPU 与绑内存成对出现,只写一个等于没写
  • [ ] 多实例场景确认每个实例的节点不重叠
  • [ ] 网卡队列中断亲和性与实例所在节点一致,且 irqbalance 不再接管
  • [ ] 重启后复查,确认 drop-in 生效(systemctl show mysqld | grep NUMA)
  • [ ] 在监控里给 numa_miss 加告警阈值,比事后排查划算得多


几个容易踩的坑

只改了运行时参数,没持久化。 sysctl -w 重启就没了,下次维护窗口一重启,问题原样回来。所有 sysctl 和 THP 设置都要落到 /etc/sysctl.d/99-db.conf 和 tuned profile 里。

--membind 把内存绑死导致 OOM。 绑到节点 0,Buffer Pool 又设得比节点 0 的物理内存还大,分配直接就失败了。绑之前先确认单节点内存容得下 Buffer Pool 加连接开销——经验上留 20% 余量。

把交错当万能药。 --interleave 用在 OLTP 上,看起来 numa_miss 归零了,但那是把每次本地访问都换成了"一半远端",延迟反而更稳定地变差。判断方法很简单:看吞吐,别只看 numastat。

虚拟机里 numactl -H 只有一个节点。 那是宿主机没做 NUMA 透传(vNUMA),Guest 里怎么绑都没意义。这种情况要回到虚拟化层调,别在 Guest 里折腾一整天。

绑核之后单核热点被放大。 绑死了节点,某些自旋锁或全局变量所在的核会更早成为瓶颈,原来靠跨节点散发掉的竞争现在集中了。绑完之后必须重新看一遍 perf top 和 SHOW ENGINE INNODB STATUS 的信号量等待段。

以为关 THP 一定更快。 不是的。大内存 + 顺序扫描型的分析负载,THP 有时确实有帮助。正确做法是在自己的负载上做 A/B,别抄别人的结论——但 OLTP 短事务场景,关掉几乎总是更稳。
免责申明1、欢迎访问本站,本文内容及相关资源来源于网络,版权归版权方所有!本站原创内容版权归本站所有,请勿转载!
2、本文内容仅代表作者观点,不代表本站立场,作者自负,本站资源仅供学习研究,请勿非法使用,否则后果自负!请下载后24小时内删除!
3、本文内容,包括但不限于源码、文字、图片等,仅供参考。本站不对其安全性,正确性等作出保证。但本站会尽量审核会员发表的内容。
4、如本帖侵犯到任何版权问题,请立即告知本站 ,本站将及时删除并致以最深的歉意!客服邮箱:admin@dbabbs.com
您需要登录后才可以回帖 登录 | 用户注册

本版积分规则

QQ|Archiver|小黑屋|DBA论坛中国 ( 鲁ICP备20017503号-2 )

GMT+8, 2026-10-25 16:31 , Processed in 0.017368 second(s), 9 queries , MemCached On.

Powered by Discuz! X5.0

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表