nginx 自己不会跑 PHP。它把请求转发给 PHP-FPM,等结果,再回给浏览器。中间这段用的是 FastCGI 协议。
大多数人配 enable-php-83.conf 就完事了,出问题只会看到两个词:502 Bad Gateway 和 File not found。这篇把中间那条链路拆开,搞清楚谁在等谁。
一、请求进来之后的三步
浏览器 ──HTTP──> nginx ──FastCGI──> php-fpm ──> 磁盘上的 .php
│ │
└── 等结果 ◄───────┘- nginx 解析 URI,匹配
location,发现是.php,决定不自己处理 - nginx 按
fastcgi_pass的地址建立连接,用 FastCGI 协议发一段二进制 - FPM 的 worker 进程收到,解析参数,执行脚本,把输出流回来
关键在于第 2 步:nginx 交给 FPM 的不是 HTTP 请求,而是一组键值对。 里面既没有请求行,也没有原始报文,只有 REQUEST_URI、SCRIPT_FILENAME 这类 CGI 变量,加上请求体。

二、那段二进制长什么样
FastCGI 把数据切成「记录(record)」,每条记录的头固定 8 字节:
| 字段 | 长度 | 说明 |
|---|---|---|
| version | 1 | 固定 1 |
| type | 1 | 1=BEGIN_REQUEST 4=PARAMS 5=STDIN 3=END_REQUEST |
| requestId | 2 | 同一连接上多路复用,nginx 用 1 |
| contentLength | 2 | 数据长度 |
| paddingLength | 1 | 填充长度,按 8 字节对齐 |
| reserved | 1 | 保留 |
一条 PHP 请求大致是这么走完的:
BEGIN_REQUEST role=RESPONDER, flags=0
PARAMS SCRIPT_FILENAME=/www/wwwroot/app/index.php
PARAMS REQUEST_METHOD=GET
PARAMS QUERY_STRING=id=1
PARAMS ...(几十个变量,可能切多条记录)
PARAMS 空记录 ← 参数结束
STDIN 请求体(GET 时是空记录)
STDOUT 响应头 + 正文
END_REQUEST appStatus=0, protocolStatus=REQUEST_COMPLETE PARAMS 的编码很省:每项是「名字长度 + 值长度 + 名字 + 值」,长度小于 128 时各占 1 字节,否则占 4 字节(最高位置 1)。所以 SCRIPT_FILENAME=/www/wwwroot/app/index.php 这一项大约 50 字节就传完了。
FPM 收到后把这些变量塞进 $_SERVER,然后 chdir 到脚本目录、include 那个文件。从这个点开始,才是你写的 PHP 在跑。
三、SCRIPT_FILENAME 才是主角
这是最容易出错的一个参数,因为它决定了「跑哪个文件」,和 URL 没有直接关系:
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;拆开看:$document_root 来自 root 指令,$fastcgi_script_name 是 URI 里那段 .php 路径。两者拼起来就是磁盘路径。
如果这一行漏了或者拼错了,你会看到两种报错,含义不同:
| 现象 | 含义 |
|---|---|
nginx 日志 Primary script unknown | nginx 侧就没算出路径,$document_root 是空的 |
浏览器看到 File not found. | FPM 收到了路径,但那文件不存在或权限不对 |
File not found. 是 FPM 自己吐的,说明协议已经通了 —— 这能帮你把问题定位到「路径 / 权限」而不是「连接」。
顺带提一个历史遗留问题:cgi.fix_pathinfo。
PHP 默认(很多发行版打包时改了)是 1,此时请求 /upload/evil.jpg/x.php,FPM 找不到 x.php 会沿着路径往前找,最后执行 evil.jpg。这就是著名的解析漏洞,能让上传的图片变成可执行脚本。
正确做法:
; php.ini
cgi.fix_pathinfo = 0同时在 nginx 里明确要求路径必须存在:
# 只有真实存在的 php 文件才转发
location ~ \.php$ {
try_files $uri =404;
include fastcgi.conf;
fastcgi_pass unix:/tmp/php-cgi-83.sock;
}try_files $uri =404 这一行是很多安全扫描的必查项,加上它之后,不存在的路径根本进不到 FPM。
四、FPM 那边:一个 master,一堆 worker
php-fpm(master)
├── worker #1 ← 真正执行 PHP
├── worker #2
└── worker #3master 进程自己不跑业务代码,职责只有四个:绑定 socket、fork worker、收信号、回收挂掉的 worker。所以你看 ps 会发现有一堆 php-fpm: pool www。
worker 数量的控制就是 pm 三选一:
| 模式 | 行为 | 适用 |
|---|---|---|
static | 启动就固定 N 个,永不增减 | 内存充足、流量稳定的生产机 |
dynamic | 按负载在 min/max 之间伸缩 | 大多数场景的默认选择 |
ondemand | 零请求时全部退出,来了再 fork | 低流量、内存紧张的机器 |
dynamic 的伸缩有延迟:突发流量到来时,FPM 已经按 pm.start_servers fork 了一批在等,如果不够,就要等 pm.process_idle_timeout 后逐个补 —— 这期间请求会堆在 backlog 里。
pm.max_children 怎么算
不要凭感觉填 100。正确的算法是:
max_children = 可用内存 ÷ 单个 worker 的常驻内存先量一下实际 RSS:
ps -o rss= -C php-fpm8.3 | awk '{s+=$1; n++} END {printf "平均 %.1f MB, 共 %d 个\n", s/n/1024, n}'假设平均 40 MB,机器给 PHP 可用 2 GB,那就是 2048 / 40 = 51,留 20% 余量取 40。
填太大会 OOM Killer 上门,填太小则请求排不上队直接 502。这个数只能靠实测,不同项目的 RSS 差几倍很正常。
还有两个配套参数容易被忽略:
pm.max_requests = 500 ; 每个 worker 处理 500 次后重启,防止内存缓慢泄漏
request_terminate_timeout = 60s ; 单请求墙钟超时,兜底防卡死pm.max_requests 是最便宜的稳定性保险:哪怕代码有轻微内存泄漏,worker 定期重启就能把它洗干净。
五、502 和 504,到底是谁的锅
这两个错误码经常被混为一谈,实际上完全不是一回事。
502 Bad Gateway:nginx 没能从 FPM 拿到一个完整的响应。常见四种:
| 成因 | 特征 |
|---|---|
| worker 全忙,连接排满 backlog | 高峰期出现,日志有 connect() failed 或 Connection reset by peer |
| FPM master 挂了 | 全站 502,systemctl status php-fpm 看得到 |
| socket 权限 / 路径不对 | 一直 502,ss -lx 看不到那个 sock |
| worker 处理时被信号杀掉 | 偶发 502,FPM 日志有 child exited on signal 11(段错误) |
504 Gateway Timeout:FPM 还在跑,但 nginx 等不下去了。这是超时问题,不是崩溃。

