当前位置: 首页 > news >正文

深入理解 PHP-FPM 的最佳配置

深入理解 PHP-FPM 的最佳配置

对大多数开发者来说,PHP-FPM 的配置并不是日常工作中需要深入研究的东西。这没什么问题,毕竟不是每个人都想或需要在服务器调优上花时间。

况且,现在有很多托管服务(宝塔, 1panel等)可以帮你把服务器配置好,安装所有依赖(包括 PHP-FPM),你只需要在控制面板点几下就能部署代码。也许你们公司有专门的运维,或者有资深开发在负责这块。即便真要自己配置 PHP-FPM,多半也就是翻几篇文章,改改参数,或者直接用默认配置。这很正常——谁有那么多时间去钻研每个服务器配置细节,尤其这只是工作的一小部分。

但随着应用不断迭代、用户越来越多,你可能会发现服务器开始变慢,请求处理时间越来越长,内存占用接近上限,甚至服务器直接挂掉。

最近我的一台服务器就遇到了类似问题,所以我决定花点时间搞清楚 PHP-FPM 到底是怎么工作的,不同配置会带来什么影响。我看了很多文章、讨论和评论,然后自己做了些测试来验证。以下是我的一些心得。

原文 深入理解 PHP-FPM 的最佳配置

问题排查

如果问题确实出在 PHP-FPM 上,有几个排查方向。首先检查 PHP-FPM 的日志,重点关注 max children 相关的警告。PHP-FPM 的主进程会按需生成子进程,直到达到 max children 的上限。每个子进程一次只能处理一个请求(比如对你应用的一次访问)。所以如果 max_children 设置成 5,而同时有 10 个用户在访问应用,日志里很可能会出现这样的警告:

WARNING: [pool www] server reached pm.max_children setting (5), consider raising it

这会导致部分请求被延迟,直到有子进程空闲出来。可以用下面的命令检查日志里有没有这类警告。如果你用的是 PHP-FPM 8.2:

sudo grep max_children /var/log/php8.2-fpm.log.1 /var/log/php8.2-fpm.log

注意你系统上的日志路径可能不同,记得先确认。另外,除了替换 PHP 版本号,有些系统的日志文件名里不带版本号,那就直接用 php-fpm:

sudo grep max_children /var/log/php-fpm.log.1 /var/log/php-fpm.log

后面提到的所有 php-fpm 命令都是同样的道理。我会用 php-fpm8.2 或 php8.2-fpm,因为这是我的版本,你的可能是 php-fpm7.4(php7.4-fpm)或者直接就是 php-fpm。

不想打开配置文件的话,可以用这个命令快速查看当前配置:

sudo php-fpm8.2 -tt

这样就能找到 pm.max_children 这一行,确认 max_children 是不是真的设置成了 5:

[19-Mar-2024 22:48:10] NOTICE:  pm.max_children = 5

另外要关注的是服务器内存使用情况。用 htop 按内存排序,可以看到内存是不是快用完了,PHP-FPM 进程占了多少。

这可能是 max_children 设置太高,生成的子进程太多,服务器内存撑不住了。或者,如果重启 PHP-FPM 后内存使用量下降,然后又慢慢涨回去,那多半是代码有内存泄漏。理想情况下当然要找到泄漏点并修复,但定位内存泄漏有时候挺难的,尤其在大项目里,而且泄漏可能来自某个必需的第三方库。

第一个问题可以通过优化配置解决,内存泄漏的话 PHP-FPM 这边也有些缓解办法,稍后会讲。

顺便说一下,重启 PHP-FPM 的命令可能是这样的(但你的情况可能不同,所以要确认一下):

sudo service php8.2-fpm restart

如前面说的,重启 PHP-FPM 能临时缓解内存泄漏问题(但不是根本解决),给你争取时间去修复泄漏或调整配置。

配置进程管理器

