50X错误全解析:从502到504的排查与修复指南

发布时间:2026/10/19 6:58:45
50X错误全解析:从502到504的排查与修复指南
凌晨两点报警群的提示音响起来。打开手机一看“xx电商项目前端接口502持续恢复中”。拖着拖鞋跑到电脑前ssh连上去nginx错误日志刷了一屏connect() failed (111: Connection refused) while connecting to upstreamphp-fpm进程全没了。这个场景做PHP后端的人多少都经历过。502、500、503、504统称50X错误常年占据“线上故障Top 10”的半壁江山。写这篇文章就是想把50X错误这头“牛”彻底切开。从状态码的语义到请求链路上每个环节可能出的问题再到日志怎么查、配置怎么调、真实故障怎么复盘一次讲透。不管你是刚接手PHP项目的运维还是写了几年业务代码的开发者这篇文章都值得收藏——下次再遇到50X你能少走两小时弯路。1. 50X错误家族全解析1.1 一次故障抢修现场50X错误的典型出场方式先说个我亲身处理的案例。某公司一个社区论坛项目每个月的活动日流量都会翻好几倍。某一个活动日的晚上用户开始集中反馈“网页打不开”技术群里有人贴出了浏览器截图清一色的“502 Bad Gateway”。当时我的第一反应是看php-fpm日志结果发现大量这样的记录WARNING: [pool www] seems busy (you may need to increase pm.start_servers, or pm.min/max_spare_servers), spawning 8 children, there are 1 idle, and 23 total children再看nginx错误日志connect() failed (111: Connection refused) while connecting to upstream, client: 203.0.113.5, server: api.example.com, request: GET /v1/user/info HTTP/1.1, upstream: fastcgi://127.0.0.1:9000两条日志放在一起问题基本就清楚了php-fpm的worker进程被打满新请求进不来或者进程被强制杀掉nginx连不上fastcgi直接返回502。忙活到凌晨三点多才彻底恢复。从那之后我就意识到50X错误不是一个孤立的技术名词它背后是一整套链路问题。不把这条链路拆开故障永远是“凌晨两点半的惊喜”。1.2 50X家族成员状态码的语义边界5开头的状态码统一语义是“服务器端出了问题”。但细分下来每个码代表的“故障层”完全不同。下表是50X家族里最常见几个成员的定位状态码含义典型生成角色用户看到的体验500Internal Server ErrorPHP-FPM / 应用代码页面报错但不至于连不上502Bad Gatewaynginx网关转发失败页面直接打不开503Service Unavailablenginx / PHP-FPM服务挂起或拒绝服务504Gateway Timeoutnginx请求长时间无响应最终超时505HTTP Version Not Supportednginx / 代理层协议版本不匹配极罕见这几个状态码最要命的不是字面含义而是它们之间的“传染性”。一个请求链路里如果PHP-FPM响应慢了nginx可能等不到结果返回504如果PHP-FPM直接崩溃nginx返回502如果FPM还活着但所有worker都在处理慢请求新请求排队nginx转发不进去表现可能是502也可能是503。换句话说状态码只是表象真正的病灶藏在下游。庖丁解牛的第一刀就是先看清每块“骨头”的位置。1.3 HTTP状态码的两层生成机制很多新手会困惑一个问题状态码到底是PHP生成的还是nginx生成的答案是谁处理的请求谁就有资格生成状态码。在典型的PHP-FPM nginx架构里存在两条生成路径。一条路径是PHP应用主动设置。代码里写了http_response_code(500)或者框架抛出没捕获的异常PHP-FPM返回的响应头里就是500。这种500业务属性很强通常伴随着具体的错误页、异常堆栈。另一条路径是nginx被动生成。nginx把请求通过fastcgi协议转发给PHP-FPM结果FPM没响应或者连接被重置或者响应超时nginx等不到一个合法响应就只能自己“编”一个状态码返回给客户端。502、503、504基本都是这么来的。nginx还有一个配置项叫proxy_intercept_errors把它打开后即使FPM返回了500nginx也可以用自定义的错误页替换掉原始响应。搞清楚这个机制排障思路就清楚了凡是带“业务特征”的50X比如错误页里能看到框架的logo往应用层查凡是浏览器直接显示“Bad Gateway”“Gateway Timeout”这种白底黑字往nginx和FPM这层基础设施查。2. 追根溯源一个请求从进入nginx到返回的全过程2.1 请求在PHP-FPM架构中的完整路径要解50X这头牛先得熟悉牛的骨骼结构。一次普通的PHP请求在nginx PHP-FPM的架构下走完是这样的客户端发起HTTP请求nginx的worker进程接收请求解析URI、请求头等信息。nginx根据location规则匹配到fastcgi_pass把请求通过fastcgi协议转发给PHP-FPM监听的地址比如127.0.0.1:9000或unix:/tmp/php-cgi.sock。PHP-FPM的master进程收到新连接从空闲worker池里挑一个worker处理请求。worker进程加载PHP文件执行业务代码期间可能连接MySQL、Redis等下游服务。worker生成响应返回给nginxnginx再把响应转给客户端。这条链路里任何一个环节出问题50X就会出现在浏览器上。真实故障里我踩过几个典型节点第1步出问题通常是nginx配置错误、worker进程崩溃表现为“nginx直接拒绝连接”。第2步出问题常见的是fastcgi监听的socket或端口没起来nginx日志里会写connect() failed。第3步出问题是PHP-FPM的worker耗尽、排队长度超过backlog表现是请求堆积。第4步出问题是PHP代码执行超时、内存溢出、死锁最常见也最难查。第5步出问题通常是PHP进程崩溃或者响应太大。排障的底层逻辑就是顺着这条链路用日志工具一点点确认“哪个环节没干活”。2.2 影响50X出现频率的核心配置项每个环节都有对应的配置项。把这些配置项调明白很多50X问题是可以在发生前就规避掉的。先说PHP-FPM侧的配置集中在php-fpm.conf和pool.d/www.conf里; 进程管理模式 pm dynamic pm.max_children 50 pm.start_servers 10 pm.min_spare_servers 5 pm.max_spare_servers 20 pm.max_requests 1000pm.max_children是最关键的参数它决定了一个FPM池最多同时开多少个worker进程。每个worker进程都会占用内存这个值设大了物理内存不够系统开始使用swap响应变慢进而引发502设小了高峰期所有worker都在忙新请求只能排队FPM的日志里会出现“seems busy”的警告。pm.max_requests则控制worker处理多少请求后自动“退休”目的是防止长期运行的进程内存泄漏。一般建议设成500~2000。再往下是请求超时类参数request_terminate_timeout 60 request_slowlog_timeout 10 request_slowlog_file /var/log/php-fpm/slow.logrequest_terminate_timeout决定了worker执行一个请求的最长耗时超时后直接被master杀掉避免“僵尸worker”占坑。request_slowlog_timeout用于慢日志超过10秒的请求会记录堆栈这是定位504的利器。nginx侧也有对应的超时时间集中在nginx.conf的http、server、location块fastcgi_connect_timeout 5s; fastcgi_send_timeout 60s; fastcgi_read_timeout 60s;fastcgi_read_timeout是nginx等待PHP-FPM返回数据的最长时间。如果PHP-FPM这边设置的是60秒但nginx那边只给了30秒就会造成“PHP还在执行nginx已经返回504”的错位局面。这两个超时时间必须配合着调否则配置优先级就是一场混乱。还有一个常被忽视的配置是listen.backlog。它控制了FPM监听队列的长度。高峰期瞬间涌来大量连接如果backlog太小连接会在内核层排队失败nginx连不上立即返回502。这个参数在系统层面可以通过调整net.core.somaxconn来配合。2.3 日志类“侦查工具”每个日志文件都有话要说排查50X最忌讳“瞎猜”。打开文件看数据比什么都管用。常用的日志就四个方向PHP-FPM的错误日志默认在/var/log/php-fpm/error.logCentOS系或/var/log/php8.2-fpm.logDebian系里面的WARNING级别内容很多是“进程忙、worker重启”等线索。PHP本身的错误日志根据php.ini的error_log配置决定。如果配置的是syslog日志会走系统日志如果配置的是一个文件路径PHP的error_log()函数发出的内容会落在那里。这里面记录了PHP代码执行时的Fatal Error、Warning是定位500错误的第一手资料。nginx的错误日志路径在nginx.conf的error_log指令。访问日志里记录的是请求结果码错误日志里记录的是nginx转发过程中遇到的异常比如connect() failed、upstream timed out、worker_connections are not enough。慢日志request_slowlog_file指向的文件。它记录的是超过阈值的请求堆栈能看出业务代码到底卡在哪个函数、哪行SQL上。这个文件对定位504有决定性的帮助。3. 庖丁解牛四类高发50X故障的实操诊断3.1 500错误先别急着翻业务代码500是50X家族里唯一带“业务基因”的。它说明PHP进程把请求接下来了但执行过程中出了问题最终返回了一个500状态码。常见原因无外乎三类语法错误、运行时异常、第三方扩展崩了。遇到500我第一个动作不是看业务代码而是看PHP错误日志。命令是tail -n 200 /var/log/php-fpm/error.log如果日志里有关键的Fatal error问题基本就定位了一半。接下来用php -l检查最近改动过的文件语法php -l /var/www/html/app/Controllers/UserController.phpphp -l的输出很直白语法有错会直接说“PHP Parse error”并给出行号。这种错误在新发布代码、部署环境升级PHP版本时尤其常见。有一个细节值得留意线上环境通常开了display_errorsOff页面只显示“500 Internal Server Error”但错误信息仍然会被写入日志。如果日志里什么都没有多半是log_errors没开或者error_log的路径无权写入。这个情况我在第5章专门讲。还有一种隐蔽的500是PHP扩展的问题。我遇到过一次诡异的场景某功能在测试环境一切正常上线后偶发500错误日志时有时无最终查出来是opcache的revalidate设置太长新代码要用旧的字节码跑一段周期。清一下opcache配合正确的发布流程问题消失。500的排查口诀是日志优先语法次之扩展兜底。3.2 502 Bad GatewayPHP-FPM进程“夭折”的三种典型原因502的“味道”里带着明显的网络和进程特征。nginx把请求转给upstream结果upstream没有正常回应nginx只能回个502。排在榜首的三个原因分别是第一PHP-FPM进程全部退出。这种情况在日志里的典型表现是connect() failed (111: Connection refused)。可能的原因包括物理内存不足触发OOM killer把php-fpm整个杀掉了或者手动kill、重启失败导致服务没起来。处理办法是先确认状态systemctl status php-fpm ps aux | grep php-fpm如果进程确实没了先看/var/log/messages或dmesg里有没有Out of memory字样的记录。有的话那就是内存问题需要调小pm.max_children或者加内存。第二worker进程执行中被强制终止。request_terminate_timeout到了master会主动杀掉超时的workerPHP脚本调用了exit或触发memory_limit进程也会终止。这时候nginx侧通常是upstream prematurely closed connection while reading response header from upstream。第三连接耗尽。pm.max_children设得太小请求量超过阈值FPM不再接受新连接。nginx日志对应的是connect() failed (110: Connection timed out)或者“seems busy”的警告。排502时最好把上面三个状态分开看Connection refused偏向进程挂了prematurely closed偏向执行超时或被杀timed out偏向排队耗尽。三者对应的处置方式完全不同。3.3 503 Service Unavailable维护模式与连接数打满的分野503的定义是“服务暂时不可用”重点是“暂时”。这个状态码有时候是故意的比如上线维护时在nginx层配置了location / { return 503; }这种情况用户看到503属于预期行为看一眼nginx配置有没有多余规则就行。更常见的情况是“非故意的503”——后端真的忙不过来了。PHP-FPM的listen队列满了新请求直接被内核拒绝nginx收到一个“端口可连但没有worker可接单”的响应。在nginx日志里表现为connect() failed (110: Connection timed out) while connecting to upstream另一类“假503”也值得注意PHP代码里主动设置了http_response_code(503)。比如某个活动页面做了“渠道限流”流量达标后直接返回503。这种属于业务设计的“软错误”不是故障但会混在监控报警里干扰判断。识别方法很简单看返回的响应体里有没有业务特征比如JSON格式的提示语。纯白板的503才是基础设施问题。3.4 504 Gateway Timeout超时链路上最容易忽略的两个节点504是“等”出来的错误。nginx等不及了主动放弃。它和502的最大区别是502是“根本没连上”504是“连上了但迟迟没有结果”。超时链路有两个节点最容易被忽略。第一个节点是“PHP-FPM内部等锁”。PHP代码在执行中调用了某个需要等待的资源比如MySQL慢查询、Redis阻塞命令、外部接口请求等。request_terminate_timeout到了就把worker杀了nginx那边同时接不到数据于是504。这种场景慢日志是利器。打开request_slowlog_timeout和request_slowlog_file重新触发一次请求看慢日志里的堆栈就能定位到具体的SQL或者函数调用tail -n 100 /var/log/php-fpm/slow.log第二个节点是“nginx和PHP-FPM的超时时间错配”。nginx的fastcgi_read_timeout设了30秒FPM的request_terminate_timeout设了60秒执行一个耗时50秒的任务最终一定是nginx先返回504。这类问题排查起来特别容易绕弯子因为两边都觉得自己没问题。我的习惯是把这两组超时时间统一管理先定业务上限一个请求最多能接受多少秒再同步设置FPM和nginx的值。FPM侧设为Tnginx侧建议设为T5秒留出缓冲时间避免边界情况误伤。3.5 快速定位决策表把上面这些现象汇总成一张决策表平时排查时可以拿出来对照现象重点排查日志常见本质浏览器白板500错误页无框架特征php-fpm错误日志、nginx错误日志FPM崩溃、worker被杀500但页面有框架错误提示PHP error_log业务异常、语法错误、依赖问题502nginx日志“Connection refused”systemctl状态、dmesgFPM进程挂掉、端口未启动502nginx日志“prematurely closed”FPM慢日志、request_terminate_timeout脚本超时被杀、内存超限503nginx日志“110: Connection timed out”FPM状态、listen.backlog、max_childrenworker耗尽、队列打满504nginx日志“upstream timed out”FPM慢日志、fastcgi_read_timeout代码慢、SQL慢、超时错配4. 真实故障案例复盘从发现到解决4.1 案例一高峰期502连环报警真凶是“进程自杀”某教育类网站每天晚8点到10点是晚自习打卡高峰流量是平时的四倍。连续三天每天晚上这个时段都会出现一波502每次持续5到10分钟随后自动恢复。起初大家都以为是流量太大FPM扛不住。我上机器一看pm.max_children设的是100物理内存却有32G怎么算都够用。但如果只看max_children方向就跑偏了。真正的线索在FPM错误日志里WARNING: [pool www] server seems busy (you may need to increase pm.start_servers, or pm.min/max_spare_servers), spawning 8 children, there are 0 idle, and 100 total children总child数已经到了100但空闲数长期是0。说明所有worker全在忙。再看慢日志大量请求卡在同一个函数——某报表模块在打卡高峰生成了全量用户统计SQL全表扫耗时几十秒。每一个这样的慢请求占着一个worker不放其他正常打卡请求全在排队。排队超过request_terminate_timeout后worker被强杀nginx连不上于是502。最终解决方案是三个动作把生成统计报表的代码改为异步任务请求进来先入队列后台再慢慢跑统计打卡接口不再等报表结果。给FPM池设置pm.max_requests 1000让worker定期自杀重启避免某些进程积累异常状态。慢日志阈值降到5秒方便后续出现类似问题时更快定位。高峰期502再也没有出现过。这个案例的核心教训是502只是“果”代码里的慢请求才是“因”。很多时候把慢查询消掉比盲目调大max_children更有效。4.2 案例二偶发500指向opcache的“陈旧代码”另一个案例来自一个交易系统某次发布后用户反馈“提交订单偶尔报500”从浏览器上看错误页白板一块没有任何框架提示。第一轮排查看了PHP错误日志干干净净一条Fatal Error都没有。又看了一遍nginx日志能搜到的500请求都指向同一个接口但错误日志里没有PHP报错记录。当时一度怀疑是流量问题但错峰时段也会偶发。后来想到一个可能性opcache。线上环境配的是opcache.enable1 opcache.validate_timestamps0validate_timestamps0意味着opcache永远不会检查源文件是否更新。发布新代码之后如果opcache没有主动清理PHP-FPM仍然用旧代码的字节码跑。但问题是旧代码和新代码的文件结构不同偶尔会出现“方法找不到”的致命错误这类错误在屏幕上不显示直接打到系统日志的某个角落。解决方案很标准发布完成之后执行一次php -r opcache_reset();或者在运维脚本里用kill -USR2 php-fpm-master-pid来触发平滑重载让opcache自然失效。从那之后我就把发布规范改了每次部署强制走“清opcache→平滑重载FPM→健康检查”三个步骤。这个习惯看着简单但能减少一大批“灵异500”。4.3 案例三定时任务引发的503雪崩有个数据统计平台每天凌晨要做大量的报表聚合。某天开始每天凌晨两点到三点之间线上服务的可用率掉到80%以下全是503。排查时发现凌晨两点同时触发了十几个定时任务每个任务都是多进程并发跑瞬间把数据库连接池打满。PHP-FPM的worker没崩溃但业务代码在等待获取数据库连接互相排队最终超过nginx的fastcgi_read_timeout返回503。这里其实有三层问题定时任务没有错峰执行全挤在同一个时间点。每个任务内部的并发数没有限制任务一启动就开几十个进程。PHP-FPM的worker数看似够用却被等待数据库连接的请求占满。调整措施是让任务排期错峰比如把统计A放到02:00统计B放到02:30统计C放到03:00。同时每个任务内部限制最大并发数为5个进程。改完之后凌晨的503彻底消失。这个案例给我的体会是50X未必是“服务真要挂了”资源争用造成的排队也能瞬间把服务淹掉。排查时不要只盯住请求路径本身上游的定时任务、下游的数据库连接池都要一并查。4.4 案例四504幽灵来自第三方接口的慢响应还有一次504故障表现很诡异用户操作某个功能时偶发504每次持续的窗口期很短一天可能就几次。MySQL和Redis的慢查询都没有异常FPM慢日志里也找不到明显的业务堆栈。后来发现慢日志里有一个file_get_contents(https://third-party.example.com/api/...)的调用耗时很长最长的一次超过80秒。某接口在业务代码里调用了外部供应商的API而PHP默认的default_socket_timeout是60秒外部接口一旦响应慢就会拖住整个PHP请求。这种问题的解法有两条路径给第三方调用设置独立的超时用curl代替file_get_contents并设置CURLOPT_TIMEOUT为5秒。把第三方调用改为异步/队列模式不能让核心业务接口等一个外部接口。我后来把项目里所有外部调用都改成了curl并且强制约定所有外部服务的超时时间不超过5秒。504的“幽灵”再也没出现过。5. 常见问题与排查技巧实录5.1 为什么改了php.ini不生效这个问题几乎每个月都会有人问。最常见的坑是改错文件了。PHP的配置加载机制是分层的。php.ini是主配置文件但对PHP-FPM而言pool.d/www.conf里的php_admin_value、php_value指令会覆盖php.ini里的同项设置。很多老项目喜欢在www.conf里写php_admin_value[memory_limit] 256M结果你在php.ini里改成512M怎么reload都不生效。用phpinfo()或者命令行php -i | grep memory_limit查看的时候看到的是CLI运行环境的值和FPM工作环境完全不同。修改之后还要确认操作是否到位。改php.ini需要重启php-fpm服务改www.conf只需要systemctl reload php-fpm。很多人改了配置只reload不restart有些参数比如pm.max_children其实reload就能生效但opcache的有些设置需要restart。5.2 curl返回200但浏览器502我在排查一个问题时遇到过分歧同一台服务器本地curl访问接口返回200但手机浏览器访问却502。后来发现是因为curl请求直接经过了nginx的server块而浏览器带了HTTPS走了443端口两个端口背后的fastcgi_pass配置不一样。生产环境里多站点、多配置段的情况很常见两个location块用的upstream地址、超时时间都可能不同。排障时始终记住不要看“另一个端口”的表现要跟着真实流量走。可以抓一下实际报错时的浏览器请求看它在nginx的访问日志里走到了哪个server、哪个location再对着那段配置查。混淆环境往往是排查最大的时间杀手。5.3 日志里什么都没有错误到底去哪了50X排障里最绝望的时刻是业务日志、nginx日志、FPM日志全翻了一遍啥都没有。这时候有几个盲区值得挖display_errorsOfflog_errorsOff。线上环境为了安全把显示错误关了如果同时把错误日志也关了PHP错误既不上报也不落盘。日志权限问题。FPM的worker进程以www用户运行error_log指向的文件目录如果权限是root日志写不进去但不报错。异常被框架捕获。很多框架有自己的异常处理机制异常在手写代码之前已经被框架接住并写入了框架自带的日志目录不会出现在PHP标准错误日志里。所以说代码里加一个全局兜底异常处理是个好习惯把未捕获异常统一记录一份完整堆栈对线上排障帮助极大。5.4 排查流程速查表最后把我常用的排查流程整理成一个小清单照着走基本不会偏确认状态码。500查PHP错误日志502查nginx错误日志里的upstream错误类型503查FPM进程数和listen队列504查慢日志和超时配置。确认时间窗口。故障是持续还是偶发持续时间段内的FPM日志、nginx错误日志有多少行。看上游资源。top看CPU和内存mysql SHOW PROCESSLIST看数据库慢查询redis-cli INFO看内存和阻塞操作。缩小范围。单接口还是全站电信用户还是所有用户是否刚发过版是否调整过配置最终定位。根据日志堆栈和监控指标找到具体的SQL、函数、进程再针对性修复。这个流程本质上是把50X拆成“入口”“转发”“处理”“下游”四个层面逐层定位不打无准备的仗。最后再说几句实在话50X错误这个东西看着吓人其实是所有Web系统都会经历的“成人礼”。我个人最大的体会是真正快速解决50X靠的不只是排查技巧更是平时的预防。状态页、监控、压测这三件事做到位能避开大多数“凌晨抢修”。具体来说你至少要做到三件事一是打开PHP-FPM的状态页用pm.status_path配置一个监控端点配合监控系统实时看空闲worker数和队列状态二是把慢日志长期打开阈值设到5到10秒每天扫一遍三是每次上线前做一轮基础的压测至少把max_children和request_terminate_timeout验证出一个合理值。再分享一个小技巧排查502/504时先在nginx配置里加一行fastcgi_next_upstream http_502 http_504;配合多upstream做自动切换能在服务端“抢修”期间把用户引流到备用节点减小影响面。当然这只能缓燃眉之急真正的根因还得按前面说的流程去挖。希望这篇“解剖报告”能让你在下次遇到50X时少一些慌乱多一些从容。