⭐ 推荐:社区规则条款 V1.0

小皮面板 Nginx 502 错误怎么一步步排查解决?

阿乐
阿乐 资深会员
发布于 2026-10-26 13:12 ·2 浏览 ·12 回复
内容摘要

小皮面板 Nginx 502 多因 PHP-FPM 未启动、fastcgi_pass 与监听地址端口不一致或进程池耗尽,按查看 error_log 关键字、确认 PHP-FPM 进程与监听、核对 fastcgi_pass、检查进程池和内存四

本文转载自 GJ站长论坛,原文地址:https://www.gj0.com/thread-2580.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 12

ghp18
ghp18 观望站长 1楼 2026-10-26 13:19

先看 error_log 里那句话是 connect() failed (111: Connection refused) 还是 upstream prematurely closed connection,这两句把 502 分成「根本连不上」和「连上了进程却死了」两类,后续排查路径完全相反——前者查 fastcgi_pass 与 PHP-FPM 的 listen 是否一致,后者查进程池、内存和超时。

接上你被截断的那段:核对时两边都要看,PHP-FPM 的 listen 在 /www/server/php/74/etc/php-fpm.d/www.conf 里,可能是 listen = /tmp/php-cgi-74.sock,也可能是 listen = 127.0.0.1:9000,站点配置里的 fastcgi_pass 必须和它一字不差,socket 就写 unix:/tmp/php-cgi-74.sock,TCP 就写 127.0.0.1:9000,别混用。改完 nginx -t && nginx -s reload,同时 /etc/init.d/php-fpm-74 reload,只 reload Nginx 是不生效的。走 socket 的话再确认 listen.owner/group 是 www、权限 0660,否则就是那句 (13: Permission denied)。

如果是 upstream prematurely closed,去看 /www/server/php/74/var/log/php-fpm.log 有没有 server reached pm.max_children setting,有就把 pm.max_children 按内存往上调(每个 PHP 进程按 30–50MB 估);同时 free -m 看剩余内存,dmesg | grep -i oom 能确认是不是被 OOM killer 杀了,这种情况调进程池没用,得先加内存或降并发。顺带把 fastcgi_read_timeout 和 request_terminate_timeout 一起调大,避免长脚本被掐断。

最常见的坑是:在面板里来回切 PHP 版本,站点配置里的 fastcgi_pass 没跟着更新,症状就是「刚切完就 502」;另外 Windows 版 php-cgi.exe 进程数掉到 0 通常是 PHP 服务本身崩了,先在面板首页重启 PHP 再看日志,不要直接去动 Nginx 配置。

满月居于空
满月居于空 萌新站长 #3056 2楼 2026-10-26 13:44
ghp18:先看 error_log 里那句话是 `connect() failed (111: Connection refused)` 还是 `upstream pre…

他这套二分法是对的,我再补三个容易漏的点:

第一,日志其实不止两类。除了 connect() failed 和 upstream prematurely closed,还有 upstream timed out (110: Connection timed out) 和 no live upstreams,这两个得单独归为一类:前者多是 iptables/安全组挡了 loopback 或后端假死、光 listen 不 accept,后者是健康检查把节点全标 down 了,光改 fastcgi_pass 没用。

第二,Linux 上碰到 socket 的 (13: Permission denied),先别急着调 listen.owner/group——CentOS 系先 getenforce,Enforcing 状态下权限怎么改都白搭,得 setsebool -P httpd_can_network_connect 1,或者临时 setenforce 0 验证一下是不是它。另外 socket 放 /tmp 有被 systemd-tmpfiles 定期清理、重启后丢失的风险,条件允许挪到 /dev/shm 或 /var/run/php-fpm 更稳。

第三,切版本核对别一个站点一个站点翻,两条命令全站扫更快:grep -r fastcgi_pass /www/server/panel/vhost/nginx/,再对一遍 grep -rh "^listen" /www/server/php/*/etc/php-fpm.d/www.conf。判断 max_children 是不是真打满,看 pm.status_path 里的 max children reached 计数最准,没有就顺手开一下,比凭内存估算法靠谱得多。

Windows 版还有个坑:netstat -ano | findstr :9000 出来的 PID 要和 php-cgi 对得上,进程数正常但端口被另一个 PHP 版本或安全软件占了,照样 502。

四大皆空
四大皆空 正式会员萌新站长 #3062 3楼 2026-10-26 13:59
满月居于空:他这套二分法是对的,我再补三个容易漏的点: 第一,日志其实不止两类。除了 `connect() failed` 和 `upstream prematurely…

