目录
- [1. 引言:为什么需要理解 PHP 的生命周期](#1. 引言:为什么需要理解 PHP 的生命周期 "#1-%E5%BC%95%E8%A8%80%E4%B8%BA%E4%BB%80%E4%B9%88%E9%9C%80%E8%A6%81%E7%90%86%E8%A7%A3-php-%E7%9A%84%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F")
- [2. 先建立整体认知:三个角色](#2. 先建立整体认知:三个角色 "#2-%E5%85%88%E5%BB%BA%E7%AB%8B%E6%95%B4%E4%BD%93%E8%AE%A4%E7%9F%A5%E4%B8%89%E4%B8%AA%E8%A7%92%E8%89%B2")
- [3. 一次请求的完整生命周期](#3. 一次请求的完整生命周期 "#3-%E4%B8%80%E6%AC%A1%E8%AF%B7%E6%B1%82%E7%9A%84%E5%AE%8C%E6%95%B4%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F")
- [3.1 阶段一:Nginx 接收请求](#3.1 阶段一:Nginx 接收请求 "#31-%E9%98%B6%E6%AE%B5%E4%B8%80nginx-%E6%8E%A5%E6%94%B6%E8%AF%B7%E6%B1%82")
- [3.2 阶段二:FastCGI 协议封装](#3.2 阶段二:FastCGI 协议封装 "#32-%E9%98%B6%E6%AE%B5%E4%BA%8Cfastcgi-%E5%8D%8F%E8%AE%AE%E5%B0%81%E8%A3%85")
- [3.3 阶段三:PHP-FPM 调度到 Worker](#3.3 阶段三:PHP-FPM 调度到 Worker "#33-%E9%98%B6%E6%AE%B5%E4%B8%89php-fpm-%E8%B0%83%E5%BA%A6%E5%88%B0-worker")
- [3.4 阶段四:响应回传](#3.4 阶段四:响应回传 "#34-%E9%98%B6%E6%AE%B5%E5%9B%9B%E5%93%8D%E5%BA%94%E5%9B%9E%E4%BC%A0")
- [4. PHP-FPM 的进程模型](#4. PHP-FPM 的进程模型 "#4-php-fpm-%E7%9A%84%E8%BF%9B%E7%A8%8B%E6%A8%A1%E5%9E%8B")
- [4.1 Master / Worker 双层结构](#4.1 Master / Worker 双层结构 "#41-master--worker-%E5%8F%8C%E5%B1%82%E7%BB%93%E6%9E%84")
- [4.2 三种进程管理策略](#4.2 三种进程管理策略 "#42-%E4%B8%89%E7%A7%8D%E8%BF%9B%E7%A8%8B%E7%AE%A1%E7%90%86%E7%AD%96%E7%95%A5")
- [4.3 为什么 PHP 不需要连接池](#4.3 为什么 PHP 不需要连接池 "#43-%E4%B8%BA%E4%BB%80%E4%B9%88-php-%E4%B8%8D%E9%9C%80%E8%A6%81%E8%BF%9E%E6%8E%A5%E6%B1%A0")
- [5. 与 Java 常驻模型的本质差异](#5. 与 Java 常驻模型的本质差异 "#5-%E4%B8%8E-java-%E5%B8%B8%E9%A9%BB%E6%A8%A1%E5%9E%8B%E7%9A%84%E6%9C%AC%E8%B4%A8%E5%B7%AE%E5%BC%82")
- [6. 全链路时序图](#6. 全链路时序图 "#6-%E5%85%A8%E9%93%BE%E8%B7%AF%E6%97%B6%E5%BA%8F%E5%9B%BE")
- [7. 实践建议](#7. 实践建议 "#7-%E5%AE%9E%E8%B7%B5%E5%BB%BA%E8%AE%AE")
- [8. 小结](#8. 小结 "#8-%E5%B0%8F%E7%BB%93")
1. 引言:为什么需要理解 PHP 的生命周期
很多同学写 PHP 时,习惯把 PHP 当成"脚本语言"来理解:写完一个 index.php,丢到服务器上,浏览器一访问就出结果。但一旦遇到线上问题------比如"为什么 PHP 没有像 Java 那样的连接池"、"为什么 static 变量在两次请求之间不共享"、"为什么 PHP-FPM 进程数调大反而更慢"------就会发现,不理解生命周期和进程模型,很多现象根本解释不通。
本文从一次 HTTP 请求出发,把「Nginx → FastCGI → PHP-FPM → PHP 脚本执行 → 响应返回」这条链路完整拆开,并重点对比 PHP-FPM 的常驻进程模型 与 Java 常驻应用模型 的本质差异,最后给出一张全链路时序图,方便你建立整体认知。
2. 先建立整体认知:三个角色
在典型的 LNMP 架构中,一次 PHP 请求会经过三个核心角色:
| 角色 | 职责 | 是否常驻 |
|---|---|---|
| Nginx | 接收 HTTP 请求、静态资源直接返回、动态请求转发给 PHP-FPM | 常驻(Master + Worker) |
| PHP-FPM | 管理一组 PHP 进程,接收 FastCGI 协议请求并调度给 Worker | 常驻(Master + Pool Worker) |
| PHP 解释器 | 在 Worker 进程内解析并执行 PHP 脚本 | 随 Worker 常驻,但每次请求重新初始化脚本上下文 |
关键点在于:PHP-FPM 的进程是常驻的,但 PHP 的"应用状态"不是常驻的。这是理解后面所有差异的钥匙。
下面用一张图把三个角色的关系与数据流向串起来:
3. 一次请求的完整生命周期
3.1 阶段一:Nginx 接收请求
浏览器发起 GET /index.php?id=1,Nginx 的 Master 进程早已监听 80/443 端口,连接建立后由某个 Worker 进程接管。Nginx Worker 根据 location 配置判断:
- 静态文件(
.css、.js、图片)→ 直接读磁盘返回; .php结尾 → 交给fastcgi_pass指定的上游,也就是 PHP-FPM 监听的地址(Unix Socket 或127.0.0.1:9000)。
nginx
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
3.2 阶段二:FastCGI 协议封装
Nginx 不会直接"调用" PHP,而是把 HTTP 请求翻译成 FastCGI 协议的二进制报文。FastCGI 是一种二进制协议,报文由「Header + Body」组成,常见类型有:
FCGI_BEGIN_REQUEST:开始一次请求;FCGI_PARAMS:传递QUERY_STRING、REQUEST_METHOD、SCRIPT_FILENAME等参数;FCGI_STDIN:请求体(POST 数据);FCGI_STDOUT/FCGI_STDERR:PHP 的输出与错误。
相比 CGI 每次请求 fork 一个新进程,FastCGI 的核心优势就是进程复用:一个进程可以连续处理多个请求,省去反复 fork 和加载解释器的开销。
3.3 阶段三:PHP-FPM 调度到 Worker
PHP-FPM 的 Master 进程收到连接后,从进程池中挑一个空闲 Worker,把请求交给它。Worker 进程内部大致做这些事:
- 读取并解析 FastCGI 报文 ,还原出
$_GET、$_POST、$_SERVER等超全局变量; - 初始化请求上下文 :重置符号表、清空上一次请求残留的全局变量、设置
max_execution_time计时器; - 编译并执行脚本:如果开启了 OPcache,会先查缓存,命中则跳过词法/语法分析,直接执行 opcode;
- 输出缓冲 :
echo的内容先进入输出缓冲区,按output_buffering配置决定何时刷出; - 请求收尾 :执行
register_shutdown_function注册的回调、调用析构函数、关闭数据库连接、释放内存(memory_limit检查贯穿全程)。
3.4 阶段四:响应回传
Worker 把输出通过 FCGI_STDOUT 写回 Nginx,Nginx 再组装成标准 HTTP 响应(补上状态行、响应头)返回给浏览器。至此一次请求结束,Worker 进程并不退出,而是回到空闲状态等待下一个请求。
把四个阶段串成一条状态流转,可以更直观地看到请求在链路中的推进:
4. PHP-FPM 的进程模型
4.1 Master / Worker 双层结构
- Master 进程 :只负责管理,不处理请求。监听
SIGUSR2做平滑重载、SIGQUIT优雅退出,根据pm策略动态增减 Worker。 - Worker 进程:真正干活的进程,每个进程同一时刻只处理一个请求(单线程同步模型)。
4.2 三种进程管理策略
| 策略 | 说明 | 适用场景 |
|---|---|---|
static |
固定数量 Worker,启动即创建 | 流量稳定、追求低延迟 |
dynamic |
空闲时缩容,繁忙时扩容,有 start_servers / min_spare_servers / max_spare_servers |
大多数通用场景 |
ondemand |
有请求才创建 Worker,空闲一段时间后销毁 | 低流量、内存敏感场景 |
ini
pm = dynamic
pm.max_children = 50
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 20
pm.max_requests = 500
其中 pm.max_requests 很关键:Worker 处理满 N 个请求后自动退出并由 Master 重建,用来规避第三方扩展的内存泄漏。
三种策略在进程数量随流量变化上的差异,可以用下图对比:
4.3 为什么 PHP 不需要连接池
因为每个 Worker 是同步阻塞 处理请求的,一个进程同一时刻只服务一个请求,所以进程内的数据库连接天然不会被并发争用。这也是为什么 PHP 里"连接池"通常要靠 pgbouncer、ProxySQL 这类外部组件,而不是像 Java 那样在应用内维护 HikariCP。
5. 与 Java 常驻模型的本质差异
很多人把"PHP-FPM 进程常驻"和"Java 应用常驻"混为一谈,其实两者常驻的粒度完全不同。
| 维度 | PHP-FPM | Java(Spring Boot 等) |
|---|---|---|
| 常驻对象 | 进程常驻,应用状态不常驻 | 进程常驻,应用状态常驻 |
| 请求处理 | 每请求重建脚本上下文 | 同一 JVM 内多线程共享堆内存 |
| 并发模型 | 多进程 + 同步阻塞 | 单进程多线程(或协程) |
| 内存共享 | 进程间不共享(除 OPcache 共享内存) | 线程间共享堆对象 |
| 连接池 | 一般无(靠外部中间件) | 应用内连接池(HikariCP 等) |
| 热更新 | reload 平滑重启 Worker |
需重启或借助热部署工具 |
| 内存泄漏影响 | 单 Worker 泄漏,max_requests 可兜底 |
整个 JVM 受影响,可能 OOM |
一句话总结:PHP-FPM 常驻的是"执行环境",Java 常驻的是"应用实例"。PHP 每次请求都从"干净"的状态开始,这既是它的简单之处,也是它无法做进程内缓存、连接池、长连接推送的根本原因。
从请求处理的角度看,两者的差异可以这样对照:
6. 全链路时序图
7. 实践建议
- 合理设置
pm.max_children:按可用内存 / 单进程内存估算,盲目调大会导致内存 swap,反而拖慢整体。 - 开启 OPcache :
opcache.enable=1、opcache.validate_timestamps=0(生产环境配合发布时 reload),能显著减少编译开销。 - 用
pm.max_requests兜底内存泄漏:尤其是用了老旧扩展的项目。 - 不要在 PHP 里做进程内缓存 :
static变量、全局数组在请求结束后都会销毁,需要缓存请用 Redis / Memcached。 - 长耗时任务交给队列:PHP-FPM Worker 数量有限,同步执行耗时任务会迅速耗尽进程池。
8. 小结
理解 PHP 生命周期,核心就三句话:
- Nginx 负责接入与转发,FastCGI 负责协议转换,PHP-FPM 负责进程调度,PHP 解释器负责执行;
- 进程常驻,但应用状态不常驻,每次请求都是一次"干净的初始化";
- 与 Java 的差异不在"是否常驻",而在常驻的粒度------这决定了连接池、缓存、并发模型等一系列设计取舍。
把这条链路在脑子里跑通,再遇到"PHP 为什么慢""进程数怎么调""为什么没有连接池"这类问题,就能从根上解释了。