现在可以开始修改 PHP-FPM 的配置文件了,看看怎么针对实际情况做优化。编辑主配置文件的命令:

sudo nano /etc/php/8.2/fpm/pool.d/www.conf

里面有各种配置项,我们只讲几个对性能影响最大的。首先要决定进程管理器如何控制子进程数量。有 3 个选项:static、dynamic 和 ondemand。大多数情况下默认是 dynamic。这几个选项有什么区别?假设你确定服务器最多需要 10 个子进程:

static 会始终保持所有 10 个进程运行,理论上最快,因为进程都已经在那儿了,负载上来时不需要 fork 新进程。但代价是即使没人访问网站,这 10 个进程也会一直占着内存。

dynamic 可以灵活调整;比如一开始启动 3 个进程,负载上来时 fork 到最多 10 个,负载下去后减少到 6 个等待连接。这个选项在内存占用和响应速度之间找平衡,至少理论上如此。

最后是 ondemand,一开始不生成任何子进程,负载上来时最多创建 10 个,负载下去后可能又回到 0 个。这个选项(理论上)适合流量不大的中小型应用、预发布环境,或者多租户共享服务器。由于子进程一直在回收,可以帮助控制内存泄漏,因为进程在内存累积之前就被干掉了。缺点是需要频繁 fork 新进程,可能影响性能和响应速度。

设置这些选项之前,需要算出 PHP-FPM 进程的最大负载。也就是确定服务器能跑多少个子进程,然后设置 max_children 值。怎么算?这有点麻烦,因为理想情况下要知道单个子进程平均用多少内存。问题是多个进程通常会共享一些内存,所以很难精确算出单个进程的实际内存使用量。

网上有很多脚本和文章教你怎么算 PHP-FPM 进程的平均内存消耗,但大多数我试下来感觉不太对,算出来的值比预期高很多。

有个 Python 脚本在好几篇文章里都提到了,看起来比较靠谱。可以用这个命令算出每个程序的总内存使用量:

cd ~ &&
wget https://raw.githubusercontent.com/pixelb/ps_mem/master/ps_mem.py &&
chmod a+x ps_mem.py &&
sudo python3 ps_mem.py

注意我用的是 python3,你的系统可能只需要 python。跑完脚本后,可能会看到类似这样的结果:

2.1 GiB + 127.5 MiB =   2.2 GiB       php-fpm8.2 (31)

这说明 31 个 PHP-FPM 进程用了 2.2 GB 内存,平均每个进程约 73 MB。另一个有用的命令可以查看空闲和活动进程数:

sudo service php8.2-fpm status -l

看这一行就能知道子进程的当前状态:

Status: "Processes active: 0, idle: 30, Requests: 56116, slow: 0, Traffic: 0req/sec"

这里没有活动进程,30 个空闲进程,加上 1 个主进程,总共 31 个,和 Python 脚本报的一致。

Python 脚本也会报总内存使用量。可以随时跑 htop 或 free -hl 检查服务器当前内存使用情况,看看这些数字是否合理、是否对得上。

还有一点,如果真有内存泄漏,单个进程的内存可能会涨到 php.ini 里定义的 memory_limit,默认一般是 128 MB。所以保守起见,可以直接用这个值作为单个进程的平均值。

好,现在可以算 max_children 值了。假设有台 8 GB 内存的服务器,其他程序用了 2 GB,剩 6 GB。再留 1 GB 作为缓冲,防止意外情况或者未来应用增长、新增进程什么的。这样就剩 5 GB 给 PHP-FPM。前面算出单个进程用约 73 MB 内存,用 5 GB 除以 73 MB 就得到 max_children 值:

5120 (MB) / 73 (MB) = 70.14

所以这台服务器的 max_children 应该设置成 70。这样无论选哪个进程管理器(pm)选项,PHP-FPM 最多都只会生成 70 个子进程。

