宝塔面板在裸金属物理机上的NUMA陷阱:PHP-FPM与MySQL绑核/绑内存实操指南

三年前的深夜,我把一套在VPS上丝滑运行的宝塔LNMP搬迁到双路E5裸金属服务器上。配置翻了三倍,并发反而掉了四成。`top` 里 `si` 软中断高得吓人,MySQL的 `CPU` 使用率不到30%,PHP-FPM的 `load` 却全线飘红。排查到最后,真凶不是配置项,而是CPU物理拓扑——**NUMA(Non-Uniform Memory Access)**。 云服务器上你几乎感知不到NUMA,因为宿主机屏蔽了真实拓扑。但裸金属物理机是全权交到你手里的,默认的“等概率分配”策略在跨节点访问内存时,性能衰减极其恐怖。 这篇文章就用一套可直接抄作业的 `systemd` 配置方案,解决宝塔面板在裸金属上的跨NUMA节点访问问题。 ## 先确认:你的服务器到底有多少个NUMA节点 别急着重启服务,先用工具看物理拓扑。SSH进入服务器,按顺序执行: ```bash # 查看CPU拓扑,重点看NUMA node0/1的CPU列表 lscpu | grep -A 8 "NUMA" # 更详细的内存分配策略视图 numactl --hardware ``` 典型双路E5的输出长这样: ```bash available: 2 nodes (0-1) node 0 cpus: 0 1 2 3 4 5 6 7 16 17 18 19 20 21 22 23 node 0 size: 127893 MB node 0 free: 118905 MB node 1 cpus: 8 9 10 11 12 13 14 15 24 25 26 27 28 29 30 31 node 1 size: 129024 MB node 1 free: 123456 MB node distances: node 0 1 0: 10 21 1: 21 10 ``` 关键看 `node distances` 矩阵:访问本节点内存代价是10,访问其他节点的代价是21,直接翻倍。 ## 识别故障:PHP-FPM与MySQL在交叉分配内存 宝塔面板默认安装的PHP-FPM和MySQL,systemd服务文件里完全没有NUMA感知。Linux内核的默认策略 `localalloc` 会导致一个很尴尬的局面:进程调度到CPU Node 0,但内存分配却落在了Node 1。跨节点内存访问通过QPI/UPI总线传输,延迟直接翻一倍。 **PHP-FPM表现:** 大量worker在Node 0和Node 1之间来回切换,CPU缓存反复失效,负载虚高。 **MySQL表现:** InnoDB缓冲池(Buffer Pool)的内存页被分散到两个节点,每次读写都要跨总线访问,磁盘IO延迟看似正常,但SQL查询耗时涨了30%以上。 我用一个暴力方法验证问题——直接查看进程当前的内存分配节点: ```bash # 查看MySQL主进程的NUMA状态 PID=$(pgrep -f mysqld | head -1) cat /proc/$PID/numa_maps | head -20 # 如果看到类似 interleave:0-1 或者大量 "N0=" 和 "N1=" 混合出现,说明内存已经交叉 ``` 解决办法:**把PHP-FPM绑定到Node 0,把MySQL绑定到Node 1,让CPU和内存永远在本节点内通信。** ## 方案:通过systemd override实现高效绑核 宝塔面板升级或修复环境时,会覆盖默认的服务配置文件。所以我们不改宝塔生成的原始文件,用 `systemctl edit` 添加override配置,这样宝塔重新生成文件后,我们的覆盖规则依然生效。 ### 为PHP-FPM绑定Node 0 先找到宝塔PHP-FPM的服务名称: ```bash # 以PHP 7.4为例,环境上实际版本可通过 ls /etc/systemd/system/ 确认 systemctl list-units | grep php-fpm ``` 然后添加override配置: ```bash systemctl edit php-fpm-74 ``` 写入以下内容(**注意ExecStart前的空行不是必需的,但建议保留清晰结构**): ```ini [Service] # 清空宝塔原始的执行命令 ExecStart= # 使用numactl绑定Node 0的物理核心:CPU 0-7 和 16-23,内存也只用Node 0 ExecStart=/usr/bin/numactl --physcpubind=0-7,16-23 --membind=0 /usr/bin/php-fpm-74 --nodaemonize # 防止OOM时内核随意kill worker进程 OOMScoreAdjust=-500 ``` 保存后重载并重启: ```bash systemctl daemon-reload systemctl restart php-fpm-74 # 验证绑定是否生效 cat /proc/$(pgrep -f php-fpm | head -1)/numa_maps ``` ### 为MySQL绑定Node 1 同理,宝塔的MySQL服务名称一般是 `mysqld`: ```bash systemctl edit mysqld ``` 写入: ```ini [Service] ExecStart= # 绑定Node 1的物理核心:CPU 8-15 和 24-31 ExecStart=/usr/bin/numactl --physcpubind=8-15,24-31 --membind=1 /usr/sbin/mysqld --basedir=/www/server/mysql ``` 如果你用的是MySQL 8.0,宝塔的启动命令里通常带有 `--user=mysql` 参数,建议先用 `systemctl cat mysqld` 查看原始命令,然后在 `ExecStart=` 后面原样保留参数,只加 `numactl` 前缀。 重启验证: ```bash systemctl daemon-reload systemctl restart mysqld # 确认MySQL所有线程的内存都落在N1上 grep -E "N[01]=" /proc/$(pgrep -x mysqld)/numa_maps | head -5 ``` ## 进阶:Nginx和Redis的分配策略 如果Node 0的PHP-FPM吃满了CPU配额,Node 1的MySQL压力不大,可以把Redis也塞到Node 1。但注意:**Redis的单线程模型对内存延迟极其敏感**,跨节点访问的代价比MySQL更明显。 ```bash systemctl edit redis # 宝塔的服务名可能是 redis-server ``` ```ini [Service] ExecStart= ExecStart=/usr/bin/numactl --physcpubind=8-15,24-31 --membind=1 /usr/bin/redis-server /www/server/redis/redis.conf ``` Nginx则不建议强制绑核,它本身就是多进程异步模型,让内核调度反而更好。 ## 验证:数字不会说谎 绑定前先记录一组基准数据,我用的是 `sysbench` 和一条简单的PHP循环脚本: ```bash # 内存延迟测试 sysbench memory --memory-block-size=1M --memory-total-size=10G --threads=4 run # 绑定后重新执行对比 numactl --membind=0 sysbench memory --memory-block-size=1M --memory-total-size=10G --threads=4 run ``` 在我负责的一台物理机上,绑定前后数据如下: | 场景 | 内存吞吐 | PHP-FPM CPU负载(并发100) | MySQL TPS | |------|----------|----------------|-----------| | 绑定前 | 8.6 GB/s | 74% | 3120 | | 绑定后 | 11.2 GB/s | 41% | 4890 | 跨节点访问消除后,PHP 脚本的 `p95` 响应时间直接从 320ms 降到 180ms,这才是裸金属该有的性能。 ## 冷门排错:绑定后服务起不来怎么办 ### 报错1:`Cannot allocate memory` 确认你绑定的NUMA节点内存是否充足。MySQL的Buffer Pool设为总内存的70%以上时,Node 0的剩余内存可能不够PHP-FPM分配。用 `free -h` 和 `numactl --hardware` 交叉核对,或者把MySQL的 `innodb_buffer_pool_size` 适当调小。 ### 报错2:`numactl: command not found` 系统缺少numactl工具,安装即可: ```bash # CentOS / 轻云互联裸金属镜像默认可能不装 yum install numactl -y ``` ### 报错3:宝塔重启后override失效 宝塔面板在软件升级时,会尝试重写systemd服务文件。override方案理论上不受影响,但如果宝塔把服务改名(比如PHP 7.4升到8.0),你需要重新执行一次 `systemctl edit`。建议切换PHP版本后,用下面这条命令检测当前实际生效的NUMA策略: ```bash for pid in $(pgrep php-fpm); do echo "PID $pid -> $(cat /proc/$pid/numa_maps | awk '{print $1}' | head -1)"; done ``` ## 最后补一刀:关闭THP,别再让内存页搞事情 NUMA绑核折腾完,顺手把透明大页(THP)关掉。跨节点场景下,THP的内存规整(compaction)频繁发生,直接导致偶发性的延迟尖刺。 ```bash echo never > /sys/kernel/mm/transparent_hugepage/enabled echo never > /sys/kernel/mm/transparent_hugepage/defrag # 写入/etc/rc.local或systemd/tmpfiles实现永久生效 ``` 这一套操作下来,宝塔面板在裸金属物理机上的表现才算真正对得起硬件。如果是轻云互联这类提供完整物理机权限的服务器,内核参数和NUMA策略完全可以按需定制;像这种绑核操作,本地跑个脚本就能自动部署到所有新采购的机器上,省去每台上线都手动调的麻烦。 别再说“裸金属性能不如云主机”了,先把CPU和内存的真正关系理顺了,差距就出来了。