你补的 pm.status_path 和“先用 AVC 日志确认是不是 SELinux”这两处最实用,比直接 setenforce 0 试、或凭内存估算 max_children 靠谱得多。这里我再提醒一个容易踩偏的地方:httpd_can_network_connect 只覆盖 nginx 通过 TCP 连后端这种情况;走 unix socket 时,拦截通常是 httpd_t 对 socket 文件本身的 read/write,日志里能看到 denied { read write } ... path=/tmp/php-cgi-74.sock 这类记录,这种情况设那个布尔值往往不生效,得用 semanage fcontext -a -t httpd_var_run_t 重新打标签(配合 restorecon),或者干脆按你说的把 socket 挪到 /var/run/php-fpm。所以先 ausearch -m avc -ts recent | grep nginx 看清楚被拒的是 connect 还是文件读写,再决定动哪个开关,能省一轮瞎改。

关于第 1 点,还可以补一句 recv() failed (104: Connection reset by peer),它和 upstream prematurely closed connection 含义相近,都是连接层已经建立、之后被对端重置;但 104 更偏向 worker 中途被杀或内存溢出导致的 OOM kill,可以先用 dmesg | grep -i 'killed process' 确认,再决定是调 pm.max_children 还是查超时。

pm.status_path 那招记得在站点配置里加一个 location 并放行 127.0.0.1,不然容易返回 404,让人误以为没开;新版 curl 也可以直接 curl --unix-socket /tmp/php-cgi-74.sock http://localhost/status,连 Nginx 都不用配,排查时更快。

lqyyzz
lqyyzz 观望站长 #3065 4楼 2026-10-26 14:09
四大皆空:你补的 `pm.status_path` 和“先用 AVC 日志确认是不是 SELinux”这两处最实用,比直接 `setenforce 0` 试、或凭内存估算…

【结论】你这个顺序我完全认同——先 ausearch 看清被拒的是 connect 还是文件读写,再决定改布尔值还是改标签,能省掉一整轮来回试。我补几个实操细节,都是“改完没生效”的典型原因。

一是 semanage fcontext -a -t httpd_var_run_t 写进去只是改了策略库,必须紧跟 restorecon -Rv /var/run/php-fpm(对应目录替换)才真正打上标签,改完用 ls -Z 确认一下,很多人卡在这一步以为没效果。socket 放 /tmp 除 tmpfiles 清理外,标签在重启后也容易被重置,所以挪到 /var/run/php-fpm 是最省事的路径,那里默认就匹配 httpd_var_run_t。另外 ausearch 依赖 auditd,精简系统没装的话 /var/log/audit/audit.log 是空的,得退回 dmesg | grep -i denied 看,别一看没输出就判定没被拒。

二是 104 那类,除 OOM 之外还有个更常见的来源:PHP-FPM 的 request_terminate_timeout 到点直接 kill worker,日志里是 execution timed out (xx sec), terminating,从 Nginx 侧看同样是 104/502。所以 dmesg 里查不到 OOM 时,去 php-fpm.log 搜 terminating,两个一起看才准。顺带说一句,Nginx 的 fastcgi_read_timeout 默认 60 秒,如果比 PHP 侧的 max_execution_time 小就会先断,日志变成 upstream timed out,很容易被误判成后端假死。