如果用 pm = static,就不需要设置其他选项了。70 个子进程会立即生成,随时待命。但要记住代价:这些进程会一直占着 5 GB 内存。

如果用 pm = ondemand,只需要考虑一个额外设置:pm.process_idle_timeout。由于 ondemand 模式下子进程会不断生成和终止,这个设置告诉 PHP-FPM 什么时候干掉空闲的子进程。默认是 10 秒,需要的话可以改,不过默认值已经挺合理了。

如果用 pm = dynamic,需要考虑几个额外设置。pm.start_servers 是启动或重启 PHP-FPM 时立即生成的子进程数。pm.min_spare_servers 设置最小空闲子进程数。pm.max_spare_servers 决定最大空闲子进程数。

假设我们这样设置:

pm = dynamic
pm.max_children = 70
pm.start_servers = 20
pm.min_spare_servers = 20
pm.max_spare_servers = 40

实际运行是这样的。上面例子里 start_servers 设置为 20,PHP-FPM 一启动就会生成 20 个子进程,占用相应的内存。有请求过来时,这 20 个进程中的部分或全部会变成活动状态处理请求。没流量时,这 20 个进程会空闲等待;它们不会被终止,继续占着内存。

把 min_spare_servers 设置得比 start_servers 低(比如 15)没什么意义,因为 20 个子进程会立即生成,即使空闲,主进程也不会为了达到 15 的最小值而终止 5 个。而且 min_spare_servers 不能比 start_servers 大,所以最好就把 min_spare_servers 设置成和 start_servers 一样。

如果大量请求涌入,20 个子进程不够用,主进程会生成额外的子进程,最多到 max_children 值,这里是 70。假设为了应对流量激增生成了 70 个子进程。过一会儿,流量恢复正常,不再需要 70 个进程了,大部分或全部进程变成空闲。这时主进程会终止空闲子进程,直到 max_spare_servers 值,这里是 40。然后你就剩 40 个空闲进程,它们不会被进一步终止,继续占着内存。

所以设置这些值时要记住:如果需要生成比 start_servers 更多的进程,流量高峰过后你会剩下那么多子进程(最多到 max_spare_servers 值)在运行。比如需要生成 30 个子进程,你会剩 30 个在运行;需要生成 50 个,过一会儿会剩 40 个(因为 max_spare_servers)在运行。所以如果不想最终可能有 40 个子进程在后台跑着,可以考虑降低这个值,甚至让它和 start_servers 一样。这些情况会一直保持到你重启 PHP-FPM。重启后会根据 start_servers 重新生成 20 个子进程。

很多文章里有个公式,建议根据 CPU 核心数来设置 start_servers、min_spare_servers 和 max_spare_servers 以获得最佳性能:

pm.start_servers = CPU 核心数 x 4
pm.min_spare_servers = CPU 核心数 x 2
pm.max_spare_servers = CPU 核心数 x 4

这个公式据说是基于单个 CPU 核心能并发处理多少进程的某种假设。我不确定是谁开始这么搞的,这些乘数怎么推导出来的,但有一点让我觉得不对劲——把 min_spare_servers 设得比 start_servers 低,如我上面解释的,会导致 min_spare_servers 值永远用不上。而且在我的测试中(稍后会讲),用这个方法并没看到明显的性能提升。所以我觉得这个公式不该盲目照搬,要根据实际情况调整。

关于 max_children 和 dynamic 相关设置,最好的建议就是边监控边调优。每种情况都不一样——你的资源、负载、整体策略都不同。按照上面的指导原则,从合理的值开始,随着应用增长和变化,不断调整配置。

这一节还有个值得注意的选项:pm.max_requests。如果真有内存泄漏,这个设置可以让子进程在处理一定数量请求后被回收。默认是 0,意味着进程不会因为这个选项被终止。合理的值可能是 500 或 1000 个请求,取决于你的场景。比如设置成 500,子进程处理了 500 个请求后会被终止(释放累积的内存),然后重新生成。

