PHP/Node.js香港服务器性能优化:CPU 绑核与中断隔离、禁用 THP、Nginx/PHP-FPM/pm2 全面调优(含完整配置与踩坑复盘)

凌晨 1:37,香港葵涌机房机柜第 7U 的那台应用机——负责 PHP 接口与 Node.js 事件流的混合业务——p99 延迟突然飙到了 900ms。业务侧说没有发布,监控上 CPU 占用并不夸张(avg 负载 7.x/24C),却频繁出现短促的尖峰。我蹲在机器前,手心还带着电池包的温度,做了一个决定:这一班,不再“头痛医头”。我把 CPU 绑核、内存页策略、网络与进程参数整套拉了起来,直到天亮,p99 回落到 120ms 以下,吞吐比之前提升了 38% 左右。下面是我当晚完整可复现的操作手记。
现场与基线
硬件/系统环境(真实参数)
| 项 | 配置 |
|---|---|
| 机房 | 香港葵涌 T3,单链路 BGP 10G 上联(电信/联通/CMI 叠加) |
| 服务器 | 单路 AMD EPYC 7402P(24C/48T,2.8GHz),NPS=4 |
| 内存 | 128GB DDR4-2933(4 NUMA Nodes,Each 32GB) |
| 系统盘 | Samsung PM983 1.92TB NVMe(ext4,noatime,nodiratime) |
| 网卡 | Intel X710-10G(驱动 i40e,RSS 开启) |
| 操作系统 | CentOS 7.9(3.10.0-1160 内核) |
| 业务栈 | Nginx 1.20 + PHP-FPM 8.1(Remi)+ Node.js 18 LTS(pm2) |
| 进程部署 | 同机:PHP 处理同步 API,Node 处理长连接/事件流与队列消费 |
注:CentOS 7 上我选择 Node 18 LTS(与 glibc 2.17 兼容),PHP 使用 Remi 仓库的 8.1。
优化前基线(30 分钟窗口)
| 指标 | 值 |
|---|---|
| 峰值 RPS(Nginx 总入口) | ~62k/s |
| p50 / p99 / p99.9 | 22ms / 410ms / 950ms |
| Node 事件循环延迟(均值/峰值) | 13ms / 210ms |
| PHP-FPM 队列长度(峰值) | ~180 |
| 网卡队列丢包(ifconfig RX dropped) | 偶发升高 |
| 软中断(softirq/s) | 峰值 110k,IRQ 与业务核抢占明显 |
| 上下文切换(cs/s) | ~120k(抖动大) |
目标:稳定把 p99 控在 150ms 内,吞吐提升 ≥25%,消灭“无辜尖峰”。
步骤 1:规划 CPU 绑核与“家务核”(Housekeeping Cores)
1.1 识别 NUMA/CPU 拓扑
lscpu -e=CPU,CORE,SOCKET,NODE,ONLINE | column -t | head
numactl --hardware
我的机器是单路 EPYC,但 NPS=4,会呈现 4 个 NUMA Node(每个 6C/12T)。我决定:
- Node.js(事件流):绑在 Node2、Node3(逻辑核 24–47)
- PHP-FPM(短请求):绑在 Node0、Node1(逻辑核 0–23)
- 家务核:每个 NUMA 留 1–2 个逻辑核给内核线程/中断(如 2、14、26、38)
- 原则:同进程的核尽量同 NUMA,降低跨节点内存访问;同时留出“家务核”处理软中断与系统杂务,避免业务核被打断。
1.2 内核启动参数隔离(可选增强)
编辑 /etc/default/grub,在 GRUB_CMDLINE_LINUX 增加(按实际核号):
isolcpus=managed,2,14,26,38 nohz_full=2,14,26,38 rcu_nocbs=2,14,26,38
我把“家务核”不隔离,把业务核交给 systemd cpuset 管控;也可以反过来——隔离业务核,仅把家务核留给内核。两者思路都行,但要保持 IRQ 亲和一致。
更新引导并重启:
grub2-mkconfig -o /boot/grub2/grub.cfg
reboot
步骤 2:用 systemd 精准绑定 Nginx/PHP/Node 的 CPU
CentOS 7 的 systemd(v219)支持 CPUAffinity=。我把三类进程分别放进不同的 slice。
2.1 定义 Slice
/etc/systemd/system/php-fpm.slice
[Unit]
Description=Slice for PHP-FPM
[Slice]
CPUAffinity=0-11
/etc/systemd/system/node.slice
[Unit]
Description=Slice for Node.js
[Slice]
CPUAffinity=24-47
/etc/systemd/system/nginx.slice
[Unit]
Description=Slice for Nginx
[Slice]
CPUAffinity=0-11
2.2 绑定服务到 Slice
/etc/systemd/system/php-fpm.service.d/override.conf
[Service]
Slice=php-fpm.slice
/etc/systemd/system/nginx.service.d/override.conf
[Service]
Slice=nginx.slice
如果用 pm2 管理 Node:
/etc/systemd/system/pm2-root.service(或 pm2 用户服务)
[Service]
Slice=node.slice
变更后执行:
systemctl daemon-reload
systemctl restart php-fpm nginx pm2-root
步骤 3:中断亲和与 irqbalance
3.1 固定网卡 IRQ 到“家务核”
查看网卡中断号:
grep -i "i40e" /proc/interrupts
假设网卡队列的 IRQ 为 120–127,我把它们绑到 2,14,26,38:
for i in {120..127}; do
echo 2,14,26,38 > /proc/irq/$i/smp_affinity_list
done
3.2 管住 irqbalance
irqbalance 常把我们手工绑的亲和性“拉平”。我在 /etc/sysconfig/irqbalance:
IRQBALANCE_BANNED_CPUS=
直接禁止它动关键 IRQ,或干脆临时停掉:
systemctl stop irqbalance
systemctl disable irqbalance
步骤 4:内存页策略(THP/HugePages)与 sysctl
低延迟业务下,THP(透明大页)的折叠/拆分可能带来抖动。我在这台机上统一禁用 THP,并保留少量静态 hugepages 给特定组件测试(非强制)。
4.1 禁用 THP(推荐)
临时:
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag
持久化 /etc/rc.local(确保可执行):
cat >> /etc/rc.local <<'EOF'
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag
EOF
chmod +x /etc/rc.local
4.2 sysctl 调优(网络/内存/文件)
/etc/sysctl.d/99-tuning.conf
# 文件与队列
fs.file-max = 2097152
fs.inotify.max_user_watches = 524288
# TCP 基础
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 250000
net.ipv4.tcp_max_syn_backlog = 16384
net.ipv4.ip_local_port_range = 10000 65000
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_mtu_probing = 1
# 缓冲
net.core.rmem_max = 268435456
net.core.wmem_max = 268435456
net.ipv4.tcp_rmem = 4096 87380 268435456
net.ipv4.tcp_wmem = 4096 65536 268435456
# 内存与脏页
vm.swappiness = 1
vm.dirty_ratio = 10
vm.dirty_background_ratio = 5
vm.max_map_count = 262144
生效:
sysctl --system
步骤 5:Nginx 参数(accept 队列与 CPU 亲和)
/etc/nginx/nginx.conf(节选)
worker_processes 12; # 与 PHP 绑的 0-11 同步
worker_cpu_affinity 00000000000000000000011111111111; # 可使用简化:每 worker 自动分配
worker_rlimit_nofile 1048576;
events {
worker_connections 65535;
use epoll;
multi_accept on;
accept_mutex off; # 配合 SO_REUSEPORT
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 20;
keepalive_requests 10000;
# 每监听端口开启 reuseport,提高多核接受连接能力
# 在 server 监听处使用:listen 80 reuseport;
}
若使用多进程监听,记得配合 listen ... reuseport;,并观察连接分布是否均衡。
步骤 6:PHP-FPM 深度调优
6.1 FPM 池配置
/etc/php-fpm.d/www.conf(核心项)
pm = static
; 计算见下文
pm.max_children = 160
pm.max_requests = 5000
request_terminate_timeout = 30s
rlimit_files = 1048576
; slowlog 与状态
slowlog = /var/log/php-fpm/www-slow.log
request_slowlog_timeout = 2s
catch_workers_output = yes
; 优化 realpath 与 opcache(php.ini)
6.2 Opcache/JIT(以 PHP 8.1 为例)
/etc/php.d/10-opcache.ini
opcache.enable=1
opcache.memory_consumption=512
opcache.interned_strings_buffer=64
opcache.max_accelerated_files=200000
opcache.validate_timestamps=0
opcache.save_comments=1
; JIT 在 IO 混合型收益有限,保持 conservative
opcache.jit=1205
opcache.jit_buffer_size=128M
动态发布可配合 opcache.revalidate_freq 与灰度开关,不要盲关 validate_timestamps 如果上线流程不稳定。
6.3 计算 pm.max_children
经验法:每个 PHP 进程常驻内存(RSS)× 并发上限 ≈ 可用内存(剔除 OS/页缓存/Node/Nginx)。
采样峰值:单 PHP 进程 RSS ≈ 450MB(带多扩展与大数组处理)
给 PHP 分配:72GB
72GB / 0.45 ≈ 160 ⇒ pm.max_children = 160
若出现 502/504 且 FPM 队列上升,先看 pm.max_children 是否顶满;不要一味加到溢出内存,宁可限流也别 OOM。
步骤 7:Node.js(pm2)与 V8/Libuv
7.1 pm2 生态配置(cluster + 绑核通过 slice)
/var/www/app/ecosystem.config.js
module.exports = {
apps: [{
name: "event-gateway",
script: "./server.js",
exec_mode: "cluster",
instances: 12, // 绑在 24-47,等价 12 worker
max_memory_restart: "2G",
env: {
NODE_ENV: "production",
UV_THREADPOOL_SIZE: "16",
// 视业务分配堆大小,避免 GC 暴冲
NODE_OPTIONS: "--max-old-space-size=2048 --max-semi-space-size=64 --stack_size=2048"
}
}]
}
CPU 亲和由 node.slice 控制,pm2 的 worker 继承父进程的 cpuset。
7.2 降低事件循环堵车
UV 线程池:大量 fs/crypto/dns 时可调到 16–32,纯网络 IO 则保守 8–16。
分离长任务:CPU 密集逻辑用 worker_threads 单独进程,或下沉到异步消费机。
GC:合理 --max-old-space-size,避免顶格吃内存导致 OOM Killer;关注 heapUsed/heapTotal。
步骤 8:网络与网卡细节
8.1 网卡 Ring/SRSS
ethtool -g eth0 # 查看 ring
ethtool -G eth0 rx 4096 tx 4096
ethtool -k eth0 # 关闭可能的合并影响延迟
ethtool -K eth0 gso off gro off lro off # 视业务而定,低延迟时常关
8.2 RPS/RFS(可选)
echo ffff > /sys/class/net/eth0/queues/rx-0/rps_cpus
# 配合 /proc/sys/net/core/rps_sock_flow_entries 与每队列 flow_entries
步骤 9:文件句柄与进程限制
/etc/security/limits.d/99-limits.conf
* soft nofile 1048576
* hard nofile 1048576
nginx soft nofile 1048576
nginx hard nofile 1048576
php-fpm soft nofile 1048576
php-fpm hard nofile 1048576
重启相关服务后用 ulimit -n 验证。
步骤 10:观测与压测方法(确保可复现)
10.1 压测
# 入口压力(两台压力机并发)
wrk -t24 -c4000 -d5m --latency http://vip.hk.example/api/ping
10.2 关键观测
pidstat -wtlu 1 # 观察上下文切换/阻塞
perf stat -e cycles,cache-misses,context-switches,cs,task-clock -p <php-fpm master pid> sleep 30
numactl -s # 校验绑定
cat /proc/interrupts | egrep "i40e|eth0"
ss -s # 套接字状态
优化后的对比(真实区间 1 小时)
| 指标 | 优化前 | 优化后 | 变化 |
|---|---|---|---|
| 峰值 RPS | ~62k/s | ~85k/s | +37% |
| p50 / p99 / p99.9 | 22/410/950ms | 15/120/280ms | p99 ↓70% |
| Node 事件循环峰值 | 210ms | 45ms | -78% |
| FPM 队列峰值 | ~180 | < 30 | 明显缓解 |
| RX dropped | 偶发 | 趋近 0 | 稳定 |
| softirq 峰值 | 110k/s | ~55k/s | -50% |
| cs/s 抖动 | 大 | 小 | 平滑 |
这些数字主要来自绑定 CPU、隔离 IRQ、禁用 THP 与合理的 pm/fd/网络参数组合;并不是某一个“银弹”。
踩坑与现场解决
irqbalance“偷改”亲和性
重启后丢包又起,查到 irqbalance 复活了。禁用服务并在 rc.local 里写死 关键 IRQ 亲和,才根治。
pm.max_children 盲目拔高导致 OOM
一度把 children 提到 220,短时 TPS 上来,但很快 OOM Killer 收割。按 RSS 采样算配额,宁可配合限流。
Node GC 抖动
堆太大(4G)时触发 Full GC,p99 上扬。调回 2G 并拆分 CPU 密集任务到 worker,事件循环延迟显著改善。
THP 在少数路由上反而更快?
某批量导出任务在 THP=madvise 下吞吐更高,但主业务延迟更差。最终主业务优先,全局禁用 THP,导出任务移到离线机。
Nginx reuseport 未配合 accept_mutex
一开始只开了 reuseport 没关 accept_mutex,连接分布不均。二者配套后才均衡。
附录:关键配置清单(可直接落地)
A. Remi 仓库 + PHP 8.1(CentOS 7)
yum install -y https://rpms.remirepo.net/enterprise/remi-release-7.rpm
yum-config-manager --enable remi-php81
yum install -y php php-fpm php-opcache php-mbstring php-xml php-json php-cli
B. Node.js 18 LTS(EL7 适配)
# NodeSource 或自行编译(建议用 NodeSource 针对 EL7 的构建)
curl -fsSL https://rpm.nodesource.com/setup_18.x | bash -
yum install -y nodejs
npm i -g pm2
pm2 startup systemd
C. Nginx(稳定版)与内核参数加载
yum install -y nginx
systemctl enable nginx php-fpm pm2-root
sysctl --system
D. 开机持久化脚本(/etc/rc.local)
#!/bin/bash
# Disable THP
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag
# IRQ 亲和(按实际 IRQ 范围调整)
for i in {120..127}; do
echo 2,14,26,38 > /proc/irq/$i/smp_affinity_list
done
exit 0
窗外天光刚泛白,机房走廊的冷白灯一如既往。那台“喘不过气”的机器像换了肺,仪表盘上的曲线乖乖贴着地面。我合上笔记本,去茶水间灌了一杯温水——不是因为“调了几个参数就涨了 30%”,而是因为我知道,这些改动都是“可控与可解释”的:CPU 不再被软中断抢走,GC 不再堵住事件循环,PHP 不再被队列淹没。真正的稳定,来自对系统每一层的尊重与取舍。
下一次告警再来,我也不怕——因为我手里不再是“玄学”,而是一套能在香港机房里复刻的硬核剧本。