77.4%的网站都在用,但TIOBE排名却跌到14?PHP-FPM配置参数一篇讲透
TIOBE地址:https://www.tiobe.com/tiobe-index/
干了这么多年运维,PHP这门语言我估计是绕不开的。你现在随便打开一个网站,大概率后端就跑着PHP。可你要是去TIOBE排行榜看一眼,会发现这哥们已经从当年的第一名跌到第14名了。这到底是PHP真的凉透了,还是说它只是换了一种方式"存在"?今天咱们就一起把这事掰扯清楚。
我今天想聊三件事:一个是全世界到底有多少项目在用PHP、PHP到底过没过时;再一个是PHP-FPM配置文件的那些核心参数到底是什么意思、应该怎么调。这两块内容网上资料很多,但讲得都比较零散,特别是PHP-FPM配置这块,很多人都是照着网上的模板抄一遍,根本不知道每个参数到底干嘛的。我争取这次给大家讲明白。
全世界有多少项目在用PHP?
先说几个数字镇场子。
根据W3Techs的统计,截至2023年7月,全世界77.4%的网站使用PHP。这个比例说实话相当恐怖,意思是每4个网站里有3个跑的都是PHP。这还没完,更早的2013年4月数据显示,PHP已经被安装在超过2.44亿个网站和210万台服务器上。这么多年过去了,这个数字只会更大不会更小。
再来看看Composer包管理器的数据。截至2023年7月,每个月Composer包安装次数在2000万左右,Composer包的总量在37.5万个,不同版本加起来400万个。这生态规模,说它"凉了"我是不信的。
再看版本分布情况。2024年有86%的PHP开发者已经在使用PHP 8,但截至2025年6月,PHP 8.3占了34.0%的份额,8.2占24.8%,8.4从2025年1月首次出现的5.1%涨到了6月份的13.7%。可以看出来大家都在慢慢往新版本迁移。
那为什么TIOBE排行榜上PHP却跌到第13名了呢?我觉得这事儿得这么看。TIOBE反映的是"热度"和"讨论量",而W3Techs反映的是"实际部署量"。两者统计维度完全不一样。PHP这两年讨论度确实不高,但谁让它是"闷声发大财"的代表呢。WordPress、Laravel、Symfony这些老牌项目依然在生产环境稳稳跑着。Slack和Tumblr这种大型平台的后端核心也都在用PHP,人家一个月150亿次页面浏览量可不是闹着玩的。
所以结论是:PHP没过时,只是没以前那么"高调"了。
PHP过时的讨论
说到这个,我得引用一下JetBrains 2025开发者生态报告里的观点:PHP、Ruby和Objective-C正处于长期下滑趋势,而Rust、Go和Kotlin则继续保持缓慢且稳定的上升态势。
下滑归下滑,但PHP的基数摆在那儿。我打个比方,这就好比一个曾经年级第一的学霸,现在成绩掉到了中游。你说他"凉了"吗?并没有,只是周围的人追上来了而已。而且PHP 8.0引入JIT之后,性能提升是实打实的,PHP 8.4又搞了属性钩子、非对称可见性这些新特性,PHP语言本身并没有停止进化。
还有一点很重要,PHP基金会的存在。2021年11月JetBrains、Automattic、Laravel、Acquia等公司一起成立了PHP基金会,专职维护PHP语言的发展。这说明大厂们依然愿意为PHP的长期发展买单。
所以我个人观点是:PHP在企业级Web开发、内容管理、电商平台这些领域依然非常能打;但在机器学习、实时高并发系统这些新兴领域,PHP确实不是最优选。工具是用来解决问题的,PHP能解决它擅长领域的问题,那就没"过时"这一说。
PHP-FPM是什么?为什么要优化它?
铺垫完了,接下来才是重头戏。
PHP-FPM全称是PHP FastCGI Process Manager,说人话就是PHP的进程管理器。它在PHP 5.3.3之后被内置进了PHP源码,负责接收来自Nginx或Apache的请求,然后丢给PHP解释器去执行,最后把结果返回回去。
为什么要优化它?很简单。一台服务器的资源是有限的,PHP-FPM如果配置得不好,要么就是浪费资源(进程开太多),要么就是响应慢甚至502(进程不够用)。这两个极端我都见过不少次。
PHP-FPM的配置文件主要有三个层级:
php-fpm.conf:全局配置www.conf(或其他pool文件):进程池配置php.ini:PHP自身的配置(这个影响FPM里跑的所有PHP脚本)
接下来我把这几个文件里最关键的参数一个个掰开讲。
php-fpm.conf全局配置
先说全局配置。这一块主要是控制FPM主进程和日志的行为。
pid —— 用来指定PID文件的位置,默认是空的。一般会写/run/php-fpm/php-fpm.pid这种路径,方便后面用systemd管理服务时读取进程号。
error_log —— FPM主进程的错误日志路径,默认是/usr/local/php/var/log/php-fpm.log。注意,子进程的错误日志一般通过php.ini里的error_log配置,别搞混了。
log_level —— 日志级别,有alert、error、warning、notice、debug五种。生产环境一般用notice就够了,如果排查问题可以临时调到debug或warning。
emergency_restart_threshold 和 emergency_restart_interval —— 这俩是搭配使用的。意思是如果FPM在emergency_restart_interval时间内,子进程收到超过emergency_restart_threshold次数的SIGSEGV或SIGBUS信号(说人话就是子进程频繁崩溃),FPM主进程就会自动重启。这是个保命参数,建议打开,比如emergency_restart_threshold = 10,emergency_restart_interval = 1m。
process.max —— FPM能fork出来的最大进程数。用动态模式(后面会讲pm = dynamic)的时候,这个值是全局限制,也就是所有pool加起来不能超过这个数。用static模式的时候这个值是不生效的,所以一般保持默认的0就行。
daemonize —— 是不是以守护进程方式跑。生产环境一般是yes,但如果用systemd管理的话建议设成no,这样systemd能直接管理主进程,出问题排查也方便。
rlimit_files —— 主进程能打开的文件描述符数量限制。生产环境这个值可以调大一点,比如65535,避免出现"too many open files"的问题。
进程池配置(pool配置)——这部分是重点
这一块是性能调优的核心,生产环境80%的优化都集中在这里。
pm 进程管理模式
这个参数是灵魂,一共三个选项:
static:固定子进程数,进程数量从头到尾都不变。适合流量稳定的场景,比如内部系统、API服务。dynamic:动态调整子进程数,根据流量自动扩缩容。适合流量有波峰波谷的场景,这是生产环境最常用的模式。ondemand:按需启动,平时没有请求时子进程会被回收掉,请求来了再启动。适合流量很小或者调试用的场景。
我的建议是:大多数生产环境用dynamic就对了。
pm.max_children 最大子进程数
这是最重要的参数。它定义了这个pool最多能开多少个PHP-FPM子进程。每个子进程大约占用20-40MB内存(看你的PHP代码和加载的扩展),所以这个值的计算公式是:
pm.max_children = 可用内存 * 0.7 / 单个进程平均占用内存举个例子,服务器有8GB内存,扣除系统、数据库、Redis等其他进程开销后剩4GB给PHP-FPM,单个进程按30MB算,那就是4 * 1024 / 30 ≈ 136。所以这个值一般设个100左右比较稳妥。别设太大,否则内存爆了直接OOM。
pm.start_servers 启动时的子进程数
只在dynamic模式下生效。意思是FPM启动的时候先创建多少个子进程。默认是(min_spare_servers + max_spare_servers) / 2。
一般建议根据平峰流量来设置,避免流量一来还要临时fork进程导致响应慢。
pm.min_spare_servers 和 pm.max_spare_servers 空闲进程上下限
也是只在dynamic模式下生效。
min_spare_servers:最小空闲进程数,如果空闲进程低于这个值,FPM会fork新进程。max_spare_servers:最大空闲进程数,如果空闲进程超过这个值,多余的进程会被回收。
举个实际例子:min设10,max设30。意思就是空闲进程最少留10个,最多留30个,超出的杀掉。
调优经验:min值要保证流量来的时候有足够"预备队",max值不能太大,否则浪费内存。常见的组合是min=20、max=50、max_children=100这种。
pm.max_requests 每个子进程最大请求数
这个参数极其重要但很多新手会忽略。它定义了一个子进程在处理多少个请求后会被自动重启。
为什么要重启?因为PHP进程在处理请求的过程中,内存可能会泄漏(特别是用了一些老旧扩展或框架)。如果不定期重启,进程占用的内存会越来越大,最终拖垮服务器。
一般建议设成500-2000之间,具体看你应用的稳定性。如果应用代码质量好,可以设大点;代码一般的话就设小点保平安。设成0是禁用这个功能,生产环境千万别这么干。
request_terminate_timeout 请求超时时间
单个请求的最长执行时间,单位默认是秒。这个和php.ini里的max_execution_time有点像,但这个更狠——它会直接杀掉进程。
一般设个30s或60s就行。如果超过这个时间请求还没完成,就直接干掉,释放进程资源给别的请求用。生产环境强烈建议打开,不然一个慢请求就能把进程卡住。
request_slowlog_timeout 和 slowlog 慢请求日志
这俩参数也是一对。
如果一个请求的执行时间超过了request_slowlog_timeout设置的时间,FPM就会把这个请求的完整调用栈记录到slowlog指定的文件里。
这是个排查性能问题的神器。比如你设成request_slowlog_timeout = 5s,slowlog = /var/log/php-fpm/www-slow.log。然后定期去翻这个慢日志文件,就能找到哪些接口、哪些代码执行得慢。
listen 监听地址
这个参数定义FPM监听在哪里接收请求。三种写法:
- TCP:
listen = 127.0.0.1:9000 - Unix Socket:
listen = /run/php-fpm/www.sock
同机部署Nginx+PHP-FPM的话,强烈建议用Unix Socket,比TCP性能高一些,因为少了TCP协议栈的开销。
listen.backlog 监听队列长度
这个参数是listen(2)的backlog值,控制未完成连接队列的最大长度。
默认值在Linux上是511。这个值的意思是:当FPM进程都在处理请求时,新来的连接请求会排队等。队列长度超过这个值,新连接就会被拒绝。
如果你的Nginx配置用的是TCP方式连接FPM(比如fastcgi_pass 127.0.0.1:9000),这个值一定不能太小。否则高并发的时候客户端会报"connect to upstream: Connection refused"或者502错误。
生产环境一般设到511或更高。同时要检查系统的somaxconn参数:sysctl -w net.core.somaxconn=4096,让内核也允许更大的队列。
6.10 access.log 和 access.format 访问日志
这两个参数用来开启FPM的访问日志。默认是关闭的。
access.log = /var/log/php-fpm/www-access.log 指定日志文件路径。
access.format 控制日志格式,默认是"%R - %u %t \"%m %r\" %s"。可以按需修改,比如加上响应时间%d、内存使用%M这些。
注意:开启访问日志会有一定的IO开销,对性能有要求的环境慎开。
php_admin_value 和 php_value —— 配置覆盖
这个是PHP-FPM的"特殊功能",可以在pool配置文件里覆盖php.ini里的设置。
php_admin_value[memory_limit] = 256M
php_flag[display_errors] = offphp_admin_value 和 php_value 的区别是:
php_admin_value设置的值,PHP代码里用ini_set()改不了(安全)php_value设置的值,PHP代码里可以用ini_set()改(不推荐用于生产)
生产环境建议用php_admin_value,避免被代码逻辑改回不安全的值。
配置示例
最后给一套生产环境推荐配置,大家可以参考:
[www]
user = www
group = www
listen = /run/php-fpm/www.sock
listen.backlog = 511
listen.owner = www
listen.group = www
listen.mode = 0660
pm = dynamic
pm.max_children = 100
pm.start_servers = 20
pm.min_spare_servers = 10
pm.max_spare_servers = 30
pm.max_requests = 1000
request_terminate_timeout = 60s
request_slowlog_timeout = 5s
slowlog = /var/log/php-fpm/www-slow.log
php_admin_value[memory_limit] = 256M
php_admin_value[error_log] = /var/log/php-fpm/www-error.log
php_admin_flag[log_errors] = on
php_admin_flag[display_errors] = off
catch_workers_output = yes
decorate_workers_output = no故障处理实战
先把日志位置捋清楚
排查之前,得先知道去哪看日志。PHP-FPM相关的日志主要有这么几个:
| 日志类型 | 默认位置 | 作用 |
|---|---|---|
| FPM主进程错误日志 | /var/log/php-fpm/error.log | FPM本身启动、运行报错 |
| Pool错误日志 | php-fpm.conf里error_log | 子进程执行PHP脚本时的错误 |
| 慢请求日志 | pool里slowlog配置项 | 执行超时的请求调用栈 |
| Nginx错误日志 | /var/log/nginx/error.log | 反代PHP-FPM失败的信息 |
| 系统日志 | /var/log/messages | OOM、进程被杀等系统事件 |
重要提醒:很多人会漏掉Nginx错误日志。其实Nginx作为反向代理,它把请求转发给PHP-FPM时,如果FPM那边出了问题,错误信息会先打到Nginx日志里。所以排查的时候两个日志要一起看。
网站打不开的排查套路
场景描述:用户反馈网站访问报502 Bad Gateway,或者一直转圈圈最后超时打不开。
我把这个排查流程总结成一个四步法,照着走基本都能定位到问题。
第一步:先看进程在不在
ps aux | grep php-fpm正常情况下你应该能看到一个主进程 + N个子进程(N就是pm.max_children配置的数量)。如果主进程都没了,那说明FPM整个挂了,直接去看FPM的主错误日志。
如果子进程数量=0但主进程在,那就是子进程都死光了,赶紧看子进程的错误日志。
第二步:看监听端口或socket在不在
# TCP方式
ss -tlnp | grep 9000
# Unix socket方式
ls -l /run/php-fpm/www.sock监听不上的常见原因有三个:
- FPM没启动或正在重启
- socket文件权限不对,Nginx用户读不到
listen.backlog被占满,新连接进不来
第三步:看Nginx错误日志
tail -f /var/log/nginx/error.log重点关注这几类错误:
connect() failed (111: Connection refused) while connecting to upstream—— FPM没起来或端口不通connect() to unix:/run/php-fpm/www.sock failed (2: No such file or directory)—— socket文件不存在upstream timed out (110: Connection timed out)—— FPM处理请求超时no live upstreams while connecting to upstream—— 所有FPM子进程都挂了
第四步:看系统资源
free -h # 内存
top -c # CPU
df -h # 磁盘
ulimit -n # 文件描述符到这里基本能定位到80%的问题了。
一个真实的502排查案例
我之前遇到过一次比较典型的案例,分享一下。
某天上午10点,业务突然开始502,监控告警一下子炸了。SSH连上去之后,我先看了下Nginx错误日志,满屏都是:
connect() to unix:/run/php-fpm/www.sock failed (11: Resource temporarily unavailable) while connecting to upstream注意这个错误码,11 = EAGAIN,意思是socket队列被占满了。我马上意识到这是listen.backlog的问题。
接着去看了下FPM状态:
curl http://127.0.0.1:9000/status # 如果开了status页的话输出显示active processes = 100,正好等于pm.max_children。也就是说所有子进程都被占满了,新的请求根本排不上队。
根因找到了:那天正好有个新功能上线,某个接口因为代码bug导致每个请求都要执行20多秒,直接把FPM的子进程全占满了。后来临时调大了pm.max_children(从100调到200),同时让开发紧急修复了那个慢接口,问题才彻底解决。
但这只是临时止血。长期方案是开启request_terminate_timeout,让慢请求自动超时释放进程。这个参数我上一节讲过,生产环境一定打开。
进程卡死怎么排查?
这个比502还头疼。502好歹有个错误码,进程卡死就是请求一直挂着不返回。下面讲讲我的排查思路。
场景一:单进程卡死,其他进程正常
这种情况一般是某个请求陷入了死循环、阻塞IO、或者死锁。
排查命令:
# 找出执行时间最长的FPM子进程
ps -eo pid,etime,pcpu,pmem,cmd | grep php-fpm
# 用strace跟踪这个进程在干啥
strace -p <PID>
# 也能用lsof看进程打开了哪些文件/网络连接
lsof -p <PID>如果strace输出一直在显示futex、read、recvfrom之类的系统调用卡住,说明进程在等某个资源。这时候根据不同的系统调用能判断问题:
- 卡在
futex—— 死锁或者锁竞争 - 卡在
read—— 读文件或读网络慢 - 卡在
recvfrom—— 等某个外部接口返回
场景二:所有进程都卡死
这种情况一般是全局性的问题,比如:
- 数据库挂了,所有请求都在等数据库连接池
- 远程API挂了,所有请求都在等HTTP返回
- 磁盘满了,写不了日志
- 内存爆了,开始疯狂swap
排查命令:
# 看整体负载
uptime
top -c
# 看是不是OOM了
dmesg | grep -i "killed process"
# 看磁盘和inode
df -h
df -idmesg这个命令特别重要。如果系统因为内存不够杀掉了进程,会在这里留下"Out of memory: Killed process xxxx (php-fpm)"的记录。我之前碰到过好几次FPM莫名奇妙消失,SSH一查dmesg就全明白了。
慢日志的使用技巧
上一节讲过slowlog这个参数,这一节讲讲怎么用好它。
第一步:开启慢日志
request_slowlog_timeout = 5s
slowlog = /var/log/php-fpm/www-slow.log第二步:定期去翻这个文件
# 找出出现频率最高的慢请求
awk '{print $NF}' /var/log/php-fpm/www-slow.log | sort | uniq -c | sort -rn | head -20第三步:看调用栈定位问题
慢日志里会记录PHP的完整调用栈,类似这样:
[0x00007f8b1c2a3638] file_get_contents() /var/www/html/app/Service/ApiService.php:42
[0x00007f8b1c2a35e0] ApiService->callThirdParty() /var/www/html/app/Controller/OrderController.php:78
[0x00007f8b1c2a3450] OrderController->create() /var/www/html/public/index.php:25一眼就能看出是ApiService->callThirdParty()这个方法慢,可能是调用了某个外部API。
几个救命的小命令
最后再分享几个生产环境救命的小命令,都是真用过的:
1. 看FPM进程状态(如果开了pm.status_path)
在www.conf里加上:
pm.status_path = /statusNginx里配一下转发:
location /status {
fastcgi_pass unix:/run/php-fpm/www.sock;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $fastcgi_script_name;
allow 127.0.0.1;
deny all;
}然后就能看到这些关键信息:
curl http://127.0.0.1/status输出里关注这几个值:
accepted conn:累计接收的连接数active processes:当前活跃进程数idle processes:当前空闲进程数total processes:总进程数max active processes:历史最大活跃进程数slow requests:慢请求数
2. 一键看FPM子进程都在干啥
# 看每个子进程在处理什么请求
ps -ef | grep "php-fpm" | grep -v grep | awk '{print $2}' | xargs -I {} sh -c 'echo "PID: {}"; cat /proc/{}/cmdline 2>/dev/null; echo'如果开了pm.status_path+/status?full,还能看到每个子进程正在处理的请求URL。这在排查某个慢请求占着进程不放的时候特别有用。
排查思路总结
最后用我自己的经验把排查流程串一下:
- 网站打不开 —— 先看进程在不在 → 看监听在不在 → 看Nginx错误日志 → 看系统资源
- 网站慢 —— 看FPM状态页的活跃进程数 → 翻慢日志定位慢请求 → 抓典型慢请求的strace
- 进程卡死 —— strace跟踪卡在哪 → 看是不是等数据库/外部API → 看是不是死锁
- 进程消失 —— 第一件事去查
dmesg看是不是被OOM Killer杀了
记住这四条,99%的PHP-FPM问题都能搞定。剩下1%是那些玄学问题,比如磁盘满了、inode爆了、容器cgroup限制搞错了,那就要再具体问题具体分析了。
如果觉得今天的内容对你有帮助,欢迎关注@运维躬行录,我会持续分享一线运维踩坑经验。咱不整那些虚的,全是生产环境摸爬滚打出来的干货。
也可以访问我的个人博客 躬行笔记 看更多实战文章,觉得有用的话顺手转发给身边搞运维的朋友,咱们一起进步。
咱们下期见。