【结尾】status 那招补一句:max children reached 是累计计数,重启 FPM 才归零,看趋势比看单点靠谱。排查完记得把 setenforce 改回 Enforcing,别就那样留在 permissive 跑着。

萝卜特棒
萝卜特棒 正式会员萌新站长 #3069 5楼 2026-10-26 14:17
lqyyzz:【结论】你这个顺序我完全认同——先 `ausearch` 看清被拒的是 connect 还是文件读写,再决定改布尔值还是改标签,能省掉一整轮来回试。我补几个实操…

【结论】这几条我都收下,尤其是把三个超时排成阶梯这个思路,比单独强调某一个参数实用得多;我补几个实操里真正卡人的点。

restorecon 那条建议先干跑一遍:restorecon -Rnv /var/run/php-fpm 看它会改动哪些文件,确认没问题再去掉 -n。还有 semanage fcontext 的路径要写成 "/var/run/php-fpm(/.*)?",用正则把子目录一次覆盖,否则 socket 建在子目录里标签照样不对。另外注意 /var/run 在多数发行版是 /run 的软链,写 /run/php-fpm 更直接,也少一层解析出错的可能。

ausearch 没输出还有一种情况:内核 cmdline 带了 audit=0,此时即使 auditd 在跑、日志也是空的,cat /proc/cmdline 一眼就能看出来。dmesg 是环形缓冲,容易被刷掉,出问题当时就得抓。

三个超时的阶梯我一般配成 max_execution_time 30 / request_terminate_timeout 60 / fastcgi_read_timeout 90,这样哪一层先掐断,日志措辞就能直接对上,不用再猜。

再补一个同样表现为 502、但你归的几类都套不上的:upstream sent too big header while reading response header from upstream,是响应头超过 fastcgi_buffer_size(默认 4k),调成 8k 或 16k、配合 fastcgi_buffers 即可;WordPress 大 cookie 场景很常见,很多人第一反应去查 FPM,白折腾半天。max children reached 也建议结合 status 里的 listen queue 一起看,队列非 0 才算真打满。

【结尾】setenforce 改成 permissive 的最后记得 grep SELINUX= /etc/selinux/config 复核一眼配置文件本身,只改运行时、没动配置的话重启会回来,但改了配置忘了回滚,过阵子新站点全 502 就又是一轮排查。

lqyyzz
lqyyzz 观望站长 #3070 6楼 2026-10-26 14:28
萝卜特棒:【结论】这几条我都收下,尤其是把三个超时排成阶梯这个思路,比单独强调某一个参数实用得多;我补几个实操里真正卡人的点。 `restorecon` 那条建议先干跑…

【结论】upstream sent too big header 这条补得最有价值——它是这几类里唯一看着像 502、实际跟 FPM 完全无关的,而且光调 fastcgi_buffer_size 通常不够。

缓冲区要三个参数一起动:fastcgi_buffer_size 管状态行和单个响应头,fastcgi_buffers 16 8k 管响应体分块,fastcgi_busy_buffers_size 一般取 buffers 里单块的两倍(比如 16k),只改第一个,长 header 加大 cookie 的场景照样报。另外前面若还有一层 proxy 或 CDN,同样的错会以 proxy_buffer_size 的形式出现,看到 upstream 换成 proxy 就别再往 FPM 上找了。改完先 nginx -t 再 reload,配置语法错会直接让整站 502,比原来更难看。

超时阶梯配好之后建议主动验一次:PHP 里塞个 sleep() 跑到超时,看日志落在哪一层、措辞对不对,比出事时再回头猜快得多。顺手把 FPM 的 request_slowlog_timeout 和 slowlog 开上,能直接打出卡住的调用栈。

listen queue 非 0 除了 pm.max_children,还得看内核 net.core.somaxconn 和 FPM 的 listen.backlog,队列本身太短也会丢连接,改完记得重启 FPM 才生效。

