做运维和技术支持的这些年,Nginx反向代理的502 Bad Gateway应该是我遇到频率最高、也最容易被误判的报错之一。不管你是刚搭好环境调试,还是线上突然报警,只要看到502,心里基本就明白:客户端到Nginx这段是通的,但Nginx往后端转发的链路出了问题。这篇文章把我这么多年排查502的完整思路、常见诱因和实操命令整理出来,适合正在踩坑的运维、后端开发,也适合刚接触Nginx的同学照着一步步定位。
1. 502 Bad Gateway是什么,先搞清楚问题发生在哪一环
1.1 从一次完整的请求链路说起
要解决502,首先得理解反向代理模式下一次请求是怎么走的。客户端发起请求,先打到Nginx,Nginx根据你配置的location匹配规则,把请求转发给upstream定义的后端服务器(可以是一台Tomcat、一个PHP-FPM进程、一个Node服务,也可以是内网其他端口上的应用)。后端处理完,把响应原路返回给Nginx,Nginx再回给客户端。
502出现的准确位置就在这里:Nginx成功建立了连接,但没能从上游服务器拿到一个有效的HTTP响应。注意这句话的两个关键词,一个是"建立了连接",一个是"没拿到有效响应"。这意味着502跟连接被拒绝(这个通常报502或者504,取决于具体错误)还不完全一样,它描述的是Nginx已经愿意去转发,但后端要么没应答、要么应答格式不对、要么中途断掉了。
很多新手一看到502就怀疑是Nginx配置错了,这其实是误解。Nginx配置错误通常会在启动阶段就报错,或者在访问时出现404、403这些更明确的错误码。502更像是一个"中间状态"的报错,它把问题范围从"整个链路"缩小到了"Nginx到后端这一段",这是好事,排查范围一下子收敛了。
1.2 502和504、499的区分
我每次排查之前都会先确认一下,这个错误到底是502、504还是499,因为这三个最容易混淆,但它们对应的故障点完全不同。
| 错误码 | 含义 | 典型原因 |
|---|---|---|
| 502 Bad Gateway | 上游返回了无效响应,或连接建立后立即断开 | 后端崩溃、PHP-FPM无可用进程、后端返回空响应 |
| 504 Gateway Timeout | Nginx等待上游响应的时长超过了proxy_read_timeout配置 | 后端接口本身很慢、数据库查询阻塞、超时时间设置过短 |
| 499 Client Closed Request | 客户端在Nginx等待上游期间主动断开了连接 | 用户刷新页面、前端请求超时取消 |
举个例子,如果你配置了proxy_read_timeout 60s,后端接口正常处理需要90秒,那么60秒一到,Nginx就会返回504,而不是502。但如果你把超时调到120秒,后端却在第100秒突然进程崩溃、连接断开,返回的就是502。所以看到报错码先别急着改配置,想清楚这个错误码在描述什么阶段的问题,后面的排查效率会高很多。
2. 排查前先做的三件事:日志、网络、进程
2.1 第一步永远先看Nginx错误日志
我见过太多人一上来就改配置、重启服务,折腾半天问题还在。成熟的排查顺序应该是先看日志,再测网络,最后才是改配置。Nginx的错误日志位置通常在nginx.conf里配了error_log,默认路径是/var/log/nginx/error.log,用下面这条命令盯住最新的报错:
tail -f /var/log/nginx/error.log然后去浏览器或curl触发一次502,回到终端看新增的日志。这里有个关键技巧:日志里的报错信息会直接告诉你Nginx连接上游的时候到底发生了什么。常见的几类错误如下。
connect() failed (111: Connection refused) while connecting to upstream这说明Nginx尝试连接后端端口时被拒绝了,多半是后端服务没监听这个端口,或者监听在别的网卡/IP上。
upstream prematurely closed connection while reading response header from upstream这说明连接是建立成功的,但后端在Nginx读取响应头部之前就把连接关了。这个场景非常经典,多见于PHP-FPM进程池耗尽、后端应用崩溃或者返回了不完整的HTTP响应。
no live upstreams while connecting to upstream这说明upstream组里配置的所有后端节点都处于不可用状态,配合max_fails和fail_timeout形成一个降级判断,Nginx直接放弃转发。
日志就是案发现场,它会告诉你是"连不上"还是"连上了又断了",这两个方向的排查动作完全不同。
2.2 绕过Nginx直接测试后端
看完日志只是第一步,接下来要跳过Nginx,直接用curl去访问后端的实际地址。假设Nginx配置里upstream指向的是127.0.0.1:8080,那么就在服务器上执行:
curl -v http://127.0.0.1:8080/注意curl的完整响应和耗时。如果curl访问正常返回HTTP 200,说明后端本身没问题,问题大概率在Nginx配置层面;如果curl也访问不了,要么是服务没起、要么是端口监听有误、要么是防火墙拦截。
这里有个细节值得多说一句:curl测试的时候要看响应头是否完整。有的后端框架在异常情况下会返回一个不完整的响应(比如缺少HTTP状态行),Nginx严格校验响应格式,只要发现不是合法的HTTP响应,就会直接给客户端一个502。而curl对这种畸形响应的容忍度反而更高,有时候curl能通但Nginx报502,原因就在这。
2.3 检查系统资源与文件描述符
如果后端服务存活、网络也通,但502还是间歇性出现,就要考虑系统资源层面的问题了。最典型的两个坑:一个是文件描述符耗尽,一个是TCP连接队列溢出。
查看当前进程的文件描述符占用情况:
ls /proc/$(pidof nginx)/fd | wc -l ulimit -n默认的ulimit -n在很多系统上是1024,对于高并发场景远远不够。Nginx的worker进程每个连接都要消耗一个文件描述符,一旦耗尽,新连接进来就会失败,表现就是访问量一上来就502,人少的时候又恢复正常。
TCP连接队列溢出这个稍微隐蔽一点。后端服务的backlog设置太小,或者accept处理速度跟不上,TCP半连接和全连接队列就会溢出。可以这样查看:
ss -lnt | grep 8080如果Recv-Q的值持续大于backlog的设定值,说明连接积压了。这种情况也会导致Nginx的连接被后端直接丢弃,于是502就产生了。
3. 最常见的几类502诱因,逐个拆解
3.1 后端服务挂了或者根本没起来
最直白的原因:后端服务没在运行。排查方法很简单,用ss或者netstat确认端口监听状态:
ss -lntp | grep 8080如果没有任何输出,说明服务没监听这个端口。再检查一下服务状态,比如systemd管理的服务用systemctl status查看。这里有个隐蔽的坑:有时候服务显示还在运行,但监听的IP不对。比如后端应用只监听了127.0.0.1,而Nginx配置里upstream写的是服务器的内网IP(比如192.168.1.10),那连接请求到达网卡后被拒绝,一样报502。用ss -lnt看监听地址就能发现这个问题。
还有一种是服务启动了就崩。Java应用最常见,内存配置不当导致OOM,进程被系统kill掉,然后你看到502。这时候要去看应用日志或者dmesg系统日志里有没有Out of Memory相关的记录。
3.2 upstream配置错误或权重异常
upstream块里配置的负载均衡策略也可能引发502,虽然不如服务宕机那么直接,但确实困扰了不少人。看一个典型的错误配置:
upstream backend { server 192.168.1.11:8080 weight=1; server 192.168.1.12:8080 weight=1; } server { listen 80; location /api/ { proxy_pass http://backend; } }这个配置本身没问题,但如果你修改过防火墙、或者某台后端机器宕机后没有从upstream里摘掉,Nginx默认会按fail_timeout周期内连续失败max_fails次后,把该节点标记为不可用。问题在于很多人默认配置的max_fails=1,fail_timeout=10s意味着某一台机器只要失败1次,10秒内就不会再往这个节点转发,如果两台机器都这样,upstream组里就会提示no live upstreams。
另外需要注意proxy_pass后面的URI写法。如果proxy_pass http://backend;不带路径,Nginx会把完整的原始URI转发给后端;如果写成proxy_pass http://backend/;带斜杠,则会用location匹配后的路径替换。很多人因为URI拼接问题导致后端收到错误的请求路径返回404,虽然跟502不一样,但排查时容易混淆,建议先把proxy_pass的转发规则梳理明白再往下查。
3.3 连接超时、读取超时设置太短
有时候后端服务没任何问题,就是处理速度慢,Nginx等得不耐烦了。这跟502有关联但不完全一样,严格来说读超时触发的是504,但很多场景下后端是"边处理边断开",表现就成了502。比如你的后端接口需要处理5秒,而proxy_read_timeout默认是60秒,正常情况下不会出问题,但如果有人改成了10秒、5秒,或者后端是一个流式接口,长时间没有数据产生,超时就会到来。
location /api/ { proxy_connect_timeout 10s; proxy_read_timeout 60s; proxy_send_timeout 60s; }我建议这三个超时至少保持默认值或者设置得更宽松一点。connect_timeout可以短一些,因为建立连接通常是快速的;read_timeout要根据业务接口的实际耗时来定,别拍脑袋。我曾经遇到过一个报表导出的接口,处理逻辑本身要跑两分钟,Nginx默认60秒直接切断,后来把read_timeout调到300秒才解决。
3.4 防火墙、安全组拦住了内网请求
Nginx和后端在同一台机器上时很少遇到这个问题,但一旦Nginx和后面那台服务不在一台机器,防火墙就是高频问题源。最常见的表现:Nginx日志里出现connect() timed out或者connect() failed (113: No route to host),而你在Nginx服务器上用curl访问后端又是通的。
为什么curl通、Nginx不通?因为你不是在Nginx服务器上curl的。正确的测试方式是在Nginx所在机器上curl后端的地址,不是在后端服务器上curl自己。很多人在后端服务器上测觉得服务正常,但问题恰恰出在Nginx这台机器到后端服务器的链路。
# 在Nginx所在机器上执行 telnet 192.168.1.11 8080 curl -v telnet://192.168.1.11:8080如果端口连不通,看两件事:一个是操作系统防火墙,Linux上通常是firewalld或iptables;另一个是云厂商的安全组规则。排查时要确认安全组同时放行了入方向和出方向的规则,入方向没放行、出方向被限制,都会导致连接失败。还有一个容易忽略的是后端服务的bind地址,服务如果bind到127.0.0.1,外网IP访问自然被拒。
3.5 PHP-FPM进程耗尽的经典场景
在LNMP架构里,502的经典元凶是PHP-FPM。Nginx本身没问题,PHP代码也没问题,问题是php-fpm的进程池扛不住了。当你访问一个PHP站点时突然报502,同时Nginx错误日志里出现upstream prematurely closed connection或者connect() failed (111: Connection refused) while connecting to upstream,而检查php-fpm进程还活着,那基本就是进程池耗尽了。
php-fpm.conf里几组核心参数:pm.max_children决定最大子进程数,pm.start_servers决定启动时进程数,pm.max_requests表示每个子进程处理多少个请求后自动重启。当所有子进程都在处理请求,新请求进来找不到可用进程时,Nginx往php-fpm的socket或端口发连接,就会被拒绝,502随之而来。
排查方法是看php-fpm的日志和进程状态:
# 查看php-fpm状态页,前提是开启了pm.status_path curl http://127.0.0.1/php-fpm-status如果显示listen queue一直有积压,说明进程池确实不够用。但别急着盲目调大max_children,要结合服务器内存来算。一个PHP-FPM子进程大约占30-60MB内存,你要是配置200个子进程,机器只有8GB内存,很快就会被内存耗尽拖垮,到时候连Nginx都可能一起挂。我常用的调优思路是:先看单进程平均内存,再评估服务器可用内存,然后反推max_children的上限,而不是拍脑袋填大数字。
3.6 keepalive连接复用导致的诡异502
keepalive引发的问题属于那种"偶发、难重现、让人怀疑人生"的类别。Nginx对上游启用了keepalive连接复用后,长连接会被多个请求复用,但如果后端服务器的空闲连接超时时间比Nginx的keepalive_timeout短,就会出现一个诡异场景:Nginx手里握着一条"以为还活着"的连接,实际后端早就把它关了,等Nginx在这条死连接上发请求,后端直接RST或者关闭,于是502。
upstream backend { server 192.168.1.11:8080; keepalive 32; } location /api/ { proxy_http_version 1.1; proxy_set_header Connection ""; }这类问题最大的特征是:频率不高、没有固定规律、重启Nginx后会暂时消失但过一会儿又出现。解决办法有几个方向:第一,让上游的keepalive_timeout大于Nginx的keepalive_timeout,比如后端设置75s,Nginx设置60s,保证后端不会先于Nginx关掉空闲连接;第二,把proxy_set_header Connection ""保持空,让Nginx明确告诉上游这条连接是keepalive的;第三,实在排查不出来就先临时关闭keepalive,通过性能损失换取稳定性,有时候这是最务实的止损手段。
3.7 proxy_buffer设置引发的边界问题
这一类问题我跟同事排查过很多次,隐藏得比较深。Nginx默认会对上游响应做缓冲,proxy_buffer_size控制响应头缓冲区大小,默认是4k或者8k,取决于系统内存页大小和编译参数。如果后端返回的HTTP响应头特别大(比如Cookie太多、Set-Cookie内容很大),超过proxy_buffer_size就放不下了,Nginx可能报upstream sent too big header while reading response header from upstream,这时候返回502的可能性也很高。
location /api/ { proxy_buffer_size 8k; proxy_buffers 8 4k; }遇到这种情况,先把proxy_buffer_size调大到16k或者32k试试。但要注意别无脑调很大,每个缓冲区都是实际内存占用,proxy_buffers数量乘以大小乘以连接数,高并发下内存开销可观。这个坑的典型特征是:小请求正常,某些带有大量Cookie或复杂响应头的请求必现502,而curl因为不带那些请求头反而测不出来。
4. 实战记录:一次生产环境502的完整排查过程
4.1 故障现象与初步判断
去年遇到一次比较典型的线上502事故,可以完整还原一下排查思路。背景是一个电商系统的订单查询接口,前端每过一段时间就有人反馈报502,但刷新一下又好了。监控上看Nginx的qps曲线并没有明显异常,后端服务的CPU和内存也都在正常范围。
我第一反应是去看Nginx错误日志,结果发现大量这样的内容:
[error] 12345#0: *6789 connect() failed (111: Connection refused) while connecting to upstream, client: 10.0.0.8, server: api.example.com, upstream: "http://127.0.0.1:8080"connect() failed (111: Connection refused)非常清晰:Nginx尝试连接127.0.0.1:8080被拒绝。当时第一反应是"后端是不是挂了",但之前监控明明显示进程存活。于是我在服务器上手动确认了一下。
4.2 逐层定位与修复
ps -ef | grep java ss -lntp | grep 8080进程在,端口在监听,看起来服务正常。但诡异的是,手动curl http://127.0.0.1:8080/order/query也报错。这就奇怪了,人不在的时候服务正常,人一多就开始间歇性拒绝连接。我继续看系统层面,发现了一个关键线索。
dmesg | tail -20日志里出现了一堆Out of memory: Kill process相关的记录,某个Java进程因为OOM被系统kill了。但奇怪的是服务还活着,端口还在监听。仔细一看才发现,原来后端部署了多实例,其中一个实例被OOM killer杀掉之后,监控系统自动拉起了一个新进程。而Nginx配置里upstream明确写着127.0.0.1:8080,新进程起来之后没有绑定到8080端口,或者绑定失败,导致一阵一阵的connection refused。
到这里问题就很清楚了:不是Nginx配置的问题,也不是负载均衡策略的问题,而是后端实例内存泄漏导致OOM,然后进程被杀、端口短暂失联,在这段时间窗口里所有打到8080的请求全部502。后来我把后端堆内存参数调大,并给JVM加上了GC日志监控,Nginx侧也加了健康检查脚本,问题才算彻底解决。
4.3 事后复盘与预防
这次事故给我最大的启发是:502的根因往往不在Nginx本身,而是在链路的任何一个环节。排查要按"日志→进程→端口→资源→配置"的层次逐层推进,而不是跳过日志直接去改Nginx配置。
复盘时还发现一个管理上的盲点:监控只覆盖了"进程是否存在",没有覆盖"端口是否持续可用"。进程活着和端口可用是两回事,后来我们额外加了一个端口探活监控,任何时刻端口不可用就会触发告警,这类502的感知时间从"用户反馈"变成了"系统主动发现"。
预防措施方面,我给后端服务增加了两条保障:一是启动脚本里明确绑定端口,启动失败就退出并告警,避免出现"进程活着但端口没监听"的假活状态;二是给Nginx和后端之间的TCP连接加了端口探活脚本,每30秒检测一次,连续失败3次就触发告警。这套组合下去,后续几个月再也没有出现过同类问题。
5. 速查表与多年踩坑经验
5.1 症状、原因、解法对照表
把多年积累的502排查经验整理成一张速查表,按错误日志的关键词快速定位,比从头到尾瞎猜高效得多。
| Nginx错误日志关键词 | 大概率原因 | 优先操作 |
|---|---|---|
| connect() failed (111: Connection refused) | 后端没启动、端口未监听、监听地址不匹配 | 检查后端进程与ss监听状态,确认bind地址 |
| connect() failed (110: Connection timed out) | 网络不通、防火墙/安全组拦截、后端负载过高 | 在Nginx所在机器telnet后端端口,检查防火墙 |
| upstream prematurely closed connection | 后端进程崩溃、PHP-FPM进程耗尽、服务主动断开 | 查后端应用日志与系统dmesg,查php-fpm状态 |
| no live upstreams while connecting to upstream | 所有上游节点被标记不可用 | 检查max_fails/fail_timeout配置与节点健康状态 |
| upstream sent too big header | 响应头超过proxy_buffer_size | 调大proxy_buffer_size与proxy_buffers |
| connect() failed (10061) Windows环境 | Windows下后端服务未启动或端口被占用 | 检查Windows服务与netstat -ano |
这张表只覆盖了最高频的场景,实际工作中还会遇到各种变体,但只要把握住一个核心原则——错误日志会把你引向正确的排查方向,就不要跳过它。
5.2 几个值得记住的排查小习惯
第一个习惯:改配置之前先备份。虽然是老生常谈,但我在排查502时见过太多人一边查问题一边手忙脚乱地改了十几处配置,最后问题没解决,配置却乱成一锅粥。正确做法是每次改之前用nginx -t先验证配置语法,改完用systemctl reload nginx平滑加载,不要用restart。
第二个习惯:善用curl的几个调试选项。curl -v能看到完整请求响应过程,curl -w可以输出耗时详情,curl -H可以模拟特定的请求头。排查502时我经常用这组命令组合拳,把后端返回的原始响应头完整抓下来,判断响应是否畸形。
curl -v -w "time_total: %{time_total}s\n" http://127.0.0.1:8080/health第三个习惯:排查502时不要只盯着一个时间点。502如果是间歇性的,一定要把时间维度拉长。光看当前状态往往发现不了问题,配合监控曲线看"502出现的时间点"和"该时间段后端资源状况"的对应关系,很多疑难杂症都能找到规律。
第四个习惯,也是我个人最深的一个体会:502的排查从来不是Nginx单点问题。它是整个链路健康度的试金石,后端代码卡顿、数据库慢查询、内存泄漏、防火墙规则变更,任何一环出问题都可能表现为502。所以遇到502先别急着怪Nginx,把它当作一个链路信号去排查,反而能更快定位到真正的根因。我这些年最惨的一次教训就是在一台机器上反复调整Nginx参数,折腾了两天,最后发现是后端数据库连接池耗尽,跟Nginx一毛钱关系都没有。
以后再遇到502,照着这个思路走:先看错误日志,再用curl绕过Nginx直测后端,检查进程和端口,确认防火墙和资源,最后才动配置。这套流程走下来,绝大多数502都能在半小时内定位出方向,剩下的那些诡异问题,可能就要往响应头缓冲、keepalive复用这些更深的层次去挖了。