实际测试

我决定做几个性能测试来验证理论:static 处理请求应该最快,因为不需要临时 fork 子进程;dynamic 应该居中,因为部分进程已经在跑,部分需要按需 fork;ondemand 应该最慢,因为它不停地生成和终止进程。结果如下。

我用 ApacheBench 做测试,按照建议从另一台服务器发送请求,不是从被测服务器本身发的。被测服务器有 16 GB 内存和 4 个 CPU 核心,PHP-FPM 配合 NGINX,请求走 Laravel 应用。所有测试用例的 max_children 都是 80。我比较的是 90% 请求的响应时间。下面表格里只列出不同 pm 选项之间的毫秒差异,0ms 是最快的。

测试命令示例:

ab -n 1000 -c 10 https://example.com

这个例子会发送 1000 个请求,并发级别是每次 10 个。

先看第一个测试。我想看看明显达到 max_children 限制时,不同 pm 选项如何影响响应时间。所以发了 25000 个请求,并发级别 1000。

用 dynamic 时的额外值如下(后面简称 20/20/40):

pm.start_servers = 20
pm.min_spare_servers = 20
pm.max_spare_servers = 40

结果如下:

Static Dynamic On demand
+1223ms +845ms 0ms

结果显示,理论上应该最慢的 ondemand 实际上最快,而 static 出人意料地最慢,比 ondemand 慢了一秒多。

第二个测试发了 10000 个请求,并发级别 100。测试了两种 dynamic 设置,一种是第一个测试的(20/20/40),另一种用基于 CPU 核心的公式(16/8/16):

Static Dynamic (20/20/40) Dynamic (16/8/16) On demand
0ms +14ms +2ms +24ms

这次 static 最快,基于公式的 dynamic 紧随其后,ondemand 最慢,符合理论预期。但总的来说,对大多数网站而言,+24ms 算不上什么性能提升,不同选项之间差异不大。

最后一个测试只发了 2000 个请求,并发级别 16。同样用了两种 dynamic 设置(20/20/40 和 16/8/16):

Static Dynamic (20/20/40) Dynamic (16/8/16) On demand
+2ms +7ms +10ms 0ms

这个规模下 ondemand 再次获胜,static 紧随其后,基于公式的 dynamic 垫底。这次差异更小了,第一名和最后一名之间只差 10ms。

最终得到了一些意外结果。测试表明,当并发请求数量远超 max_children 值,或者远低于 max_children 值时,ondemand 是最佳选择。当并发请求数量接近 max_children 值时,static 最佳。但要注意,第二个测试,特别是第三个测试中,响应时间差异相当小。而且如果重新跑这些测试,排名很可能会变。

所以这些结果不能当成铁律,理论也是如此。我觉得在现代服务器上,fork 一个新子进程已经不是什么昂贵操作了,不会明显影响响应时间,至少在测试的规模上不会。这就是为什么 ondemand 不该被轻易否定,即使在处理请求速度方面也是如此。

最好的前进方式是做你自己的测试,因为你的负载、设置和每个请求执行的操作可能完全不同,然后根据这些测试,应用看起来最高效的设置。

其他设置

还有几个额外的设置我们应该了解一下,在 PHP-FPM 出问题或者你需要追踪慢请求时可能会很有用。

要启用 slowlog(当然是用来记录慢请求的),我们需要编辑之前的同一个配置文件:

sudo nano /etc/php/8.2/fpm/pool.d/www.conf

然后找到 slowlog 部分:

slowlog = /var/log/php8.2-fpm.log.slow

取消注释。那里还有几个相关选项你应该考虑取消注释。第一个是 request_slowlog_timeout,默认设置为 5 秒。如果你只想记录耗时 3 秒及以上的请求,应该取消注释并修改这个值。第二个是 request_slowlog_trace_depth,默认设置为 20。在 Laravel 应用中,这个值可能太低,无法遍历所有 vendor 函数并到达实际被调用的代码,比如你的 controller。所以我认为大多数情况下,50 应该没问题,但要确认一下是否适合你。

