高防CDN回源PHP-FPM打满?别死磕max_children:somaxconn、upstream keepalive与fastcgi_keep_conn的联动封锁
一、先看现场:CDN清洗正常,源站PHP-FPM却“假死”
某客户高防CDN节点回源量只有 3Gbps,源站是 16C32G 的 PHP 业务,PHP-FPM 进程数肉眼可见打满,Nginx 大量 502/504。登录源站一看:
ss -s
# TCP: 12034 (estab 312, closed 11520, orphaned 2, timewait 11480)
# 大部分 TIME_WAIT 集中在 Nginx 到 PHP-FPM 的 TCP 连接上
问题不在 CDN,也不在 PHP 代码,而是 Nginx 与 PHP-FPM 之间每次请求都新建连接,加上内核 backlog 太小,SYN 队列溢出,PHP-FPM 的 accept 队列被瞬间打爆。
二、内核层:somaxconn 和 tcp_max_syn_backlog 是隐形天花板
Nginx 和 PHP-FPM 都调用 listen(),内核的 net.core.somaxconn 决定 accept 队列上限。默认 128,高并发下根本不够。
# /etc/sysctl.d/99-php-fpm.conf
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15
net.ipv4.ip_local_port_range = 10000 65000
net.ipv4.tcp_abort_on_overflow = 0
tcp_abort_on_overflow = 0 很关键:当 accept 队列满时,内核默认丢弃 SYN,让客户端重传,而不是直接 RST,避免瞬时 502。生效:sysctl -p /etc/sysctl.d/99-php-fpm.conf。
三、PHP-FPM:listen.backlog 必须显式拉高,socket 放 tmpfs
PHP-FPM 默认 listen.backlog = 511,在 somaxconn 调大后它还是小。改 pool 配置:
; /etc/php-fpm.d/www.conf
listen = /dev/shm/php-fpm.sock
listen.backlog = 65535
listen.owner = nginx
listen.group = nginx
listen.mode = 0660
pm = static
pm.max_children = 256
pm.max_requests = 2000
为什么用 /dev/shm/php-fpm.sock?/dev/shm 是 tmpfs,走内存,比磁盘上的 Unix socket 少一次文件系统开销。注意权限和 SELinux,如果开了 SELinux 需要 chcon 或设 permissive。TCP 方式下 listen = 127.0.0.1:9000 会多一层 TCP 栈,高并发下 Unix socket 的 QPS 通常高 15%-30%。
pm = static 不是拍脑袋,在 CDN 回源流量突增时,dynamic 的扩容有延迟,容易雪崩。max_children 按内存算,但别只看 RSS,要留 20% 给内核和 Nginx。
四、Nginx 层:upstream keepalive + fastcgi_keep_conn 才是连接复用关键
很多人只调 fastcgi_pass,却没开连接池。Nginx 到 PHP-FPM 的 upstream 需要显式声明 keepalive:
upstream php_fpm {
server unix:/dev/shm/php-fpm.sock;
keepalive 128;
keepalive_requests 10000;
keepalive_timeout 60s;
}
server {
listen 80;
server_name example.com;
location ~ \.php$ {
fastcgi_pass php_fpm;
fastcgi_keep_conn on;
fastcgi_connect_timeout 1s;
fastcgi_send_timeout 5s;
fastcgi_read_timeout 60s;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
keepalive 128 表示每个 worker 最多保持 128 个空闲长连接。配合 fastcgi_keep_conn on,Nginx 会复用与 PHP-FPM 的 FastCGI 连接,不再每个请求都三次握手。实测在 4 核 8G 源站上,QPS 从 1200 提到 3800,TIME_WAIT 从 11000 降到 200 以内。
注意:fastcgi_keep_conn on 要求 PHP-FPM 的 listen 支持,Unix socket 天然支持。如果用的是 TCP,也支持。
五、压测验证:别用 ab,用 wrk 看长连接效果
wrk -t8 -c2000 -d30s --latency http://127.0.0.1/index.php
# 观察 PHP-FPM status: curl http://127.0.0.1/php_status
# 观察 ss -s 中 timewait 数量
如果 timewait 仍然很高,检查 net.ipv4.tcp_tw_reuse 是否生效,以及 Nginx 的 keepalive 是否配置在 upstream 块内而不是 server 块。另一个坑:fastcgi_keep_conn 在 Nginx 1.15+ 才稳定,老版本可能不生效。
六、高防 CDN 场景下的额外注意
高防 CDN 回源 IP 通常是一个网段,如果源站 Nginx 做了 allow 白名单,记得把 CDN 回源 IP 加进去。另外,CDN 的 CC 防护会主动丢弃部分连接,源站的 listen.backlog 要留够余量,否则 CDN 重试会放大回源压力。
我们在轻云互联的高防 CDN 节点上做过对比,回源走 Unix socket + keepalive 的源站,在 10G 口打满时 PHP-FPM 的 CPU 抖动明显更小,因为 accept 队列不再丢包。如果源站是轻云互联的服务器,交付时内核参数已经预调过 somaxconn,省去很多折腾。
七、一张图总结调优链路
- 内核:somaxconn=65535,tcp_max_syn_backlog=65535,tcp_tw_reuse=1
- PHP-FPM:listen.backlog=65535,Unix socket 放 /dev/shm,pm=static
- Nginx:upstream keepalive 128+,fastcgi_keep_conn on,fastcgi_connect_timeout 1s
- 验证:ss -s 看 timewait,wrk 看 QPS,php-fpm status 看 listen queue
别再把 max_children 当银弹。连接层的复用和内核队列,才是高防 CDN 源站扛住回源风暴的底牌。