ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Nginx 502 Bad Gateway排查指南:反向代理故障全链路分析与速查表

Nginx 502 Bad Gateway排查指南:反向代理故障全链路分析与速查表 搞反向代理的日子我和502 Bad Gateway打了无数次照面。浏览器白屏出Bad Gateway、接口监控报警、用户截图里各种错误码十有八九就是它。作为一个天天和Nginx打交道的人我踩过各种稀奇古怪的坑也慢慢总结出了一套从“看到502”到“摸到根因”的排查思路。这篇东西不是Nginx官方文档的翻译而是我在真实环境里压出来的经验先讲502到底怎么产生的再顺着请求链路一层层拆排查步骤最后给你一个我自己平时排查用的速查表。刚接手Nginx配置的运维、被502折磨的开发、以及想系统理解反向代理故障排查的读者按这条思路走下去基本能在几分钟内把问题锁定到具体某一层。1. 先搞清楚502到底在报什么1.1 502在HTTP协议里到底算哪一出502 Bad Gateway是HTTP协议里的一个状态码大体意思是服务器作为网关或代理时从上游服务器收到了无效响应。放在Nginx反向代理的场景里Nginx就是那个“网关”。浏览器先请求NginxNginx再把请求转给后端服务后端如果返回了不正常的结果Nginx只能告诉浏览器“我拿到的不是个有效响应”。用生活里的例子打比方你打电话到前台前台帮你转接负责人结果负责人要么不接电话要么说话驴唇不对马嘴你最后只能从前台嘴里听到一句“联系不上回头再打吧”。这个“回头再打吧”就相当于502。这里要注意的是502不是后端把错误码传给你了而是Nginx压根没拿到能用的结果。后端服务直接挂掉、端口不通、连接被重置、响应头不完整、上游返回了空响应这些情况都可能被Nginx判定为502。所以排查的起点不是先看应用日志而是先确认Nginx到后端这一整条链路的健康状况。1.2 500、502、504别傻傻分不清很多刚接触的人会把502和500、504搞混其实这三个状态码是排查方向的分水岭判断错会绕很大的弯路。500 Internal Server Error是后端应用自己抛出的错误应用进程活着Nginx也把请求成功转发过去了但后端代码执行到一半炸了。这时候错误根源在后端应用内部直接去查应用日志就行。502 Bad Gateway是Nginx作为代理从上游收到的响应无效或者连接压根没建立起来。问题可能出在网络链路、Nginx配置、后端进程存活状态等多个层面。504 Gateway Timeout是Nginx把请求转发给了上游但上游在规定时间内没有返回完整响应Nginx等得不耐烦主动切断了连接。这时候多半要往后端接口性能、慢SQL、超时配置这些方向查。一句话记忆500是应用层炸了502是传输层没接上504是等太久不等了。1.3 一条请求链路上可能出问题的位置理解502产生的原理先要清楚一条完整的请求链路长什么样浏览器 → Nginx监听80/443 → 后端服务监听8080、9000或者Unix SocketNginx在这条链路上有两种转发方式一种是proxy_pass把请求转发给HTTP服务比如Java的Spring Boot、Node.js、Go服务另一种是fastcgi_pass把请求转发给FastCGI服务最常见的就是PHP-FPM。502可能出现在这条链路的任何一环。后端进程没起来、后端监听地址和Nginx配置不一致、中间防火墙挡了、后端服务处理到一半崩了、系统资源打满导致无法建立新连接这些都会让Nginx向上游要不到有效响应。接下来就按排查优先级一层一层往下拆。2. 第一梯队排查后端服务与网络链路2.1 先确认后端是不是真的活着看到502我的第一反应永远是先去后端机器上看一眼而不是先翻Nginx配置。后端服务要是已经挂了后面的排查全部白搭。最直接的一条命令是检查端口监听状态ss -lntp | grep 8080如果输出里看不到LISTEN状态的端口说明后端服务没有起来。再用systemd管理的服务直接看状态systemctl status 你的服务名服务进程活不等于端口通端口通了也不等于服务能正常响应。建议顺手用curl测一下后端自己的健康接口curl -I -m 5 http://127.0.0.1:8080/healthcurl加-I参数只拿响应头加-m限制最大等待时间避免命令卡住。能正常返回HTTP状态码说明本机访问这个端口是通的。有一个特别隐蔽的坑后端服务只监听了IPv6的地址或者只监听了127.0.0.1但Nginx配置里写的proxy_pass目标是另一个地址。比如服务监听的是[::]:8080而Nginx转发到127.0.0.1:8080连接会被直接拒绝。所以不光要看端口有没有监听还要看监听在哪个地址上。ss输出里会明确显示0.0.0.0、127.0.0.1、[::]这些不同形式和配置一对就知道问题在哪。提示很多服务有多个实例或端口确认健康接口是最靠谱的方式光看进程活着可能判断不出来。2.2 Nginx到后端之间到底被谁拦住了Nginx和后端不在同一台机器上的时候防火墙和安全组是重灾区。很多场景都是后端机器自己curl一切正常但Nginx机器转发过去的请求全部超时最后发现是防火墙只放行了部分来源IP。从Nginx机器手动探测一下后端端口nc -vz 192.168.1.10 8080 curl -m 3 http://192.168.1.10:8080/health通了就继续查下一层不通就检查后端机器的防火墙规则。RedHat系用firewalldfirewall-cmd --list-allDebian系用ufwufw status verbose传统的iptables还在的话也要看iptables -L -n云服务器还要单独查控制台里的安全组入方向规则。安全组和本机防火墙是两个独立的东西任何一个没放行都会导致连接不通。我在实际排查中见过很多次本机防火墙关了但安全组规则只放行到80端口的流量后端8080端口只允许特定IP访问Nginx转发过去自然就被丢了。2.3 proxy_pass的路径替换规则一个斜杠就能坑一下午Nginx的proxy_pass路径替换规则是新手最容易踩的坑因为这个错误从浏览器上很难看出来。先看两个配置对比location /api/ { proxy_pass http://127.0.0.1:8080; }这种情况下Nginx会把原始URI原样传给后端。请求/users接口时如果location匹配的是/api/那么实际传给后端的URI是/api/users。再看另一个配置location /api/ { proxy_pass http://127.0.0.1:8080/; }注意proxy_pass目标地址末尾多了一个斜杠行为就完全不同了。Nginx会用proxy_pass里的URI部分替换location匹配到的部分/api/users会变成/users转发给后端。如果后端只暴露了/user但前端请求的是/api/user第一种写法会让后端返回404如果配置里填错了路径对应关系后端返回404或者直接拒绝Nginx在某些情况下就会给浏览器呈现502。遇到这种问题先把自己的location和proxy_pass对照一遍确认路径替换关系是你想要的样子。2.4 别忽视SELinux和DNS缓存的诡异现象RedHat系发行版包括AlmaLinux 9、CentOS、Rocky Linux默认SELinux是Enforcing模式。Nginx作为代理去连接后端端口时SELinux策略默认可能不允许httpd进程发起对外网络连接连接直接被内核拦截Nginx日志里会显示权限错误或者连接失败后端服务本身完全正常。检查SELinux状态getenforce如果输出是Enforcing再用SELinux审计日志确认是否有拦截记录ausearch -m avc -ts recent确认是SELinux拦截后开放Nginx对外连接后端端口的权限setsebool -P httpd_can_network_connect 1这个问题在自建服务器上特别常见排查时如果发现后端端口ss显示正常、Nginx配置也对、但连接就是失败一定要想到SELinux。另一个隐蔽坑是DNS解析缓存。如果proxy_pass里填的目标是域名location / { proxy_pass http://api.internal.corp:8080; }Nginx默认只在启动和reload的时候解析一次这个域名。后端服务换了机器IP域名DNS已经指向新地址但Nginx进程还记着旧IP所有请求都打到已经不存在的旧机器上自然全部502。遇到“重启Nginx就好了但过一阵子又502”的诡异现象优先怀疑这个。在Nginx机器上直接解析一下域名看结果是不是后端实际监听的IPgetent hosts api.internal.corp如果解析结果和实际后端IP对不上执行nginx -s reload让Nginx重新解析域名就行。3. 动态站点专项FastCGI与PHP-FPM的5023.1 先分清你的站点走的是哪条路Nginx转发动态请求主要分两种proxy_pass转发给HTTP服务fastcgi_pass转发给FastCGI服务。很多传统PHP站点用的是后一种502的排查方向也会不太一样。PHP-FPM监听方式有TCP和Unix Socket两种。默认配置通常在/etc/php-fpm.d/www.conf里listen 127.0.0.1:9000或者listen /run/php-fpm/www.sockNginx里对应的配置分别是location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; include fastcgi_params; }location ~ \.php$ { fastcgi_pass unix:/run/php-fpm/www.sock; fastcgi_index index.php; include fastcgi_params; }两边只要不一致502是必然结果。比如PHP-FPM改成监听Unix Socket但Nginx还按旧的127.0.0.1:9000去连接连接会直接被拒绝。3.2 从Nginx错误日志快速定位FastCGI故障Nginx的错误日志是排障的第一现场默认路径是/var/log/nginx/error.log。遇到502第一时间打开它看最后几行报什么错。不同报错对应完全不同的原因错误日志关键词含义排查方向connect() failed (111: Connection refused)连接被拒绝目标端口上没有服务在监听PHP-FPM没启动、监听地址和端口与Nginx配置不一致connect() failed (2: No such file or directory)Unix Socket文件不存在socket路径错误或者PHP-FPM没启动导致socket文件没创建recv() failed (104: Connection reset by peer)连接被对端重置PHP-FPM进程崩溃、被kill、或主动断开连接upstream prematurely closed connection while reading response header上游在返回响应头之前就断开了连接PHP请求执行超时被终止、PHP-FPM重载、进程池被占满第4类报错在大流量业务里最常见。PHP-FPM处理请求超时或者进程池里所有worker都被慢请求占满新请求进来后PHP-FPM来不及处理Nginx和PHP-FPM之间频繁断开连接就会在日志里刷出大量prematurely closed。3.3 PHP-FPM进程池参数一个大坑PHP-FPM的进程池配置直接决定动态站点的并发能力。很多人默认配置一用就是好几年业务量涨上去之后502就开始频繁出现。主要参数在www.conf里pm dynamic pm.max_children 30 pm.start_servers 8 pm.min_spare_servers 4 pm.max_spare_servers 16 pm.max_requests 1000max_children表示最多同时运行多少个PHP-FPM进程。这个值不是越大越好每个进程都会占用内存配置太大内存耗尽系统会开始用swap性能反而崩。一个粗略的估算方法是先用free -m看可用内存再看单个PHP-FPM进程平均占用多少内存用可用内存除以单进程占用再打个七折留余量就是max_children的参考值。比如机器可用内存2G每个PHP-FPM进程平均占40M2000*0.7/40约等于35那max_children设置在30左右比较稳妥。pm.max_requests是另一个容易忽略的关键参数。每个PHP-FPM进程处理完指定数量的请求后会自动退出重启目的是防止脚本内存泄漏累积。不设置这个值内存泄漏就会一点点把系统内存吃光进程越来越多最终502。建议设置在500到1000之间既不会频繁重启也能及时释放内存。另外强烈建议给PHP-FPM设置request_terminate_timeoutrequest_terminate_timeout 30这个参数的作用是让单个PHP请求最多执行30秒超时就被强制终止。有些请求卡在外部API调用、卡在数据库查询上业务代码里根本没写超时逻辑如果没有这层兜底这些僵尸请求会慢慢把整个进程池占满然后整个站点的访问全部502。4. 超时与系统资源容易忽略的隐形杀手4.1 分清connect、read、send三种超时Nginx转发请求给后端时超时设置是三个维度connect超时、read超时、send超时。很多人只配一个proxy_read_timeout就完事其实三个都可能出问题。connect超时是TCP三次握手的时间后端服务在内网且正常运行时这个时间应该在毫秒级。如果connect都超时了基本可以断定后端有问题或者网络不通。read超时是Nginx建立连接后等待后端返回数据的时间。注意这里指的是两次读操作之间的时间间隔不是整个请求的总时长。后端处理一个接口要10秒只要中间持续有数据返回Nginx不会报read超时但如果后端一直不发数据超过proxy_read_timeout就会触发超时。send超时是Nginx向后端发送请求数据时的等待时间上传大文件时这个参数特别重要。推荐一组比较合理的配置proxy_connect_timeout 5s; proxy_send_timeout 30s; proxy_read_timeout 30s;PHP站点对应的是fastcgi版本fastcgi_connect_timeout 5s; fastcgi_send_timeout 60s; fastcgi_read_timeout 60s;这里有个容易犯的错误后端导出报表要30秒但中间没有数据返回Nginx到了read超时时间就直接504了。遇到这种场景要么把read_timeout调大要么让后端边处理边返回进度保持连接活跃。反过来如果后端接口正常情况下都是几百毫秒返回就没必要把read_timeout设成几分钟超时时间设太长只会让故障发现得晚。4.2 文件句柄和worker连接数系统层的隐形上限Nginx在高并发场景下出现502有时候不是后端挂了而是Nginx自己到瓶颈了。每个TCP连接在Linux里对应一个文件描述符fd而Nginx的worker进程能打开多少fd受配置和系统参数双重限制。先看系统层面的限制ulimit -n如果输出1024那意味着这个进程最多同时打开1024个文件描述符。反向代理场景下一个请求在Nginx侧占一个前端连接转发给后端再占一个后端连接等于一个用户请求要消耗两个fd。并发500个请求就需要1000个fd基本就到顶了。Nginx配置里对应的调优参数worker_processes auto; worker_rlimit_nofile 65535; events { worker_connections 10240; }worker_processes一般等于CPU核心数或者用auto让Nginx自动识别。worker_connections表示单个worker进程能同时保持的最大连接数。理论最大并发连接数约等于worker_processes乘以worker_connections但反向代理场景还要考虑每个请求占两份连接实际并发能力要打个对折。有一种502场景之前让我排查了很久后端服务正常、响应也快但一到高峰期就出现零星502。最后用ss -s看系统连接状态发现TIME_WAIT状态的连接堆积了几万个每个TIME_WAIT连接虽然不占fd但会占用端口资源。原因就是Nginx到后端的连接不断新建、不断关闭端口和内核资源被耗尽。TIME_WAIT的优化方向不是盲目调内核参数而是从源头上减少短连接。把upstream keepalive配置好Nginx到后端的连接就能复用TIME_WAIT瞬间降下来。内核参数里tcp_tw_reuse可以开但tcp_tw_recycle就不要再开了NAT环境下很容易造成连接被重置。4.3 upstream keepalive到底怎么配才是对的网上关于Nginx keepalive的配置讨论很多实际配置却经常不对。最简单的配置长这样upstream backend { server 127.0.0.1:8080; keepalive 32; } server { listen 80; location / { proxy_pass http://backend; } }很多人的配置到这里就结束了但注意一个细节Nginx默认用HTTP/1.0协议向后端发起请求。HTTP/1.0协议没有默认的连接复用机制即使upstream里配置了keepalive实际也不会生效每次请求都要新建TCP连接。必须显式指定HTTP/1.1并清空Connection请求头location / { proxy_http_version 1.1; proxy_set_header Connection ; proxy_pass http://backend; }proxy_http_version 1.1是让Nginx用HTTP/1.1访问上游Connection头置空是告诉上游不要在响应里要求关闭连接让连接可以被复用。keepalive 32的意思是每个worker进程最多保留32个空闲的后端连接。这个值不是越大越好要结合后端服务自己的最大连接数来定超过后端的承受能力反而会引发连接排队。配置生效后观察后端连接数应该能看到大量ESTABLISHED状态的长连接TIME_WAIT明显减少。这个优化在高并发场景下收益极其明显也是很多502问题背后的真正解法。5. 一些隐蔽的坑与长期治理5.1 健康检查不到位时502会雪崩Nginx的upstream模块自带一个被动健康检查机制通过max_fails和fail_timeout控制upstream backend { server 127.0.0.1:8080 max_fails2 fail_timeout30s; }含义是如果30秒内有2次转发失败就把这个后端标记为不可用。但这种被动检查有个特点它只在有流量经过的时候才判断而且fail_timeout窗口结束后Nginx会重新尝试把流量转发过去不会主动验证后端是否恢复。这就导致一个现象后端服务挂了Nginx快速把节点标记为不可用短时间内不会往这个节点转发流量但fail_timeout一过Nginx又开始转发失败、再标记、再等待周而复始。如果所有后端节点都在这条路上就会表现为502一阵、好一阵、又502一阵。主动健康检查需要额外模块或者脚本配合。没有商业版的情况下我习惯用Cron跑一个监控脚本定时curl后端的健康检查接口curl -s -m 5 -o /dev/null -w %{http_code} http://127.0.0.1:8080/health返回000表示连接失败返回502、504这些说明后端应用异常。脚本结合告警通道能第一时间发现问题不用等用户来投诉。5.2 proxy_next_upstream和重试的副作用Nginx还有一个重试机制当上游返回错误时可以把请求转给下一个后端节点。配置看起来人畜无害proxy_next_upstream error timeout http_502 http_504;含义是连接出错、超时、或上游返回502、504时换一个后端节点重试。这个机制在有两个以上后端节点时能提高可用性但也藏着风险。最大隐患在于第一个后端节点可能已经接收并处理了请求只是在返回响应时网络断了或者返回了让Nginx认为“可以重试”的状态码。Nginx把同样的请求转发给第二个节点第二个节点又处理了一遍。如果这个请求是个下单操作、支付操作、数据写入操作且后端没有做幂等处理就会出现重复下单、重复扣款。我的建议是读写场景区别对待读接口可以配置http_502 http_504重试写接口尽量只配置error timeout不要把502、504这种HTTP状态码纳入重试条件。宁可让用户看到一次502也不能让用户被重复扣两次钱。5.3 缓存了错误响应后端恢复了还在502有些团队会开启Nginx的proxy_cache缓存后端响应这个方向本身没错但配置要注意别把错误响应也缓存了。问题出在proxy_cache_valid上proxy_cache_valid 502 1m;这个配置的意思是上游返回502时把这个错误响应缓存1分钟。后端服务宕机期间Nginx确实能快速返回缓存用户不会长时间等待。但后端恢复之后Nginx在缓存过期前仍然在返回缓存的502用户看到的还是错误页。检查自己的缓存配置时确认proxy_cache_valid里没有包含5xx状态码。如果是想在后端挂掉的时候用旧缓存撑一撑正确的姿势是用proxy_cache_use_staleproxy_cache_use_stale error timeout http_500 http_502 http_503 http_504; proxy_cache_background_update on;含义是当上游出错、超时、或者返回5xx时Nginx可以返回之前缓存的旧版本响应同时后台继续尝试更新缓存。这样既能扛住源站故障又不会把错误响应永久缓存下来。5.4 Host头与真实IP头不是502却经常和502一起出现的坑后端服务如果配置了虚拟主机对Host头很敏感。Nginx默认会把浏览器请求里的Host头原样转发给后端但如果你在Nginx里显式改写了Host头比如proxy_set_header Host example.com;而后端实际配置的虚拟主机名不是example.com后端可能会直接拒绝请求表现就是连接被重置或者返回无效响应浏览器看到的就是502。还有一种常见情况是反向代理后面接的多个虚拟主机Nginx配置了Host头转换但漏了某一条规则导致部分域名访问正常、部分域名502。排查时可以临时去掉proxy_set_header Host让Nginx用默认方式转发看问题是否消失。X-Forwarded-Proto头也要一起配置尤其是后端需要生成HTTPS链接的场景proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme;X-Forwarded-Proto不配后端收到的是http生成的回调地址、重定向地址全变成了http。用户访问HTTPS站点被跳转到HTTP浏览器可能直接拦截或者报错表现上虽然不是标准502但排查体验极其迷惑。6. 502排查速查表与日常监控6.1 常见原因与排查方向速查表这里我把排查过程中最常见的几种情况整理成一张表方便你遇到问题时快速对照现象 / 日志关键词可能原因解决方向connect() failed (111: Connection refused)后端服务没启动或监听地址、端口与Nginx配置不一致启动服务用ss -lntp确认监听地址对齐配置connect() failed (2: No such file or directory)Unix Socket路径不存在或错误确认php-fpm监听地址与fastcgi_pass一致upstream timed outNginx等待上游超时调大proxy_read_timeout / fastcgi_read_timeout排查后端慢接口upstream prematurely closed connection后端在响应头返回前断开PHP-FPM进程池占满、请求执行超时被终止、服务重启本机curl后端正常Nginx转发不通防火墙、安全组、SELinux拦截检查防火墙规则、云安全组、getenforce确认SELinux状态重启Nginx后好一阵又开始502upstream域名解析的IP没更新getent hosts解析确认IP执行nginx -s reload重新解析高并发时零星502文件句柄不足、TIME_WAIT堆积、keepalive未生效调worker_rlimit_nofile、worker_connections补齐keepalive配置后端恢复了Nginx还在502缓存了错误响应fail_timeout窗口未过检查proxy_cache_valid是否包含5xx调整max_fails/fail_timeoutWindows下部署Nginx出现502路径分隔符、文件权限、端口占用检查Nginx目录权限和后端服务监听状态确认配置文件路径正确这张表覆盖了日常环境中90%以上的502场景。遇到没见过的症状记住一个原则顺着请求链路从后往前查。先确认后端再确认中间网络最后才怀疑Nginx配置本身。6.2 把502扼杀在用户发现之前502这种故障最让人头疼的不是难修而是难发现。用户不投诉你可能永远不知道站点已经间歇性502了几个小时。养成给自己加监控的习惯非常重要。首先把Nginx的日志格式扩展一下把上游返回状态和响应时间带出来。默认的combined日志格式看不到upstream相关字段。自定义log_formatlog_format main $remote_addr - $remote_user [$time_local] $request $status $body_bytes_sent $http_referer $http_user_agent $http_x_forwarded_for upstream_addr$upstream_addr upstream_status$upstream_status upstream_time$upstream_response_time;然后access_log里引用这个格式access_log /var/log/nginx/access.log main;这样每行日志里都能看到Nginx连接了哪个上游地址、上游返回了什么状态码、花了多长时间。排查502时会精确很多直接看出是连接被拒绝还是超时。配合一个简单的Cron脚本做监控tail -n 300 /var/log/nginx/access.log | awk {if ($index 502) count} END {print count}根据日志格式调整字段位置统计最近300条请求里的502数量超过阈值就触发告警。有Zabbix、Prometheus、Grafana这些监控系统当然更好没有的话Shell脚本顶上也够用。还有一个经验值得分享Nginx的access.log里$upstream_status字段如果显示为000代表Nginx根本没有收到上游的任何响应连接在建立阶段就失败了。这种状态和502还不完全一样但排查方向基本一致优先查网络和后端进程。我在实际工作中发现502问题90%以上是下面这几类原因后端服务没起来、防火墙或SELinux拦截、proxy_pass配置路径不对、PHP-FPM进程池被占满、超时时间太短、keepalive没有真正生效。这些全部排查一遍剩下的基本都是比较冷门的场景了。最后说一个我自己的教训。有一次业务方半夜打电话说接口大面积502登录服务器一看Nginx在跑、php-fpm在跑、端口也通、curl后端也正常浏览器就是502。折腾了快一个小时最后发现是某条业务代码把数据库连接池打满所有PHP进程都在等数据库连接Nginx转发过去后长等无果疯狂报502。那晚之后我给自己立了两条规矩第一日志格式里必须带upstream_status和upstream响应时间没有这两个字段排障就是盲人摸象第二健康检查不能只检查端口通不通要模拟真实请求链路端口活着不代表业务是好的。后来再遇到502我基本能在几分钟内判断出问题出在哪一层希望这套思路也能帮到你。
返回列表