上周四晚上八点多,一个跑 WordPress 的轻量服务器开始零星 502,到九点基本打不开。监控上 CPU 只有 30%,内存 3.6G 顶到 3.9G,swap 一直没开。nginx 的 error.log 里反复是这一行:
upstream timed out (110: Connection timed out) while reading response header from upstream, request: "GET /archives/1247 HTTP/1.1", upstream: "fastcgi://unix:/run/php/php8.1-fpm.sock"
php-fpm 的日志更直接,server reached pm.max_children setting (10), consider raising it。当时也没多想,一个日 PV 两万多的站只有 10 个进程,确实偏少,顺手改成 50,又把 max_requests 设成 500,reload 完看着是正常的。
撑了大概二十分钟。502 没消失,MySQL 先没了。dmesg 里躺着 Out of memory: Killed process 1187 (mysqld) total-vm:3128544kB。php-fpm 每个进程实测常驻 50 到 60MB,50 个就是 2.7G 上下,再叠上 MySQL 的 800M 和系统本身,4G 内存根本装不下。这一步算是自己给自己挖的坑。
真正的毛病不在进程数。翻 slowlog,一条请求要 8 秒才结束,堆栈停在某个统计插件里,它每次页面渲染都去请求一个外部接口,没设超时,对端一慢就跟着慢。进程占着不放,后面排队的请求只能干等,等到 nginx 放弃就变成 502。
改法落在两处。先给 FastCGI 加个硬上限,别让请求无限挂住:
; /etc/php/8.1/fpm/pool.d/www.conf
request_terminate_timeout = 30s
pm = dynamic
pm.max_children = 12
pm.start_servers = 3
pm.min_spare_servers = 2
pm.max_spare_servers = 6
再处理那个外部调用。插件本身不好动,就在它请求的位置套一层 transient,成功的结果缓存十分钟,失败也缓存一分钟,免得对端挂了以后每个请求都去撞一次。opcache 也打开,PHP 这边能省一点是一点。
做完已是十一点半,内存稳在 2.9G,八点到十一点那三个小时的 502 从每小时几百条掉到零。max_children 最后停在 12 不是随手写的,按 60MB 一个进程算,留 1G 给 MySQL 和系统,剩下才是 PHP 能动的额度。
取舍还得说清楚。这台机器就 4G,调参数撑不住流量再涨。想省事就升到 8G,想省钱就得把慢请求一条条揪出来。只把 pm.max_children 往上加,看着进程数够了,实质是拿内存换时间,等 MySQL 被 OOM 杀掉,查起来比 502 麻烦得多。