间歇性 502 里最磨人的一种:低峰期一切正常,流量一上来就崩,过会儿又自己好了。FPM 日志里翻出 server reached pm.max_children,基本就能确诊——子进程开到上限,新请求没进程可用了。
很多教程看到这条日志就让你把 pm.max_children 调大,却不说调到多少。拍脑袋填个 100,机器 2G 内存,结果就是 502 治好了,OOM 上门了——内存不够,内核开始杀进程,这次死的可能是 MySQL。
正确的算法是拿内存反推。先量出一个 PHP 子进程实际占多少内存,再用可用内存除一下。所谓可用内存,是总内存减去系统开销、减去 Nginx、减去 MySQL 这类常驻服务,剩下的才轮得到 PHP 分。
量单进程内存有个讲究:不能直接看 ps 里的 RSS 平均值,因为刚启动的进程占用小,跑过一轮请求的进程占用大。要取跑过请求之后的稳态值,取所有进程的 RSS 平均值作为参考,再留两成余量。
<pre style="background-color:#282c34;color:#abb2bf;padding:16px 20px;border-radius:8px;overflow-x:auto;font-family:Consolas,'Courier New',monospace;font-size:14px;line-height:1.7;margin:16px 0;-webkit-overflow-scrolling:touch;"><code style="font-family:inherit;"><span style="color:#7f848e;font-style:italic">#!/bin/bash</span>
<span style="color:#7f848e;font-style:italic"># pm.max_children 内存反推计算</span>
<span style="color:#7f848e;font-style:italic"># 用法:bash calc_max_children.sh</span>
<span style="color:#7f848e;font-style:italic"># 参数:可用给 PHP 的内存(MB),不填则按当前空闲内存的 60% 估算</span>
RESERVE_MB=${<span style="color:#d19a66">1</span>:-<span style="color:#d19a66">0</span>}
<span style="color:#7f848e;font-style:italic"># 1. 取所有 php-fpm 子进程的 RSS,算平均值(KB)</span>
AVG_KB=$(ps -o rss= -C php-fpm <span style="color:#d19a66">2</span>>/dev/<span style="color:#56b6c2">null</span> | awk <span style="color:#98c379">"{s+=\$1; n++} END {if(n>0) printf \"%d\", s/n; else print 0}"</span>)
<span style="color:#c678dd">if</span> [ <span style="color:#98c379">"<span style="color:#e06c75">$AVG_KB</span>"</span> -eq <span style="color:#d19a66">0</span> ]; then
<span style="color:#c678dd">echo</span> <span style="color:#98c379">"没取到 php-fpm 进程,确认服务是否运行"</span>; <span style="color:#c678dd">exit</span> <span style="color:#d19a66">1</span>
fi
AVG_MB=$((AVG_KB / <span style="color:#d19a66">1024</span>))
<span style="color:#7f848e;font-style:italic"># 2. 可用内存:没指定就按空闲内存的 60% 算(保守留余量)</span>
<span style="color:#c678dd">if</span> [ <span style="color:#98c379">"<span style="color:#e06c75">$RESERVE_MB</span>"</span> -eq <span style="color:#d19a66">0</span> ]; then
FREE_MB=$(free -m | awk <span style="color:#98c379">"/^Mem:/ {print \$7}"</span>)
RESERVE_MB=$((FREE_MB * <span style="color:#d19a66">60</span> / <span style="color:#d19a66">100</span>))
fi
<span style="color:#7f848e;font-style:italic"># 3. 反推进程数,再打八折留安全边界</span>
RAW=$((RESERVE_MB / AVG_MB))
SAFE=$((RAW * <span style="color:#d19a66">80</span> / <span style="color:#d19a66">100</span>))
<span style="color:#c678dd">echo</span> <span style="color:#98c379">"单进程平均内存 : ${AVG_MB} MB"</span>
<span style="color:#c678dd">echo</span> <span style="color:#98c379">"可分配给PHP : ${RESERVE_MB} MB"</span>
<span style="color:#c678dd">echo</span> <span style="color:#98c379">"理论上限 : ${RAW}"</span>
<span style="color:#c678dd">echo</span> <span style="color:#98c379">"建议 max_children: ${SAFE}(已留20%余量)"</span>
<span style="color:#c678dd">echo</span> <span style="color:#98c379">""</span>
<span style="color:#c678dd">echo</span> <span style="color:#98c379">"配套的 dynamic 配置建议:"</span>
<span style="color:#c678dd">echo</span> <span style="color:#98c379">"pm.max_children = ${SAFE}"</span>
<span style="color:#c678dd">echo</span> <span style="color:#98c379">"pm.start_servers = $((SAFE / 4))"</span>
<span style="color:#c678dd">echo</span> <span style="color:#98c379">"pm.min_spare_servers = $((SAFE / 8))"</span>
<span style="color:#c678dd">echo</span> <span style="color:#98c379">"pm.max_spare_servers = $((SAFE / 3))"</span>
<span style="color:#c678dd">echo</span> <span style="color:#98c379">"pm.max_requests = 500"</span></code></pre>
pm.max_requests 这行别漏。它规定每个子进程处理多少个请求后就自动重启,用来兜底 PHP 代码里的内存泄漏——跑久了进程会越来越胖,漏得慢的时候你可能一两个月都发现不了,等发现时已经吃掉了大半内存。设成 500 左右,进程会被温和地轮换掉,代价几乎可以忽略。
三种进程管理模式的取舍也得说清。static 是一开始就把进程全开好,响应最快但内存常驻占用高;dynamic 按需增减,适合大多数站点;ondemand 用到了才 fork,最省内存但高并发时 fork 本身的开销会拖慢响应。小内存机器上跑访问量不大的站,ondemand 是合理选择;有稳定流量的站用 dynamic 更稳。
改完怎么验?别只看页面能打开。用压测工具按你算出的进程数乘以 1.5 的并发量打一轮,看 FPM 日志里还会不会冒出 max_children,同时盯着内存别让它逼近上限。两项都过关,这个配置才算站得住。
申请创业报道,分享创业好点子。点击此处,共同探讨创业新机遇!
