浪白的博客の小站
首页项目归档照片墙音乐灵境说说杂谈友链关于
封面

PHP-FPM 参数调优,一次线上的经历

写作时间:2026-05-12 21:35:00
# PHP
# 运维
# 性能
# 故障记录

记录一次给 PHP-FPM 调参的过程。背景是个查询比较重的接口,高峰期 502 和超时交替出现。

初始状态

服务器:4 核 8G,PHP 8.1,Nginx 反代。

pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3

这是宝塔面板的默认值,看着就很小。max_children = 5 意味着最多同时处理 5 个请求,第 6 个就得排队。

问了下为什么是 5,回答是"默认的,没改过"。

第一步:算该设多少

pm.max_children 的算法是:

max_children = 可用内存 / 单个进程占用内存

先看单个进程占多少:

ps -ylC php-fpm --sort:rss | awk '{sum+=$8; n++} END {print "平均 " sum/n/1024 " MB, 共 " n " 个进程"}'

结果是平均 45MB。

理论上 8G / 45MB ≈ 180。但不能这么算,因为:

  • MySQL 和 Redis 在一台机器上(或者不在,但网络连接、Nginx、系统本身都要内存)
  • 进程占用是动态的,遇到大请求(比如导出)单个进程能涨到 200MB+
  • 要留至少 20% 余量,不然上 swap 更慢

我当时留了 2G 给其它服务,可用 6G。按平均 45MB 算是 130 个,但考虑到峰值会涨,我定了 60。

这个数字是拍脑袋的吗? 不是完全拍脑袋,但确实没法精确。我的方法是设一个值,然后压测看内存曲线,不够就降。

第二步:pm 模式怎么选

三种模式:

; static:固定进程数,最简单可预测
pm = static
pm.max_children = 60

; dynamic:按负载伸缩
pm = dynamic
pm.max_children = 60
pm.start_servers = 15
pm.min_spare_servers = 10
pm.max_spare_servers = 30

; ondemand:有请求才起进程
pm = ondemand
pm.max_children = 60
pm.process_idle_timeout = 10s

我选了 dynamic,理由:

  • static 内存占用固定,但低峰期浪费(60 个进程常驻 2.7G)
  • ondemand 省内存,但冷启动有延迟,高峰期第一个请求要等进程创建,不适合流量波动快的场景
  • dynamic 折中

dynamic 的参数含义:

  • start_servers:启动时创建多少
  • min_spare_servers:空闲进程少于这个数就扩容
  • max_spare_servers:空闲进程多于这个数就缩容

我设的是 start 15 / min 10 / max 30。经验是 min 和 max 之间不要差太多,不然频繁扩缩容本身有开销。

第三步:关键的几个超时参数

这几个参数配错了会出很诡异的问题:

; FPM 等待请求数据的时间
request_terminate_timeout = 60s

; FPM 内部读 FastCGI 请求的超时
; 注意:这是"读取请求"的超时,不是处理超时

; Nginx 侧
fastcgi_read_timeout 60s;
fastcgi_connect_timeout 10s;
fastcgi_send_timeout 60s;

关系必须理清:

request_terminate_timeout 是 FPM 强制杀掉工作进程的时间。设成 60s,意味着一个请求跑超过 60 秒,那个 php-fpm 进程直接被 kill。

这个参数要略大于 max_execution_time(PHP 脚本自己的超时)。如果反过来,PHP 还没来得及报"超时"就被 FPM 杀了,日志里只有一句 child exited on signal 9,什么线索都没有。

我设的是:

; php.ini
max_execution_time = 55

; php-fpm.conf
request_terminate_timeout = 60s

Nginx 的 fastcgi_read_timeout 要大于 FPM 的,不然 Nginx 先超时返回 504,但 FPM 那边还在跑,白白占着进程。

所以链条是:max_execution_time < request_terminate_timeout < fastcgi_read_timeout。我是 55 < 60 < 65。

第四步:状态页面

这个太有用了,但很多人不开:

pm.status_path = /fpm-status
ping.path = /fpm-ping

