上一篇 下一篇 分享链接 返回 返回顶部

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

发布人:Minchunlin 发布时间:2025-09-27 09:15 阅读量:687


凌晨 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 不再被队列淹没。真正的稳定,来自对系统每一层的尊重与取舍。
下一次告警再来,我也不怕——因为我手里不再是“玄学”,而是一套能在香港机房里复刻的硬核剧本。

目录结构
全文