宝塔面板在裸金属物理机上的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和内存的真正关系理顺了,差距就出来了。