502 不是 Nginx 的错,是 PHP-FPM 池子满了
很多站长对 502 Bad Gateway 的第一反应是「Nginx 挂了」或者「服务器被打死了」,于是重启 Nginx、重启 PHP-FPM,页面恢复,问题暂时消失——三天后卷土重来。真正的根因通常在 Nginx 的 error log 里写得清清楚楚:
connect() to unix:/run/php/php8.2-fpm.sock failed (11: Resource temporarily unavailable)
recv() failed (104: Connection reset by peer) while reading response header from upstream这两行是两种不同的故障。Resource temporarily unavailable 意思是 PHP-FPM 的监听队列已经满了,Nginx 连进程池的 socket 都排不上队;Connection reset by peer 则是子进程接手请求后崩了或者被杀了。本文只讲第一种——进程池容量算错,这是个人站长最容易踩、也最容易修好的坑。
先搞懂 pm 三个模式:static、dynamic、ondemand
PHP-FPM 的进程管理由 pm 参数决定,三种模式行为完全不同,选错了后面所有参数都白调。
pm = static:固定进程数
pm = static
pm.max_children = 20启动时直接 fork 20 个子进程,之后不再增减。优点是响应快(没有 fork 开销),内存占用可预测;缺点是哪怕凌晨没人访问,20 个进程也占着内存。适合流量稳定、内存充足的站,或者容器里明确分配了内存的场景。
pm = dynamic:按需伸缩(默认,也是最常配错的)
pm = dynamic
pm.max_children = 20
pm.start_servers = 5
pm.min_spare_servers = 3
pm.max_spare_servers = 10
pm.max_requests = 500启动时开 start_servers 个,空闲进程低于 min_spare_servers 就 fork 新的,高于 max_spare_servers 就杀掉多余的,上限是 max_children。看起来智能,但伸缩是有延迟的:突发流量到来时,新进程的 fork 跟不上请求到达的速度,队列立刻堆积,Nginx 就报 Resource temporarily unavailable。
pm = ondemand:极度省内存,但延迟高
pm = ondemand
pm.max_children = 20
pm.process_idle_timeout = 10s
pm.max_requests = 500没有请求就一个进程都不留(常驻 0 个),来了请求才 fork。省内存到极致,适合小内存 VPS 或访问极稀疏的站。代价是每个新请求都要等 fork,首次响应可能多出几十毫秒。追求 TTFB 的站不要用。
pm.max_children 到底该填多少:别拍脑袋,算
这是全篇最重要的公式。填太小 → 502;填太大 → 内存耗尽被 OOM Killer 杀,或者触发 swap 把整机拖死。正确算法是从单进程内存反推,而不是从 CPU 核数猜:
pm.max_children = 可用于 PHP 的内存 / 单个 PHP 进程平均常驻内存先量单个进程的真实占用。别用 ps aux 的 RSS 直接加,那个数字对 PHP 经常偏小(共享库被重复计算)。用这个脚本取真实的常驻内存(PSS):
# 统计所有 php-fpm 池进程的平均 PSS(KB)
for pid in $(pgrep -f 'php-fpm: pool www'); do
awk '/^Pss:/ {s+=$2} END {print s}' /proc/$pid/smaps_rollup 2>/dev/null
done | awk '{sum+=$1; n++} END {if(n) printf "进程数=%d 平均PSS=%.1f MB\n", n, sum/n/1024}'假设量出来平均 55MB,服务器是 2GB 内存,其中系统 + MySQL + Nginx 已经吃掉 900MB,留给 PHP 的约 1100MB,那么:
pm.max_children = 1100 / 55 ≈ 20但这 20 是理论上限,还要给突发留缓冲、给系统留余量,实践中我一般取算出来的 70%~80%,也就是 14~16。留出来的这部分不是浪费,是防止某个请求临时吃内存时整机雪崩。
反过来,如果你发现 pm.max_children 已经开到 50、100 还在 502,那问题不在容量,而在单个请求太慢——池子被慢请求占满了。这种情况加进程是饮鸩止渴,必须去查慢查询、外部 API 超时、或者被攻击。怎么判断?看 FPM 的 status 页(下一节)。
打开 FPM status 页:这是排查池子的唯一可靠仪表盘
不开 status 页调 PHP-FPM 等于闭着眼开车。在池配置里加:
; /etc/php/8.2/fpm/pool.d/www.conf
pm.status_path = /fpm-status
ping.path = /fpm-ping然后在 Nginx 里只允许本机访问,绝不暴露公网:
location = /fpm-status {
allow 127.0.0.1;
allow ::1;
deny all;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}请求 curl "http://127.0.0.1/fpm-status?full",关键字段这样读:
active processes:当前正在处理请求的进程数。如果它长期贴着 max_children,池子就是饱和的。idle processes:空闲进程。持续为 0 说明没有余量接新请求。listen queue和max listen queue:这是 502 的直接证据。listen queue 大于 0 表示有请求在排队;max listen queue 记录了历史峰值。只要这个峰值超过 0,你就一定在某个时刻丢过请求。max children reached:进程数达到上限的次数。非 0 就说明池子撞过顶,必须扩或优化。
用 ?full 参数还能看到每个进程正在跑哪个脚本、跑了多久——哪个页面在拖垮池子,一目了然。
pm.max_requests:防内存泄漏的保险丝
pm.max_requests = 500PHP 进程跑久了内存会缓慢增长——可能是第三方库泄漏、可能是 opcache 碎片、也可能是某个插件在全局变量里越攒越多。pm.max_requests 让一个子进程处理够 N 个请求后自动退出、由 master 重新 fork,把内存还给系统。
取值有讲究:太小(比如 50)会导致进程频繁重建,每次重建都要重新加载全部 PHP 代码,CPU 白烧;太大(比如 0 即不限制、或 100000)等于没保护。我的经验值是 300~1000,配合监控单进程 RSS 曲线来定:如果 RSS 在 500 个请求内涨了 30% 以上,就把这个值降到 300。
验证方式很直接,观察进程 PID 是否在轮换:
watch -n 2 'ps -o pid,rss,etime,cmd -C php-fpm | head -20'dynamic 模式的参数约束:配错直接启动失败
这几个参数有硬性关系,违反的话 PHP-FPM 会拒绝启动,日志里写 failed to post process: permission denied 或者直接 ERROR: You must set pm.start_servers:
pm.min_spare_servers <= pm.start_servers <= pm.max_spare_servers
pm.max_spare_servers <= pm.max_children常见的错误是 start_servers 比 min_spare_servers 还小,或者 max_spare_servers 直接等于 max_children——后者会导致进程永不回收,等价于 static,但你又以为配的是 dynamic,非常隐蔽。
我的动态配比经验:min_spare 设为 max_children 的 20%~25%,max_spare 设为 40%~50%,start_servers 取两者中间。以 max_children=20 为例:
pm.start_servers = 6
pm.min_spare_servers = 5
pm.max_spare_servers = 10
pm.max_children = 20socket 还是 TCP:一个被忽视的容量因素
PHP-FPM 监听方式也影响表现。Unix socket 少一层网络栈,通常更快,但有两个坑:
listen.backlog默认 511。突发流量下,如果 backlog 太小,内核会直接丢弃连接,Nginx 立刻看到Resource temporarily unavailable。加大它:listen = /run/php/php8.2-fpm.sock listen.backlog = 4096- socket 文件路径太长(超过 107 字节)会静默失败。别把 socket 丢到多层嵌套目录里。
如果 PHP 和 Nginx 不在一台机器(或者跑在容器里跨 namespace),只能用 TCP:listen = 127.0.0.1:9000。注意配 listen.allowed_clients = 127.0.0.1 限制来源,否则 9000 端口被扫到就是灾难。
改完怎么验证?四步闭环
- 改之前记录基线:
curl "http://127.0.0.1/fpm-status"存一份,记下 active/idle 和 max children reached。 - 平滑重载,不要 restart:
systemctl reload php8.2-fpm会让旧进程处理完当前请求再退出,业务不中断。restart会掐断正在进行的请求,用户直接看到 502。 - 压测验证:用 ab 或 wrk 打一轮,观察 status 页的 listen queue 和 max children reached 是否仍然增长。
wrk -t4 -c60 -d30s --latency https://www.example.com/ - 看错误日志归零:
grep -c 'Resource temporarily unavailable' /var/log/nginx/error.log,压测前后对比,理想情况是压测期间不再新增。
一个真实的反例:max_children 开太大反而全站崩
我曾接手一个 2GB 内存的 VPS,站长把 pm.max_children 设成了 100(因为「网上教程说流量大就调大」)。结果是:正常情况下只有 5 个进程在跑,但在一次流量小高峰时,dynamic 模式疯狂 fork 到 100 个进程,每个 55MB,瞬间吃掉 5.5GB 内存——远超物理内存,内核开始疯狂 swap,整机 IO 打满,SSH 都连不上,最后 OOM Killer 随机杀进程,MySQL 被干掉,网站比 502 更彻底:连数据库都没了。
修法不是「调大」也不是「调小」,而是按内存算容量 + 限制上限 + 加监控:max_children 降到 16,加 pm.max_requests = 500 防泄漏,再用一条 cron 每 5 分钟检查 status 页,max children reached 增长就告警。
并发容量之外:慢日志与请求超时才是 502 的隐性来源
就算 pm.max_children 算得再准,只要池子里有请求「赖着不走」,容量也会被慢慢耗光。两种情况特别常见:
其一,后端调用没有超时。 PHP 里用 curl 请求第三方 API、用 PDO 连数据库、或者用 file_get_contents 拉远程资源,默认超时可能长得离谱(curl 默认不限时,file_get_contents 默认 60 秒)。一旦对方变慢,每个请求都会占住一个 FPM 进程几十秒。二十个进程池,被二十个这样的卡死请求占满,第 21 个请求立刻就是 502。修法是给每一个出网调用加显式超时:
; php.ini:兜底限制单请求最长执行时间
max_execution_time = 30
; 让 exec/curl 这类外部等待也算进执行时间(部分 SAPI 有效)
max_input_time = 30$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_CONNECTTIMEOUT => 3, // 连接超时 3 秒
CURLOPT_TIMEOUT => 8, // 总超时 8 秒
CURLOPT_RETURNTRANSFER => true,
]);其二,FPM 侧没有超时兜底。 无论 PHP 代码怎么写,池子这一层都该设一道硬闸:
; 单个请求最长处理时间,超时后 FPM 直接杀掉进程
request_terminate_timeout = 60
; 记录执行超过 5 秒的脚本(用来定位是哪个页面在拖后腿)
request_slowlog_timeout = 5s
slowlog = /var/log/php-fpm/slow.log有了 slowlog,502 发生时直接 tail -50 /var/log/php-fpm/slow.log,就能看到具体是哪个脚本、卡在哪一行(会附上 PHP 调用栈)。这是从「容量不够」走向「找到真正慢点」的关键一步,比反复调大 max_children 有效得多。
内存相关的两个致命配置:别让 OOM 冒充 502
有时候页面报的不是 502,而是 500 或者空白页,但根因同源——进程被内存限制干掉了。两个参数要一起看:
; 每个 PHP 脚本能用的内存上限(不是进程池,是单脚本)
memory_limit = 256M
; 单个 FPM 子进程的内存软上限(超过会打日志,硬上限会杀进程)
; 一般不用设,但排查时可用
; pm.process_idle_timeout / rlimit 相关参数常见故障链是:某个插件或图片处理脚本要吃 512M,而 memory_limit 只有 128M,脚本中途被内核杀掉,FPM 返回 502。查证方式是翻 FPM 的错误日志:
grep -iE 'exceeded|killed|OOM' /var/log/php*/fpm.log | tail -20
dmesg -T | grep -i 'killed process'如果 dmesg 里出现 Out of memory: Killed process ... php-fpm,说明不是 PHP 自己限制的,是整机内存真的不够了——这时候要回上文的公式重新算 max_children,而不是继续调大 memory_limit。
压测之外:用一条命令看到池子的实时挤压情况
不想装监控系统的话,这两行组合足够日常盯梢:
# 每 2 秒打印一次 FPM 的核心指标
watch -n 2 'curl -s "http://127.0.0.1/fpm-status" | grep -E "active|idle|listen queue|max children"'
# 同时看进程数与内存
watch -n 2 'echo "进程数: $(pgrep -c -f "pool www")"; free -m | head -3'判断标准很简单:active 长期贴着 max_children、idle 长期为 0、listen queue 大于 0,三点同时成立就是容量不足;若 active 不高但 listen queue 依然堆积,那说明 socket/backlog 或网络层出了问题,不是进程数的事。分清这两种,才能对症下药。
小结
502 排查的顺序应该是:先看 Nginx error log 分清是队列满还是进程崩 → 开 FPM status 页看 active/idle/listen queue/max children reached → 用 PSS 量单进程内存反推 max_children → 用 pm.max_requests 防泄漏 → reload 而非 restart。绝大多数的 PHP-FPM 502 都逃不出这五步。记住核心原则:容量是算出来的,不是猜出来的;而算完之后留着的那点余量,才是防止雪崩的关键。