☰
Nginx反向代理502 Bad Gateway实战排查:从日志定位到根因修复的完整路径
2026/9/25 10:09:19 网站建设 项目流程

做Nginx反向代理的同学,一定都见过那行刺眼的502 Bad Gateway白色页面。无论是刚上线的新服务还是跑了几年的老网关,502出现的瞬间,用户的投诉、领导的电话跟着就来。这篇文章我不打算讲那些遍地都是的基础教程,而是把我自己在几个业务线里排查502的真实路径、遇到的高频坑、以及最终落地的预防手段全部摊开,按“看懂错误—系统排查—根因修复—长效预防”的顺序写下来,希望你在Nginx反代报502时,能少走几轮弯路。

1. 502 Bad Gateway的本质:先搞懂错误从哪里来

1.1 一次反向代理请求的完整生命线

先说个最简单的场景:用户浏览器发起请求,Nginx作为反向代理收到请求后,把流量转发给后端应用服务器——比如Java的Tomcat、Python的Gunicorn、Node的PM2进程。

整个交接过程大致是四步:

  1. 浏览器把请求发到Nginx的监听端口(通常80或443);
  2. Nginx根据location和proxy_pass配置,选择一个upstream后端IP和端口;
  3. Nginx向后端发起TCP连接,发送HTTP请求;
  4. 后端处理完成后返回HTTP响应,Nginx再把响应原样交还给浏览器。

502发生的位置,就在第3步到第4步之间。更准确地说:Nginx已经尝试向后端发起通信,但要么连接根本建立不起来,要么后端返回了一个在Nginx看来无效的响应,要么响应头还没读完连接就断掉了。Nginx没办法把有效的响应头回给用户,于是只能自己从5xx里挑一个502状态码返回。

所以502的全称叫Bad Gateway,网关(Nginx)和网关后面的服务(后端)之间通信出了问题。问题一定出在“Nginx与后端之间的这一跳”上,而不是Nginx自身静态文件服务出了问题。搞懂这一点,你就不会在用户浏览器、DNS解析、CDN这些八竿子打不着的地方浪费时间。

1.2 502、503、504到底有什么区别

很多同学分不清这三个状态码,排查时容易走冤枉路。我做个简单的区分:

状态码含义本质差异常见典型病因
502网关收到无效响应或拿不到响应后端给出的响应在Nginx看来是无效的后端没起来、连接被拒、响应被截断
503服务不可用Nginx本身或upstream整体没有可用节点维护中、权重全为0、瞬间过载
504网关超时Nginx等后端等太久了,自己先放弃后端处理太慢、SQL卡死、线程池耗尽

这个区分特别重要。因为502的排查方向聚焦在“连接与响应完整性”,504主要聚焦“超时与性能”。如果错误日志里写着Connection refused,那大概率是后端端口根本没有服务在监听;如果写着timed out,那方向就要转向网络连通性和后端慢查询。

2. 系统化排查路径:别上来就改配置

2.1 第一件事永远是翻Nginx错误日志

我见过太多人一看到502,第一反应是打开nginx.conf改proxy_pass,改完reload,发现还是502,来回折腾一上午。其实Nginx早就把原因写在日志里了,你只需要去看。

# 看当前nginx使用的配置文件路径 nginx -t # 查看error_log配置 nginx -T 2>/dev/null | grep error_log # 默认通常在 /var/log/nginx/error.log tail -f /var/log/nginx/error.log

错误日志里针对502最典型的几种报错,我摘出来逐条看。第一条:

2024/06/12 10:23:01 [error] 12345#0: *789 connect() failed (111: Connection refused) while connecting to upstream, client: 192.168.1.100, server: www.example.com, request: "GET /api/order HTTP/1.1", upstream: "http://127.0.0.1:8080"

这一条信息量已经很大:upstream是127.0.0.1:8080,后端8080端口拒绝了连接。方向直接锁定:检查8080端口到底有没有服务在监听。

第二条:

2024/06/12 10:24:11 [error] 12345#0: *790 upstream prematurely closed connection while reading response header from upstream, upstream: "http://127.0.0.1:9000"

这一条是“后端把连接过早关闭了”。后端确实接受了连接,但还没返回完整的响应头就断了。常见原因是后端进程崩溃、被OOM killer杀掉、或者后端业务代码在返回响应之前异常退出。

第三条:

2024/06/12 10:25:00 [error] 12345#0: *791 no live upstreams while connecting to upstream, upstream: "http://backend_pool"