Nginx 里只允许内网访问:

location = /fpm-status {
    allow 127.0.0.1;
    allow 10.0.0.0/8;
    deny all;
    include fastcgi_params;
    fastcgi_pass unix:/tmp/php-cgi-81.sock;
}

访问 ?full 能看到每个进程在处理什么请求:

curl 'http://127.0.0.1/fpm-status?full'

输出里能看到:

pool:                 www
process manager:      dynamic
active processes:     42
idle processes:       18
total processes:      60
max active processes: 60      ← 这个到顶了说明不够
max children reached: 137     ← 非 0 说明经常达到上限,请求排队了
slow requests:        3

max children reached 是关键指标。 只要不是 0,就说明并发不够。

我们改完之后这个数字还是每天有几万,说明 60 也不够。但一看 slow requests 只有 3 个,说明不是所有请求都慢。

第五步:找到真正的问题

调完参数之后 502 少了,但慢请求还是有。

用 ?full 状态页看到有几个进程长时间卡在同一个 URL 上——是个报表导出接口。

看了下代码:

// 一次查了 30 万行
$rows = DB::table('orders')->whereBetween('created_at', [$start, $end])->get();

// 然后在 PHP 里循环处理
foreach ($rows as $row) {
    // 每次循环还查一次关联表!
    $user = DB::table('users')->find($row->user_id);
    $out[] = [...];
}

30 万行一次性加载进内存,然后在循环里查 30 万次数据库。这个接口要跑好几分钟。

这才是根因。 FPM 参数调优只是缓解,不是解决。

改法是:

  1. 用 chunkById 分批处理,每批 1000
  2. 关联数据一次性查出来做 map
  3. 改成异步任务,导出完发通知

改完之后这个接口从"卡死 FPM 进程几分钟"变成"200ms 返回任务 ID"。502 彻底消失。

我学到的东西

FPM 参数调优的收益有上限。 它解决的是"并发不够",不解决"单个请求太慢"。如果有一个接口要跑 60 秒,你把 max_children 调到 200 也只是让 200 个进程一起卡住。

先看指标,再调参数。 状态页里的 max children reached、slow requests、listen queue 这三个数字基本能定位问题类型:

  • max children reached 高 → 并发不够,调 max_children 或优化慢请求
  • slow requests 高 → 有慢接口,去查代码
  • listen queue 非 0 → 请求在排队,同上

别一次改太多参数。 我一开始把 max_children 从 5 直接改到 200,结果触发 OOM,MySQL 被 OOM killer 干掉了。改参数要一档一档调,每次观察。

listen 用 socket 还是 TCP? 同机的话 unix socket 略快,而且省掉 TCP 端口。但如果 Nginx 和 FPM 不在同一台机器,只能用 TCP。我们用 socket。

pm.max_requests 要设。 默认是 0(不重启)。PHP 进程跑久了可能有内存碎片或第三方扩展的小泄漏,设成 500~1000 让它定期重启,代价很小:

pm.max_requests = 800

最终的配置

pm = dynamic
pm.max_children = 60
pm.start_servers = 15
pm.min_spare_servers = 10
pm.max_spare_servers = 30
pm.max_requests = 800

request_terminate_timeout = 60s
pm.status_path = /fpm-status
pm.ping_path = /fpm-ping

; 慢日志,查慢请求
request_slowlog_timeout = 5s
slowlog = /var/log/php-fpm-slow.log

request_slowlog_timeout 强烈建议开。 它会把超过指定时间的请求的完整 PHP 调用栈写到日志里,直接告诉你慢在哪一行。这个比任何监控都好用。

配好之后,我看慢日志的第一行就找到了上面那个报表接口。

avatar

浪白

邪恶的浪白

RECOMMENDED

Composer 用了这么多年,几个我认为值得坚持的习惯

2026-04-27 19:15:00

context 传参这件事,我踩过的三个坑

2026-05-06 20:05:00

Go 的 error 处理,用久了反而觉得还行

2026-04-19 22:15:00

Table of Contents