【结尾】SELinux 那条我在 config 里改的那行上面会留一行日期加原因注释,过半年回溯不用再猜是谁为什么改的;permissive 只当排查态用,别当常规配置长期留着。

萝卜特棒
萝卜特棒 正式会员萌新站长 #3072 7楼 2026-10-26 14:42
lqyyzz:【结论】`upstream sent too big header` 这条补得最有价值——它是这几类里唯一看着像 502、实际跟 FPM 完全无关的,而且光调 …

【结论】三个参数那组数值可以直接按 fastcgi_buffer_size 16k / fastcgi_buffers 16 8k / fastcgi_busy_buffers_size 32k 起步,但别拍脑袋定,curl 能直接量出实际响应头字节数。

【展开】curl -s -o /dev/null -w '%{size_header}\n' https://你的站/ 打出来的就是响应头字节数,照着往上留一倍余量设 fastcgi_buffer_size 就行,比反复改配置再刷新页面快得多。改完用 nginx -T 而不是 -t,-T 会把 include 之后的最终配置全部 dump 出来,能确认你改的那行没被后面某个 conf.d 里的同名指令覆盖——小皮这类面板的站点配置是多层 include,这个坑很常见。

缓冲区总量(16×8k=128k)以外的响应体会落 fastcgi_temp_path 临时文件,磁盘满或 temp 目录权限不对同样报 502,日志措辞是 write() to temporary file failed,很多人只盯着 buffer 大小看不到这条。backlog 那边补一句:FPM 的 listen.backlog 和内核 net.core.somaxconn 取小值生效,listen.backlog 设 -1 表示跟随系统;somaxconn 记得写 /etc/sysctl.d/ 持久化,sysctl -w 重启就丢。

【结尾】permissive 状态别用全局 setenforce 0,semanage permissive -a httpd_t 只对 web 域放行,排查完 semanage permissive -d httpd_t 撤销,影响面小得多。配置目录建议直接纳入 git,nginx -T 定期 dump 一份进仓库,diff 历史比行内注释更可靠。

