各位同学,大家上午好!
把椅子拉过来,坐好。别在那玩手机了,今天咱们不聊“今天天气真不错”,咱们聊点硬核的,聊点能让你的服务器“咕嘟”冒泡,或者让你在老板面前保住饭碗的东西——PHP-FPM。
很多人对PHP有误解,觉得它是“爸爸写的语言”,是“写脚本的”。这话没错,它起步是这么回事,但现在?它可是C语言写出来的核心,跑得比谁都欢。而驱动这头大象跳舞的,就是那个大家伙——PHP-FPM (PHP FastCGI Process Manager)。
今天这场讲座,咱们不整那些虚头巴脑的“为了解决什么问题而提出什么方案”,咱们直接上干货。咱们要扒开PHP-FPM的皮,看看它的骨,摸摸它的肉,最后教你怎么给它穿衣打扮,好让它在高并发的大街上能抗住几千个流氓的围殴。
准备好了吗?咱们开始。
第一部分:PHP-FPM到底是个什么鬼?(一图胜千言)
首先,你得知道PHP-FPM在这个生态系统里干嘛的。
以前,Web服务器(比如Nginx)请求PHP,就像点外卖。它是这样工作的:
- 用户点餐。
- 后厨(PHP解释器)一看,这单来了。
- 后厨立马腾出一块地,架起炉灶,洗菜,切菜,炒菜,出锅。
- 喊一声:“外卖小哥,拿走!”
- 后厨把地擦干净,回去睡觉。
如果你点了10单,后厨就得忙活10次。这在CGI时代就是这样,性能差,开销大。要是赶上双十一,后厨非得累散架不可。
PHP-FPM 的出现,就是为了把“点单”变成“雇佣全职厨师”。
它是一个进程管理器。
- Master 进程:这就是那个穿着西装、坐在办公室里盯着监控屏的老板。它不干活,它负责指挥。谁累了?谁该休息了?谁来干活了?谁该被炒鱿鱼了?都是老板说了算。而且,老板是单线程的,但它特别稳,就算它挂了,整个工厂就瘫痪了,所以它没事儿就给自己拍个快照(备份)。
- Worker 进程:这就是后厨的厨师。他们时刻待命,手里拿着菜刀。一旦老板喊一声“上菜”,厨师立马拿起菜单,开工。干完一单,把盘子一端走,然后继续等着下一单。这就是“长连接”。一单接一单,不用重新启动,多爽?
代码示例:看看你的工厂长什么样
打开你的终端,敲这行命令:
ps aux | grep php-fpm
你会看到类似这样的画面:
root 1234 0.0 0.1 ... /usr/sbin/php-fpm: master process
www-data 5678 0.0 5.2 ... /usr/sbin/php-fpm: pool www
www-data 5679 0.0 5.1 ... /usr/sbin/php-fpm: pool www
www-data 5680 0.0 5.0 ... /usr/sbin/php-fpm: pool www
你看,那个 master process 就是你的老板。下面的 pool www 就是你的小工。注意,这里默认有3个小工。如果你只有1个用户访问,这3个小工里有一个去干活了,剩下两个在旁边喝奶茶。
第二部分:Process Manager (PM) 模式——到底是用固定员工还是按需招聘?
PHP-FPM 之所以牛,在于它有三种工作模式。这三种模式对应着你不同的业务场景。选错了模式,轻则服务器CPU空转(浪费钱),重则服务器内存爆炸(蓝屏)。
1. Static 模式(固定员工模式)
配置:
pm = static
pm.max_children = 10
pm.start_servers = 10
pm.min_spare_servers = 10
pm.max_spare_servers = 10
场景: 你的网站流量很稳定,就像那个只开一家店的夫妻老婆店,每天就那几百个人来逛,不多也不少。
原理: 老板(Master)一开机,直接雇10个厨师(Worker)。不管你现在有没有活干,这10个人必须都在工位上坐着。
- 优点: 没有建立连接的开销,来了请求立马就做,响应最快。
- 缺点: 浪费资源。如果来了5个人,老板还坐着10个厨师,这5个厨师没事干只能干瞪眼。如果来了100个人,只有10个厨师,那就排队吧,前面的人吃完了后面的人才动。
2. Dynamic 模式(灵活用工模式)—— 这是最常用的!
配置:
pm = dynamic
pm.max_children = 50
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 20
场景: 你的网站像个大商场,周末人多得吓人,平时门可罗雀。
原理: 老板一开始先雇10个人。然后呢?老板会时刻盯着门口。
- min_spare_servers (5): 闲得无聊的时候,不能让工位空着太惨。必须得保证至少有5个人在工位上玩手机。这样周末一来,立马就能顶上去。
- max_spare_servers (20): 也不能雇太多。如果工位上闲着20个人,说明你人招多了,太浪费电费了,赶紧把多余的人辞退几个。
动态扩缩容过程:
假设你现在有10个员工,流量激增来了50单。
- 第11单来了:老板招第11个。
- …
- 第20单来了:老板招第20个。
- 第21单来了:老板看了一眼,工位上已经满了(20人),行吧,排队。
代码示例:动态参数的实时监控
你可以写个脚本去统计当前有多少个worker在待命:
# 这是一个简单的 bash 脚本,用来观察动态模式下的变化
while true; do
echo "Time: $(date) - Active Workers: $(ps aux | grep 'php-fpm' | grep 'pool www' | wc -l)"
sleep 1
done
当你访问网站时,你会发现那个数字在跳动,这就是动态招聘的威力。
3. OnDemand 模式(临时工模式)
配置:
pm = ondemand
pm.max_children = 50
pm.process_idle_timeout = 10s
场景: 你的服务器内存非常非常紧张,你一分钱恨不得掰成两半花。你不想让那些没事干的小工浪费内存。
原理: 这种模式下,老板一开机,工位上一个人都没有。没人没事干。
- 当来了第1个请求,老板喊一声“干活!”,立马招一个。
- 这个人干完活,如果不闲着,10秒(
process_idle_timeout)后,老板说“你可以下班了,回家睡觉吧,别在这浪费内存了”,把他踢走。 - 下一单来了,再喊。
优点: 节省内存,没有空闲浪费。
缺点: 每次来新活,都得先“招人”,这个过程是有开销的。如果并发量特别大,老板招人招得手抽筋,反而影响性能。
第三部分:高并发调优的核心——别把服务器撑爆了!
这是今天的重头戏。很多新人一上来就瞎配参数,把 max_children 设成1000。然后呢?服务器直接 OOM Killed。
公式篇:计算你的 pm.max_children
这是数学,不是玄学。
- 总内存: 拿出你的服务器规格,比如 2GB。
- 系统保留: 操作系统、Nginx、MySQL(如果也在同一台机子上)得吃饭啊。咱们留个1GB给它们,保平安。
- PHP单进程内存: 这是个关键。一个 PHP 进程到底占多少内存?
- 基础开销:几十 MB。
- 脚本内存:你的代码、数据库连接、引用的对象。
- 你需要实测。怎么测?写个脚本。
代码示例:内存压测脚本
<?php
// memory_test.php
$mem = [];
for($i = 0; $i < 10000; $i++) {
// 模拟分配内存,比如分配 1MB 的字符串
$mem[] = str_repeat('a', 1024 * 1024);
}
echo "Memory used: " . memory_get_usage(true) . " bytesn";
echo "Peak memory: " . memory_get_peak_usage(true) . " bytesn";
?>
运行它:
/usr/bin/php memory_test.php
假设输出是 50MB 和 55MB。
那么你的计算公式就是:
Max_Children = (总内存 - 系统保留) / (单进程内存 + 50MB冗余)
假设 2GB 机器:
Max_Children = (2048 - 1024) / (55 + 50) = 1024 / 105 ≈ 9
结论: 在这台2GB机器上,pm.max_children 你敢设成 10,恭喜你,你稳了。设成 11,对不起,服务器挂了。
场景分析:
- 低配服务器(512MB – 1GB): 咱们用 Static 模式吧。别搞动态了,动态模式那维护开销(
pm.start_servers等参数)对于小内存机器来说是奢侈品。直接max_children = 3,别贪多。 - 中配服务器(2GB – 4GB): 推荐 Dynamic 模式。给系统留够内存,剩下的钱砸在PHP上。比如
max_children = 20到 40 之间。 - 高配服务器(8GB+): 这种情况,瓶颈通常不在于PHP进程数,而在于网络带宽或数据库。这时候可以考虑 Static,因为上下文切换的开销可能开始成为瓶颈了。
第四部分:死循环与内存泄漏——为什么要有 pm.max_requests?
兄弟们,咱们都知道 PHP 是脚本语言,脚本执行完就死。但是 PHP-FPM 是进程模型啊!
如果一个 Worker 进程是“永生”的,那就意味着它是“有毒”的。
场景模拟:
- 小工(Worker)A 开始干活。
- 小工 A 写代码的时候手抖了,或者用了某个没有释放的闭包,或者打开了文件句柄没关。
- 这就叫内存泄漏。
- 正常情况下,脚本跑完,内存会释放。
- 但是!Worker A 很变态,它跑完一次请求,内存没释放,反而多占了 10MB。
- 它又跑了一次,又多了 10MB。
- 跑了1000次,它占用了 10GB 内存!
- 整个服务器内存爆了,老板(Master)崩溃,全站挂了。
代码示例:模拟内存泄漏
<?php
// leak.php
// 这个脚本故意不释放内存
$large_array = [];
for($i = 0; $i < 1000000; $i++) {
$large_array[] = str_repeat('x', 1024); // 每次加1KB
}
// 故意不 unset($large_array)
// 也不 exit(); 让它继续循环下去
while(true) {
sleep(1);
}
?>
如果这是个 Worker 进程,服务器很快就完蛋了。
解决方案:pm.max_requests
这是 PHP-FPM 的保命符。它的意思就是:“不管你累不累,干了多少单,必须给我停!”
配置:
pm.max_requests = 500
含义: 每一个 Worker 进程,处理完 500 个请求后,必须强制重启。
逻辑是这样的:
- Worker A 处理到第 499 单,内存 100MB。
- Worker A 处理第 500 单,内存 101MB。
- 第 500 单处理完,Worker A 收到信号,准备重启。
- 关键步骤: Worker A 会把“当前正在处理的这一个请求”的任务传递给一个新的 Worker B(如果配置了
pm.fast_cleanup之类的高级参数,或者如果是简单模型,可能会丢弃,视具体版本而定,但通常会有优雅处理),然后 Worker A 自杀。 - 老板(Master)一看:“A 卒了!赶紧招个 B 补上!”
- B 进程是全新的,内存清零。
为什么不是 0?
有些系统操作(比如 opcache 重载、某些扩展的 init 函数)在进程启动时执行一次就够了,不需要每次请求都执行。所以给一点缓冲是合理的,比如 500 或者 1000。
第五部分:信号机制——老板的暴脾气
PHP-FPM 是基于信号(Signal)通信的。理解信号,你就理解了如何平滑重启。
1. SIGTERM (杀人)
当你执行 kill -TERM 1234 时,Master 进程收到信号。
- Master 不会立马死。
- Master 会告诉所有 Worker:“别干了,准备下班!”
- Worker 会把当前手里那单做完。
- Worker 死掉。
- Master 死掉。
- 这叫优雅关闭。期间不会丢失请求。
2. SIGUSR2 (重启)
当你执行 kill -USR2 1234 时。
- Master 会把它的 PID 复制一份,改名为
php-fpm.pid.oldbin。 - Master 启动一个新的 Master 进程(这叫“Fork”出新老板)。
- 新老板接管了旧老板所有的 Worker。
- 旧老板挂了。
- 这就是无缝重启! 在升级 PHP 版本、修改
php.ini参数时,你用的是这个命令。你的网站完全不会停机,用户根本感觉不到。
代码示例:编写一个自动重启脚本
写个 restart.php,放在 crontab 里,每5分钟检查一下,如果进程卡死了就重启。
<?php
// watchdog.php
$pid_file = '/var/run/php-fpm.pid';
$timeout = 5; // 超过5秒没响应就干掉
if (file_exists($pid_file)) {
$pid = file_get_contents($pid_file);
// 检查进程是否存在
if (posix_kill($pid, 0)) {
// 检查进程是否还活着(非阻塞)
$status = shell_exec("ps -p $pid -o etime= | tr -d ' '");
if ($status > $timeout) {
echo "Worker process ($pid) is stuck for {$status}s. Killing...n";
// 这里用 SIGTERM
posix_kill($pid, SIGTERM);
}
}
}
?>
第六部分:慢日志——抓出那些慢吞吞的猪
高并发下,最怕的不是服务器挂了,而是服务器没挂,但用户在等。
场景: 你的代码里有个 sleep(10),或者查询了一个索引失效的 SQL。
如果是静态 PHP(每次都重启),那10秒后报错。
如果是 PHP-FPM Worker,这个 Worker 占着茅坑(进程)不拉屎整整10秒!这10秒内,这个进程不能干别的,只能干这一单。
如果有1000个并发请求进来,而你只有10个 Worker。这就意味着前10个请求在等,第11个请求进来,发现前10个还没干完,第11个也得排队。
配置:
slowlog = /var/log/php-fpm/slow.log
request_slowlog_timeout = 10s
实战技巧:
开启慢日志后,一旦有请求超过10秒没跑完,日志里就会记录下来。
你可以直接用 tail -f 盯着它。
一旦看到某个脚本在飙日志,立马去查那个脚本!99%是死循环、数据库锁或者网络超时。
代码示例:抓取慢日志
# 终端A:监控慢日志
tail -f /var/log/php-fpm/slow.log
# 终端B:发起大量请求压测
for i in {1..1000}; do curl http://your-site.com/api/do-something & done
你会发现,slow.log 开始疯狂滚动,记录了哪个进程(通常包含进程号)、哪个脚本(filename)、花了多久。
第七部分:进阶优化——让 PHP-FPM 呼吸顺畅
除了上面的基本参数,还有一些细节能让你的服务器更舒服。
1. request_terminate_timeout(超时控制)
有时候 Worker 卡死了,内存没泄漏,但它就是不返回响应。这时候不能让它永远占着位置。
request_terminate_timeout = 30
超过30秒,PHP-FPM 会直接杀掉这个请求,并且断开连接。这能防止 Worker 被恶意请求耗尽。
2. 进程优先级
如果你想让你的 PHP-FPM 比 MySQL 优先级高一点,可以用 nice 或者 ionice。
php_admin_value[error_log] = /var/log/php-fpm/www-error.log
php_admin_value[sendmail_from] = [email protected]
php_admin_flag[log_errors] = on
php_admin_value[memory_limit] = 256M
(注意:这些是 php.ini 里不能改的,得在 www.conf 里用 php_admin_value 覆盖,这能防止某些黑客修改 php.ini 跑满内存)。
3. buffer 设置
Web 服务器和 PHP 之间传输数据,是有缓冲区的。
output_buffering = Off
建议关掉。PHP 搞那一套缓冲,反而让调试变难了,而且会增加延迟。让 PHP 搞它的 echo,Nginx(或 Apache)去搞它的缓冲。Nginx 的 proxy_buffering 才是真正控制大文件传输和响应速度的关键。
第八部分:总结与哲学思考
好了,各位,今天聊了不少。我们回顾一下:
- 架构理解:PHP-FPM 是 Master(指挥官) + Worker(苦力) 的架构。进程模型保证了状态隔离(防止内存泄漏互相传染)。
- 模式选择:
- 流量稳 -> Static。
- 流量波动大 -> Dynamic(推荐)。
- 内存极紧 -> OnDemand。
- 核心参数:
- max_children:根据内存算,宁少勿多,留点钱给操作系统。
- max_requests:防止内存泄漏,这是保命符。
- slowlog:发现问题的雷达。
- 调优:没有最好的配置,只有最适合你服务器资源、业务场景的配置。
最后,我想说点题外话。
很多面试官问你:“你觉得 PHP-FPM 和 Nginx 的区别是什么?”
其实它们关系很微妙。
Nginx 是门童,只负责接待,把客人(请求)领到后厨。
PHP-FPM 是后厨,负责烹饪,但它是独木桥(进程模型),一次只能做一单(除非是多进程)。
如果把服务器比作一家餐厅:
- Nginx 是前台服务员,他动作快,能同时接待100桌客人,但因为他只负责“端菜”,所以他不能做菜。
- PHP-FPM 是后厨大厨。大厨一单接一单地做,他做菜的速度决定了餐厅的上菜速度。
当你觉得餐厅上菜太慢(高并发响应慢)时,你应该:
- 加派服务员(Nginx 多线程/多进程)?——通常没用,瓶颈在后厨。
- 招更多大厨(增加
pm.max_children)?——可以,但要看大厨的饭量(内存)。
所以,调优的本质,不是“堆人”,而是“精细化运营”。
别总想着怎么让 PHP 变得像 C++ 一样快,那是没用的。PHP 的快,在于它的生态和开发效率。PHP-FPM 的快,在于它的进程复用。
希望今天的讲座能让你在下次看到 502 Bad Gateway 或者 504 Gateway Time-out 时,心里不再慌张,而是能冷冷地拍拍服务器的机箱,说一句:“有点意思,看来是 pm.max_children 没设好啊,兄弟,给点面子,加几个 Worker 进程。”
好了,今天就到这儿。下课!记得把你的 php-fpm.conf 改好再下班!