一、为什么 Nginx 不直接解析 PHP?
Nginx 的核心设计哲学是事件驱动 + 异步非阻塞,它擅长处理静态资源和高并发连接。但 PHP 是同步阻塞型语言,脚本执行过程中可能涉及数据库查询、文件 IO 等耗时操作,如果让 Nginx 直接内嵌 PHP 解析器,一个慢请求就会拖垮整个 worker 进程。
因此,Nginx 将 PHP 请求转发给一个独立的 PHP 进程管理器处理,两者之间通过 FastCGI 协议通信。这种解耦架构让 Web 服务器和应用运行时各司其职,互不阻塞。
二、FastCGI 协议内核解析
2.1 从 CGI 到 FastCGI 的演进
传统 CGI 模式下,每来一个请求就 fork 一个新进程执行 PHP 脚本,执行完毕进程销毁。进程创建和销毁的开销在高并发场景下不可接受。
FastCGI 的核心改进是进程常驻:PHP 进程启动后不退出,持续监听 FastCGI 请求,处理完一个请求后继续等待下一个。这消除了频繁 fork 的开销,同时支持多进程并发处理。
2.2 FastCGI 协议的数据结构
FastCGI 协议是一种二进制协议,所有数据以「记录(Record)」为单位传输。每条记录由固定 8 字节头部 + 可变长度内容组成:
typedef struct {
unsigned char version; // 协议版本,通常为 1
unsigned char type; // 记录类型
unsigned char requestIdB1; // 请求 ID(大端序,2 字节)
unsigned char requestIdB0;
unsigned char contentLengthB1; // 内容长度(大端序,2 字节)
unsigned char contentLengthB0;
unsigned char paddingLength; // 填充长度
unsigned char reserved; // 保留字节
} FCGI_Header;
关键记录类型包括:
- FCGI_BEGIN_REQUEST:Nginx 通知 php-fpm 开始一个新请求
- FCGI_PARAMS:传递环境变量(如
SCRIPT_FILENAME、REQUEST_METHOD、QUERY_STRING等) - FCGI_STDIN:传递请求体(POST 数据)
- FCGI_STDOUT:php-fpm 返回的响应内容
- FCGI_STDERR:php-fpm 的错误输出
- FCGI_END_REQUEST:请求结束标志
2.3 一次完整通信的流程
以 GET /index.php?id=1 为例:
- Nginx 与 php-fpm 建立 TCP 连接(或 Unix Socket 连接)
- Nginx 发送
FCGI_BEGIN_REQUEST,指定请求 ID 和角色(RESPONDER) - Nginx 发送
FCGI_PARAMS,将 HTTP 请求头、URL 信息、服务器变量等编码为键值对传入 - Nginx 发送空的
FCGI_PARAMS记录,标志参数传递结束 - Nginx 发送
FCGI_STDIN(GET 请求体为空,直接发送空记录结束) - php-fpm 执行 PHP 脚本,将输出通过
FCGI_STDOUT返回 - php-fpm 发送
FCGI_END_REQUEST,Nginx 将响应转发给客户端
值得注意的是,FastCGI 协议中同一个连接可以复用多个请求 ID,但在 Nginx + php-fpm 的实践中,通常一个连接同时只处理一个请求。
三、php-fpm 进程模型深度剖析
3.1 php-fpm 的进程层级
php-fpm 采用 Master-Worker 多进程模型:
php-fpm (Master 进程)
├── Worker 进程 1 → 处理 FastCGI 请求
├── Worker 进程 2 → 处理 FastCGI 请求
├── Worker 进程 3 → 处理 FastCGI 请求
└── ... → 处理 FastCGI 请求
Master 进程的职责:
- 监听 FastCGI 端口 / Unix Socket
- 管理 Worker 进程的生命周期(创建、销毁、重启)
- 接收 Nginx 信号并平滑重载配置
- 不直接处理任何请求
Worker 进程的职责:
- 竞争 Accept 新连接(通过锁机制避免惊群)
- 读取 FastCGI 协议数据,解析参数
- 执行 PHP 脚本,返回结果
- 处理完毕后继续等待下一个请求
3.2 三种进程管理策略
php-fpm 的 pm 配置决定 Worker 进程的管理方式:
static(静态):启动时创建固定数量的 Worker,永不增减。适合内存充足、流量稳定的生产环境,避免了动态伸缩的开销。
dynamic(动态):根据负载动态调整 Worker 数量,核心参数包括 pm.max_children(最大进程数)、pm.start_servers(启动进程数)、pm.min_spare_servers(最小空闲数)、pm.max_spare_servers(最大空闲数)。空闲进程过多时销毁,不足时创建。
ondemand(按需):初始不创建 Worker,有请求时才 fork。适合低流量场景,节省内存,但首次请求有 fork 延迟。
3.3 请求的生命周期
一个 FastCGI 请求进入 php-fpm 后的完整流程:
- Accept 连接:Worker 进程通过
accept()获取新连接 - 读取协议数据:解析
FCGI_BEGIN_REQUEST、FCGI_PARAMS、FCGI_STDIN - 初始化请求环境:设置
$_GET、$_POST、$_SERVER等超全局变量 - 执行 PHP 脚本:通过
php_execute_script()进入 Zend 引擎 - 输出缓冲:脚本输出写入 FastCGI 响应缓冲区
- 发送响应:通过
FCGI_STDOUT返回给 Nginx - 清理资源:重置全局状态,关闭请求相关资源
- 等待下一个请求:Worker 回到 Accept 循环
3.4 进程数与并发能力的关系
php-fpm 的并发处理能力直接受 pm.max_children 限制。每个 Worker 进程同时只能处理一个请求,因此:
最大并发数 = pm.max_children
如果所有 Worker 都在忙碌,新请求会排队等待,直到有 Worker 空闲。如果队列也满了(受 listen.backlog 限制),Nginx 会收到连接拒绝,返回 502 错误。
因此,调优的核心是合理设置 pm.max_children:过小会导致请求排队,过大则消耗过多内存(每个 PHP 进程约占 20-50MB)。经验公式:
pm.max_children = 可用内存 / 单个 PHP 进程平均内存占用
四、Nginx 侧关键配置与调优
4.1 upstream 与 fastcgi_pass
location ~ \.php$ {
fastcgi_pass unix:/run/php-fpm.sock; # 或 127.0.0.1:9000
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
Unix Socket 比 TCP 少一层网络协议栈开销,本机通信性能更优,但无法跨主机。TCP 适合 php-fpm 与 Nginx 分离部署的场景。
4.2 关键调优参数
fastcgi_connect_timeout:与 php-fpm 建立连接的超时fastcgi_read_timeout:等待 php-fpm 响应的超时,慢脚本需适当调大fastcgi_buffer_size/fastcgi_buffers:响应缓冲区大小,过小会导致磁盘缓冲fastcgi_keep_conn on:保持与 php-fpm 的长连接,减少连接建立开销
4.3 502 错误的根因排查
502 Bad Gateway 通常意味着 Nginx 无法从 php-fpm 获取有效响应,常见原因:
- php-fpm 进程未启动或崩溃
pm.max_children耗尽,所有 Worker 忙碌且队列满- php-fpm 执行超时,被 Nginx 断开
- Socket 文件权限问题
- PHP 脚本致命错误导致 Worker 异常退出
五、总结
Nginx 与 PHP 的通信本质是两个独立进程通过 FastCGI 二进制协议进行请求-响应式交互。Nginx 负责高并发连接管理和静态资源处理,php-fpm 负责 PHP 脚本执行。理解 FastCGI 协议的记录结构和 php-fpm 的 Master-Worker 进程模型,是排查 502 错误、调优并发能力和设计高可用架构的基础。掌握这些底层原理,才能在实际运维中做到有的放矢,而非盲目调参。
未经允许不得转载:任鹏个人博客 » PHP 与 Nginx 通信原理:FastCGI 协议内核解析与 php-fpm 进程模型深度精讲