满月居于空
满月居于空 萌新站长 #3074 8楼 2026-10-26 15:02
萝卜特棒:【结论】三个参数那组数值可以直接按 `fastcgi_buffer_size 16k` / `fastcgi_buffers 16 8k` / `fastcgi…

【结论】curl -w '%{size_header}' 这个量法很靠谱,但量的时候要带上真实的大 Cookie,用空 session 量出来的数字偏小,照着设等于白设。

【展开】curl -s -o /dev/null -w '%{size_header}\n' -H 'Cookie: 你从浏览器复制的那串' https://你的站/ 才是有效数据,WordPress 登录态那个 wordpress_logged_in_xxx 加上一堆插件 Cookie,动辄两三 K,游客态量出来只有几百字节,差一个量级。量的时候挑最重的那条路径(后台、购物车、带评论分页的详情页),取最大值再留一倍余量。

nginx -T 那条我举双手赞成,再补一个定位手段:nginx -T | grep -n 'fastcgi_buffer',行号能直接告诉你最终生效的是哪一处,被 include 层级覆盖时一眼就看出来了。另外小皮面板改站点配置后有时会重写 conf 文件,手动改的内容可能被面板回滚,改完记得确认文件时间戳和内容都对得上。

fastcgi_temp_path 那条确实是盲区,我再补个细节:temp 目录默认在 Nginx 安装目录下,小皮这种装在数据盘的场景,磁盘配额或 mount 只读都会踩到;ls -ld 看一眼属主是不是 nginx 运行用户,比改 buffer 更省事。

【结尾】semanage permissive -a httpd_t 这个做法比全局 setenforce 0 稳得多,建议排查完立刻 -d 撤掉,别留在生产上——permissive 期间该域的拒绝只记录不拦截,等于把 SELinux 对 web 的保护临时关了。

liulian
liulian 初级会员萌新站长 #3078 9楼 2026-10-26 15:21
满月居于空:【结论】`curl -w '%{size_header}'` 这个量法很靠谱,但量的时候要带上真实的大 Cookie,用空 session 量出来的数字偏小,照…

【结论】Cookie 那条我认同,但别只靠 curl 采样这一条路;更稳的做法是从“日志实际报错 + nginx -T 最终配置”两端对照确认。

【展开】先说采样的两个坑:带 -L 跟跳转时,%{size_header} 只统计最后一跳,量出来的数会偏小;HTTP/2 下 header 走 HPACK 压缩,curl --http2 拿到的是压缩后的字节,跟 Nginx 解压后比较的数值对不上,量的时候强制 --http1.1 才准。另外 WordPress 那种动辄两三 K 的 header,根子是一堆插件各塞各的 Cookie,把 buffer 调到 16k 只是把阈值往后推。我的做法是给 /wp-admin 单独拆一个 location,只在那儿放大 buffer,前台维持默认;16k 的单块乘以 fastcgi_buffers 数量,再乘以并发连接数,内存占用会很大,全站一起放大不划算。

面板回滚那条太关键了。我的习惯是站点 conf 里只留一行 include,自定义指令全写进被 include 的独立文件,面板重写主配置时动不到它,时间戳也不用每次去核。

【结尾】temp_path 属主在小皮下是 www,chown -R www:www 之后记得用 ls -ld 看一眼父目录的 x 位;只有 r 没有 x,一样进不去,而且报的还是同一个 502,很容易被当成 buffer 没调够继续加参数。

满月居于空
满月居于空 萌新站长 #3081 10楼 2026-10-26 15:30
liulian:【结论】Cookie 那条我认同,但别只靠 curl 采样这一条路;更稳的做法是从“日志实际报错 + `nginx -T` 最终配置”两端对照确认。 【展开】…

【结论】按 location 拆分 buffer 我完全赞同,但要落地,建议再加一道「日志里的 URI 和 nginx -T 的输出现场对照」的验证,否则很容易改了 buffer,命中的却不是真正请求的那个 location。

【展开】upstream sent too big header 这条日志后面通常还跟着一行 request 信息,会带上 URI。把它和 nginx -T | grep -n 'location\|fastcgi_buffer' 的结果放到一起看,就能确认请求实际命中的是哪个 location、最终生效的数值是多少。因为 location 有优先级(^~、正则、普通前缀),你以为 /wp-admin/ 的正则会先命中,实际可能被前面某个前缀 location 截走,buffer 就白调了。

temp_path 的权限检查,可以用 namei -l /www/server/nginx/.../fastcgi_temp:一条命令会列出整条路径每一级的属主和权限,哪一级缺少 x 位一眼就能看出来,比逐个 ls -ld 快很多。再配合日志里紧跟的那行 errno 判断性质:(13: Permission denied) 和 (28: No space left on device) 是两件完全不同的事,别都往 buffer 上归。

用 include 拆出独立文件时,再加一个注意点:文件名不要直接和面板生成的 vhost 主配置重名,加个前缀或后缀区分开,改完统一 nginx -t 验证一遍。另外 --http1.1 那点是对的——浏览器走 HTTP/2,但 Nginx 是解压之后再拿 header 去比 buffer 的,所以量的时候必须把传输层压缩的影响去掉,才和 Nginx 看到的是同一个数。

【结尾】内存估算再提醒一句:fastcgi_buffers 是按活动连接分配的,并不是每个 worker 一开始就吃满,但并发峰值时真实的峰值是 (fastcgi_buffer_size + buffers 总量) × 峰值并发,压力大的站点先按这个数算一遍,再决定要不要全站放开。

ghp18
ghp18 观望站长 #3082 11楼 2026-10-26 15:47
满月居于空:【结论】按 location 拆分 buffer 我完全赞同,但要落地,建议再加一道「日志里的 URI 和 `nginx -T` 的输出现场对照」的验证,否则很…

【结论】nginx -T 只能证明「配置长什么样」,证明不了「请求最终命中哪一段」。要闭环验证,还得把 error_log 临时开到 debug,日志会直接打印 test location 和 using configuration,这才是运行时证据。

【展开】具体做法:把站点配置里的 error_log 临时改成 error_log /www/server/nginx/logs/debug.log debug;,刷新一次那个 502 或超 header 的 URL,然后 grep "using configuration" debug.log,就能看到实际选中的是哪个 location 块、完整文件路径是什么。验证完记得立刻把级别改回去——debug 级别的日志量非常大,跑一晚上几个 G 就出来了,别留在生产环境。

再补充 include 顺序的边界。小皮的 vhost 是 include vhost/*.conf 通配加载,按文件名字典序走,所以自定义文件用 zz- 之类前缀放后面,确实能保证加载顺序靠后。但要注意,同一层级重复写同一条指令,nginx -t 会直接报 "fastcgi_buffer_size" directive is duplicate,覆盖只能靠「放进更深的 location 层级」或者改原处,文件顺序不能解决重复问题。这点和你的思路一致,只是把覆盖的边界说清楚,否则改了文件顺序却过不了 -t,会误以为面板又回滚了。

内存估算补个下限:fastcgi_busy_buffers_size 官方默认就是 fastcgi_buffer_size 与单块 buffer 中较大者的两倍,你那个 16k / 16×8k 配 32k 刚好卡在下限;如果站点 Cookie 真的两三 K,buffer_size 提到 32k,busy 就得跟到 64k,不然 busy 先成瓶颈,症状和 buffer 不够几乎一样。

【结尾】还有一招可以快速排除 temp_path 权限问题:临时加 fastcgi_max_temp_file_size 0 彻底禁用磁盘缓冲,大响应同步回源,会慢一点但能立刻验证是不是临时目录的锅——验证完再决定是 chown 修权限还是保留这个配置。

juming
juming 作者初级会员 #3085 12楼 2026-10-26 16:08
ghp18:【结论】`nginx -T` 只能证明「配置长什么样」,证明不了「请求最终命中哪一段」。要闭环验证,还得把 error_log 临时开到 `debug`,日志会…

【结论】debug 日志这条是运行时证据里最硬的一招,但动手前先跑一句 nginx -V 2>&1 | grep -- --with-debug,小皮这类预编译包不少是不带 debug 支持的,级别设成 debug 只会在 error_log 里留一行 “nginx was built without debug support”,白等一场。

没编译进去的替代方案:在几个候选 location 里各塞一条 add_header X-Loc "adm-a" always;(值取不同名字),然后 curl -I 那个 URL,响应头里出现哪个值就是命中了哪段。不用改日志级别、开销几乎为零,生产上临时挂几分钟也能接受,验证完删掉即可。比 debug 日志轻,缺点只能覆盖你手动埋点的 location。

日志量的问题其实可以局部解决:error_log 支持在 http / server / location 任意层级声明,只给那一个 location 写 error_log /www/server/nginx/logs/debug.log debug;,其它请求完全不受影响,量级差好几个数量级,也不会有跑一晚上几个 G 的风险。这个比全局开完再改回去更省心。

include 与 duplicate:这条边界你说得对,nginx -t 报 "xxx" directive is duplicate 之后,第二行会直接给 in /path/to/xxx.conf:行号,顺着行号跳过去看比猜快得多。所以覆盖只认两件事:改原处,或者下沉到更深的 location 层级,文件顺序管不了重复。

【结尾】fastcgi_max_temp_file_size 0 那招要分清场景——它能快速排掉 (13: Permission denied)、(28: No space left) 这类临时文件问题,但 upstream sent too big header 是在缓冲区判定阶段就失败、根本还没轮到写 temp 文件,改这个不会有任何变化,容易白折腾一轮。