最终整个 slowlog 设置可能是这样的:

slowlog = /var/log/php8.2-fpm.log.slow
request_slowlog_timeout = 3s
request_slowlog_trace_depth = 50

最后,还有另一个配置文件我们可以编辑,控制当子进程因为某种原因开始失败时会发生什么。下面是编辑这个文件的示例:

sudo nano /etc/php/8.2/fpm/php-fpm.conf

在那个文件中,我们关注 3 个相互关联的选项,它们默认都设置为 0 并被注释掉。如果你打算使用它们,确保先取消注释。之后,你可以把它们设置为这些值:

emergency_restart_threshold = 10
emergency_restart_interval = 1m
process_control_timeout = 10s

使用的值是你可能在其他一些文章中也会看到的。前两个设置告诉 PHP-FPM,如果在一分钟内有 10 个子进程失败,PHP-FPM 应该自动重启。第三个设置意味着子进程在响应主进程发送的信号之前会等待 10 秒。所以如果主进程向子进程发送 KILL 信号,它会有 10 秒时间完成任务后再退出。当然,你可以根据需要调整这些值。

在失败时重启 PHP-FPM 可能会解决一些问题,但如果问题与即使 PHP-FPM 重启后仍会重现的东西有关,它会一直重启,直到你弄清楚到底发生了什么。所以你应该自己决定,当发生意外情况时,是想让 PHP-FPM 完全失败,还是让它自动重启。

总结

本人讨论了 max_children 选项的最佳值。探讨了不同进程管理器设置的优势和缺陷,以及如何测试它们。还了解了一些在调试慢请求或处理失败时可能有用的额外设置。希望这篇文章对你有帮助,你能够用文章中的信息作为起点,更好地监控和调优你服务器上的 PHP-FPM。

http://www.hskmm.com/?act=detail&tid=32694

相关文章:

  • 【GitHub每日速递 251017】95k star,程序员专属!超全做饭指南,涵盖千道美食做法与进阶秘籍
  • 洛谷 P6715 [CCO 2018] Fun Palace (神秘DP)
  • AT 随机做题 I
  • moni 32
  • git 舍弃当前所有修改
  • 2025.10.17——1蓝
  • C# 使用 using 关键字间接实现只读局部变量的方法
  • 画图3D真好用 - Gon
  • 高校与某中心共建机器人技术教育项目
  • 2025年国际物流服务领域优质品牌最新推荐排行榜 —— 聚焦行业头部企业核心优势与选择参考
  • WordPress维护模式完整指南:手动实现与插件方案
  • Lean语言如何连接数学与编程
  • Github上文本切分相关的优秀项目
  • 微信机器人开发
  • 微信社群机器人开发
  • 《程序员修炼之道:从小工到专家》第三章读后感
  • 原型链污染学习
  • 重新认识 Golang 中的 json 编解码
  • (二)CUDA在Windows系统上的编译运行方法
  • 关于价值原语与AI元人文构想的对话全记录——DeepSeek研究
  • 关于价值原语与AI元人文构想的对话全记录
  • 升鲜宝生鲜配送供应链管理系统,辅助开发工具,《多语言自动翻译与导出工具(WinForms版)》开发文档 及 阿里云机器翻译,数据库Mysql .net 全部源代码
  • MySQL学习
  • 植物大战僵尸全系列下载 PVZ植物大战僵尸全集版分享下载 原版民间修改版含安卓手机+电脑+ios各平台
  • 10.17
  • Pytorch66页实验题
  • Excel学习
  • 记一次激活Jetbrains全家桶流程
  • uni-app x开发商城系统,商品列表
  • PySimpleGUI 中有没有类似VB的timer组件