三个超时参数各管一段,必须按 nginx < FPM 的顺序排:
max_execution_time (PHP 层,只算 CPU 时间,不计 IO 等待)
<
request_terminate_timeout (FPM 层,墙钟,到点直接 SIGTERM worker)
<
fastcgi_read_timeout (nginx 层,等上游响应的上限)比如 PHP 设 30s、FPM 设 60s、nginx 设 90s。这样最坏情况是 PHP 自己先报错退出,用户能看到一个正常的错误页,而不是白屏的 504。
反过来的配置最坑:nginx 设 30s,PHP 设 60s。用户等到 30 秒看到 504,但脚本还在后台继续跑,写了一半的数据留在库里。
还有一条容易被忽略:max_execution_time 在 Linux 上不包含数据库查询、文件读写、curl 等待的时间(因为计时器只在 CPU 密集时触发)。所以「跑 10 分钟的慢 SQL」不会被它拦住,只有 request_terminate_timeout 才管得住。
六、一份真实配置
下面是我自己站点(nginx + PHP 8.3 + 宝塔面板)在用的配置,去掉证书部分:
server {
listen 443 ssl;
http2 on;
server_name ly1314.com www.ly1314.com;
root /www/wwwroot/ly1314.com;
index index.php index.html;
# 安全响应头
server_tokens off;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# 动态 sitemap,必须放在 rewrite 包含之前
rewrite ^/sitemap\.xml$ /sitemap.php last;
# 屏蔽敏感文件
location ~ ^/(\.env|\.git|composer\.(json|lock)|package\.json) {
return 404;
}
# PHP:交给 enable-php-83.conf 里的 location 处理
include enable-php-83.conf;
# Typecho 的伪静态
include /www/server/panel/vhost/rewrite/ly1314.com.conf;
}enable-php-83.conf 是面板生成的,展开后核心就是:
location ~ [^/]\.php(/|$) {
fastcgi_pass unix:/tmp/php-cgi-83.sock;
fastcgi_index index.php;
include fastcgi.conf;
include pathinfo.conf;
}几个可以立刻改进的点:
用 unix socket 而不是 TCP。 unix:/tmp/php-cgi-83.sock 省掉了 loopback 的 TCP 握手和校验和计算,小请求下能省 0.1~0.3 ms。注意 sock 文件和 nginx、php-fpm 的进程用户要一致,不然会 Permission denied。
给连接加 keepalive。 FPM 支持长连接,但 nginx 默认每次新建一条:
upstream php_fpm {
server unix:/tmp/php-cgi-83.sock;
keepalive 32;
}
location ~ \.php$ {
fastcgi_pass php_fpm;
fastcgi_keep_conn on; # 关键
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}要同时写 fastcgi_keep_conn on,否则 upstream 里的 keepalive 不生效 —— 这是个很常见的「配了没用」的坑。
静态资源不要进 PHP 的 location。 图片、CSS 的 location 要写在 PHP 那条前面,或者用 ^~ 修饰符,否则正则匹配顺序会让它们也被转发给 FPM。
七、小结
- nginx 交给 FPM 的是一组 CGI 变量,不是原样的 HTTP 报文;载体是 FastCGI 的 8 字节头 record
SCRIPT_FILENAME决定跑哪个文件,和 URL 无直接关系cgi.fix_pathinfo = 0+try_files $uri =404是最低限度的上传漏洞防护pm.max_children用「可用内存 ÷ 实测 RSS」算,配pm.max_requests当内存保险- 502 是拿不到响应(worker 不够 / 进程挂了 / socket 不通),504 是等太久(超时链没排好)
- 三个超时按
max_execution_time < request_terminate_timeout < fastcgi_read_timeout排
下次再看到 502,先别重启服务 —— 看一眼 FPM 的 error log 和 ps aux | grep php-fpm 的数量,基本三分钟就能定位。



































































































