这一条说明upstream配置的健康节点数为0,所有后端节点都被标记为不可用。这种在负载均衡场景下很典型,后面我专门讲。

2.2 三层验证:Nginx配置、后端状态、网络链路

日志给了方向,但还不够,最好用三层验证把怀疑坐实。

第一层,Nginx侧。确认Nginx配置没有语法问题、proxy_pass路径对不对。运行nginx -t检查,再用curl直接访问Nginx的对外入口,观察返回状态。

第二层,后端侧。绕过Nginx,直接访问后端服务,看后端本身是不是正常的。这一步能区分问题出在Nginx还是后端。

第三层,网络链路。确认Nginx所在机器到后端所在机器的网络通不通,中间有没有防火墙、安全组拦截。特别是云服务器环境,同一台机器还要注意SELinux和iptables。

2.3 完整排查命令清单

整理一份我自己常用的排查顺序,直接复制到服务器一行行执行:

# 1. 检查Nginx配置 nginx -t # 2. 检查Nginx进程 ps -ef | grep nginx # 3. 直接curl Nginx入口 curl -I http://127.0.0.1:80 # 4. 检查后端端口监听 ss -lntp | grep 8080 # 5. 直接curl后端(绕过Nginx) curl -I http://127.0.0.1:8080 # 6. 如果后端在别的机器,检查连通性 telnet 192.168.1.10 8080 # 7. 检查iptables和selinux iptables -L -n | grep 8080 getenforce

这套命令走下来,绝大多数502的根因已经浮出水面,剩下的就是针对具体根因去修。

3. 六大高频根因与实操解法

3.1 根因一:后端服务没起来或端口没监听

最朴实但也是出现频率最高的原因:后端应用崩了、没启动、或者启动后端口没监听。体现在日志上就是connect() failed (111: Connection refused)。

处理步骤:

# 确认端口 ss -lntp | grep 8080 # 如果没有任何输出,说明后端应用没有监听8080,去后端日志找启动失败原因 journalctl -u your-app -n 50 systemctl status your-app

如果是PHP-FPM场景,502的经典原因是php-fpm进程死亡或者socket文件丢失,日志通常长这样:

connect() failed (111: Connection refused) while connecting to upstream, upstream: "fastcgi://127.0.0.1:9000"

解决办法是重新拉起php-fpm:

systemctl start php-fpm

这里的坑在于:php-fpm可能因为pm.max_children设置过小,请求一多全部被占满,新请求直接被拒。这种情况光重启不解决,要调pm.max_children、pm.start_servers。我遇到过一个真实案例,业务高峰期每分钟1000+请求,pm.max_children只给了8,502每小时准时出现,调成32后彻底消失。

3.2 根因二:proxy_pass路径拼接错位

路径问题不容易一眼发现,因为Nginx配置完全合法,nginx -t不报错,但访问后端的实际URL和后端路由对不上,后端返回404,严重时连接被重置,Nginx兜底给了502。

先记住两个关键规则。

不带URI的写法,保留原始URI:

proxy_pass http://127.0.0.1:8080;

带URI的写法,用URI替换匹配部分:

proxy_pass http://127.0.0.1:8080/api;

举个例子。用户访问的是https://www.example.com/bbs/login,配了:

location /bbs { proxy_pass http://127.0.0.1:8080; }

后端收到的是/bbs/login,注意开头斜杠没丢。

但如果配的是:

location /bbs { proxy_pass http://127.0.0.1:8080/; }

后端收到的是/login,/bbs前缀被替换掉了。

很多老项目前后端路由是严格绑定的,少了或多了prefix,后端框架直接404或400。排查办法很简单:打开Nginx的access.log,看$request_uri和upstream_status,前后对照一次就明白。

3.3 根因三:防火墙和安全组拦截

当日志出现connect() failed (113: No route to host)或(111: Connection refused)且确认后端端口在监听时,大几率是防火墙在拦。

云服务器场景尤其要查两个地方:

  1. Linux本机的firewalld或iptables;
  2. 云厂商控制台的安全组规则。

我之前在云ECS上踩过一回:后端部署在另一台内网机器上,telnet内网IP 8080根本不通,最后发现是安全组里忘了放行“内网入方向的8080端口”,只放行了从外部访问的80端口。加上后502秒变200。

检查命令:

firewall-cmd --state firewall-cmd --list-ports iptables -L -n

还要注意SELinux。CentOS/RHEL系列默认enforcing模式下,Nginx连接后端端口可能被拒,日志表现又不太像防火墙。临时验证办法:

setenforce 0

如果关掉后502消失,那就是SELinux策略问题,需要放行:

setsebool -P httpd_can_network_connect 1

3.4 根因四:缓冲区大小不够导致upstream提前断开

这种502的症状很诡异:小接口正常,大接口随机502;本地curl后端一切正常,走Nginx就偶发失败。日志里会出现:

upstream prematurely closed connection while reading response header from upstream

根本原因是响应头太大,超出了proxy_buffer_size的容量,Nginx读不完响应头,而socket缓冲又放不下,触发重置。默认proxy_buffer_size通常是4k或8k,如果后端返回的Set-Cookie、Token、自定义响应头很大,就会顶爆。

解法:

location /api { proxy_pass http://127.0.0.1:8080; proxy_buffer_size 16k; proxy_buffers 4 32k; proxy_busy_buffers_size 64k; }

我总结的调参经验:先把proxy_buffer_size提到16k,看502是否消失。如果响应头特别大,再往上加。别忘了FastCGI场景要用fastcgi_buffer_size和fastcgi_buffers。

3.5 根因五:超时配置太紧

502里也混着一部分其实是超时引起的,尤其在后端处理时间波动较大的场景。Nginx和后端之间的三个超时参数对应三个阶段:

参数对应阶段默认值
proxy_connect_timeout建立连接60秒
proxy_send_timeout发送请求60秒
proxy_read_timeout等待响应60秒

如果后端接口本身就要跑几十秒,而代理层默认的read_timeout还是60秒,那请求基本一到临界点就被Nginx掐断。报表导出、批量数据处理这类接口特别容易踩。

合理配置:

location /report { proxy_connect_timeout 5s; proxy_send_timeout 10s; proxy_read_timeout 300s; }

注意connect_timeout不要设太大,5秒足够;真正应该放宽的是read_timeout,因为后端计算需要时间。我见过有人把三个都设成600s,结果后端宕机时Nginx也傻等十分钟,体验极差。连接建立失败要快速失败,读响应可以耐心等。

3.6 根因六:keepalive配置不完整

这是最容易被忽略的。很多生产环境为了性能,在upstream里加了keepalive,但没配proxy_http_version和Connection头,导致客户端复用和后端的长连接失效,出现间歇性502或者连接被重置。

正确的长连接配置样板:

upstream backend_pool { server 127.0.0.1:8080 weight=1 max_fails=2 fail_timeout=30s; keepalive 32; } server { listen 80; location / { proxy_pass http://backend_pool; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }

这里的关键是proxy_http_version 1.1和proxy_set_header Connection "";两者缺一不可。缺少Connection头,后端可能认为这是短连接,处理完就关闭,Nginx复用到一半发现连接已失效,客户端就看到502。

keepalive不要乱调大,我一般是32起步,再根据worker_connections和后端并发模型微调。后端是单线程阻塞模型时,keepalive太大反而占满后端线程池。

4. 负载均衡和高并发场景下特有的502形态

4.1 后端重启瞬间的雪崩

单台后端没有这个问题,但在upstream里有多个后端节点时,只要其中一个节点重启,处于该节点上的活跃连接会立即断开,Nginx对这些请求返回502。如果重启操作是滚动进行的,那502的窗口会更大。

这里有三个动作可以显著减少窗口期:

  1. 后端进程停止前,先通过负载均衡或者注册中心把它摘流;
  2. Nginx的upstream配置里给每个节点设置合理的max_fails和fail_timeout;
  3. 后端应用启动后做健康检查,通过后再把流量放回去。

如果用的是Nginx Plus或者OpenResty,可以依赖主动健康检查模块。开源版Nginx做不了真正意义上的主动探测,除非编译第三方模块,所以最实用的办法还是靠被动健康检查加后端优雅上下线脚本。

4.2 max_fails和fail_timeout的设置

默认情况下max_fails=1,fail_timeout=10s。含义是:10秒内失败1次,就把这个节点标记为不可用,接下来10秒都不再往这个节点转发流量。

这个默认值看起来合理,但高并发场景下你就会发现:一次偶发的超时(比如后端GC停顿)就会触发摘除,10秒内的流量全部压往其他节点,其他节点可能因此过载雪崩,继而也被摘除,最后出现no live upstreams。

我的建议是按业务特性调:

upstream backend_pool { server 192.168.1.11:8080 max_fails=3 fail_timeout=30s; server 192.168.1.12:8080 max_fails=3 fail_timeout=30s; keepalive 32; }

max_fails设成3,fail_timeout设成30s,既不会因为小抖动立刻摘节点,也不会让坏节点带病工作太久。

4.3 长连接、健康检查与连接池的配合

高并发502有一个非常隐蔽的形态:后端一切正常,但日志刷Connection reset by peer。这往往不是后端崩溃,而是后端有连接数上限,比如Tomcat maxThreads、Gunicorn的worker数、MySQL连接池,Nginx复用连接过多过快,把后端连接数打满了。

排查思路是:先看后端的连接数监控和错误日志,确认是否有连接超限记录。再回头看Nginx的keepalive参数是否合理。如果后端的worker数只有10,前端keepalive却配了64,那高峰期必然出问题。把keepalive降到和后端worker数接近的水准,比如16到24,往往立竿见影。

另外nginx_upstream_check_module这种主动健康检查模块,可以在请求发起前就探活,把不健康的节点摘掉。但编译配置比较麻烦,如果要快速解决,用被动健康检查配合监控告警也一样够用。

4.4 后端响应慢引起的连锁502

还有一种高并发场景容易被忽略:后端响应变慢,Nginx等待超时后主动断开,但后端的实际业务还在继续执行,数据库连接、线程、内存都被占用。等到Nginx侧认为这个节点fail了,又把流量切到其他节点,其他节点也开始变慢,最终整个集群雪崩。

面对这种问题,光调大proxy_read_timeout解决不了根本,反而会让Nginx占住更多连接。正确思路是:把后端接口的P99耗时监控起来,设置性能基线,一旦响应变慢就告警,让研发去优化接口本身。Nginx侧的timeout只能兜底,不能救命。

5. 生产环境的长效预防:把502消灭在发生之前

5.1 监控告警:先于用户发现问题

502一旦大面积出现,用户感知是灾难性的,所以告警必须先于用户。我在生产里给Nginx入口的每个server都加了状态码监控,重点盯5xx比例。

具体做法:

  1. 从access.log里用脚本统计每分钟5xx请求占比;
  2. 超过阈值,比如1%,触发告警;
  3. 告警信息自带时间范围和具体URI,方便快速定位是哪个接口出了问题。

如果access.log里没有upstream_status,建议加上。log_format可以这样配置:

log_format access '$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 ' 'request_time=$request_time ' 'upstream_response_time=$upstream_response_time';

把upstream_addr和upstream_status打出来,502发生的时候你能第一时间知道是哪个后端节点的问题,而不是在多个节点之间瞎猜。

5.2 优雅重启与上线流程

后端要发布升级,直接kill进程是502的最快产生方式。正确的优雅下线流程应该是:

  1. 在Nginx upstream里把该节点的weight调成0,reload;
  2. 等该节点上的存量请求全部返回;
  3. 停止后端进程,发新版本,启动;
  4. 确认新进程健康后再把weight调回去,reload。

这个流程对没有注册中心的传统架构特别有用。用脚本把reload封装好,上线集群成员时不要一次性全部替换,要一批一批来,保证任何时候都有可用节点。

5.3 常见问题速查表

最后把各类502的表现、日志特征和直接解法整理成一张表,方便团队wiki留存:

日志关键词直接原因优先处理方向
connect() failed (111: Connection refused)端口无服务启动或拉起后端服务
connect() failed (110: Connection timed out)网络不通检查防火墙、安全组
connect() failed (113: No route to host)链路不可达检查路由、安全组
upstream prematurely closed connection响应被截断检查后端进程是否崩溃、缓冲区是否太小
no live upstreams while connecting所有节点被摘除检查后端整体状态与max_fails设置
Connection reset by peer连接被后端重置检查后端连接数、线程池是否打满

排查顺序还是那句话:先看error.log判断列别,再按列别做对应处理,而不是没有章法地改配置。Nginx reload之前先nginx -t确认语法,避免改完配置又叠加一个新的503出来。

这套流程我在实际业务里跑了很久,也是被线上事故一轮一轮逼出来的。502本身并不可怕,可怕的是没有排查思路,东改一下西改一下,把线上环境越搞越复杂。真到了处理502的时候,第一件事把错误日志翻出来,再按这里的路径一步步来,大多数问题都能在十分钟内定位清楚。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询