1. 从一次线上告警说起:502到底意味着什么
凌晨两点,手机开始疯狂震动。监控平台推送过来一条告警:核心网关的5xx错误率在五分钟内从0.02%飙升到37%。打开大盘一看,清一色的502 Bad Gateway。这个状态码对运维和后端同学来说再熟悉不过了,但它背后的成因却可能横跨网络、系统、应用、配置四个层面,排查起来非常考验基本功。
先把概念说清楚。502 Bad Gateway的本质是:网关或代理服务器(这里就是Nginx)作为中间人,向上游服务器(upstream)发起请求后,收到了一个它认为无效的响应,或者压根没收到响应。注意,是Nginx"认为"无效,这个判断标准很关键。它可能是上游直接拒绝连接,可能是上游返回了格式错误的响应头,也可能是上游处理超时后Nginx主动断开。所以502不是一种病,而是一类症状,就像发烧一样,病因可能十几种。
这篇文章适合谁看?如果你正在维护Nginx网关、负责后端服务的稳定性,或者你是个开发同学,线上服务偶尔被网关甩个502过来却不知道从哪查起,那这篇内容就是写给你的。我会把整个排查过程拆成一条可复用的路径,从最外层的日志开始,一层层往里剥,每一步都告诉你为什么这么查、查到了怎么判断、判断完怎么处理。中间会穿插我自己踩过的坑和一些容易被忽略的细节,比如内核参数、文件描述符、DNS解析缓存这些平时不太注意但关键时刻要命的东西。
整个排查的核心思路就一句话:先定位502是Nginx自己产生的,还是上游真实返回的,然后再顺着连接链路逐段排查。听起来简单,但实际操作中很多人一上来就去看后端日志,结果方向完全错了。下面我按真实排查顺序展开,你可以直接照着这个流程走。
2. 排查前的准备工作:先把日志和监控用起来
2.1 确认Nginx错误日志的级别和格式
很多团队Nginx的error_log级别默认是error,这个级别其实已经能记录502相关的信息了,但如果你想要更细的上下文,比如上游的连接耗时、具体是哪台后端出的问题,就需要把日志格式调一下。我习惯在http块里自定义一个log_format,把upstream_addr、upstream_status、upstream_response_time、request_time这几个变量都带上。这样一条访问日志就能告诉你:请求打到了哪台后端、后端返回了什么状态码、后端处理了多久、Nginx总共花了多久。
log_format detailed '$remote_addr - $remote_user [$time_local] ' '"$request" $status $body_bytes_sent ' '"$http_referer" "$http_user_agent" ' 'upstream_addr=$upstream_addr ' 'upstream_status=$upstream_status ' 'upstream_response_time=$upstream_response_time ' 'request_time=$request_time';这个格式配好之后,排查502的时候直接grep一下访问日志里upstream_status为502或者upstream_addr为空的记录,就能快速判断问题出在哪一段。如果upstream_status是502,说明后端确实返回了502,问题在后端;如果upstream_status是"-",说明Nginx根本没拿到后端的响应,问题在连接层。
2.2 监控指标要覆盖的三个维度
光有日志还不够,排查502需要监控提供趋势和分布。我建议至少覆盖三个维度:第一是Nginx自身的连接指标,包括active connections、reading、writing、waiting这四个值的变化趋势;第二是upstream的健康指标,每台后端的响应时间和错误率;第三是系统层指标,包括TCP连接数、TIME_WAIT数量、文件描述符使用率、CPU和内存。
这三个维度缺一不可。我遇到过好几次情况是Nginx层看起来正常,但系统层的TIME_WAIT数量爆了,导致新连接建立缓慢,最终表现为间歇性502。如果只看Nginx监控,根本发现不了。
提示:监控采集频率建议至少10秒一次,502这种问题往往来得快去得也快,采集间隔太长会漏掉关键窗口。
2.3 准备一份上游服务清单
排查之前,先把当前Nginx配置里所有的upstream块整理出来,包括每台后端的IP、端口、权重、健康检查配置。这份清单在排查过程中会反复用到,尤其是当你需要逐台后端去验证连通性的时候。我一般会直接从nginx.conf里把upstream段提取出来,配合配置管理工具生成一份最新的拓扑图,避免因为配置变更导致排查时信息过时。
3. 第一层排查:从Nginx错误日志定位502的直接原因
3.1 读懂错误日志里的关键信息
Nginx的error_log里关于502的记录通常长这样:
2024/01/15 02:13:47 [error] 12345#0: *987654 connect() failed (111: Connection refused) while connecting to upstream, client: 10.0.1.100, server: api.example.com, request: "GET /v1/order HTTP/1.1", upstream: "http://10.0.2.10:8080/v1/order", host: "api.example.com"这条日志信息量很大。connect() failed说明Nginx在尝试建立到上游的TCP连接时就失败了,括号里的111: Connection refused是系统错误码,表示目标端口没有进程在监听。upstream字段告诉你是哪台后端,request字段告诉你是哪个接口。如果是upstream timed out,那就是连接建立了但后端迟迟不返回数据。如果是no live upstreams,说明健康检查把所有后端都摘掉了。
我一般会先用一条命令把错误日志里的502相关记录按错误类型聚合一下:
grep -E "502|upstream" /var/log/nginx/error.log | \ awk -F'[()]' '{print $2}' | sort | uniq -c | sort -rn这样能快速看出是连接拒绝占多数,还是超时占多数,还是其他类型。不同类型的处理方向完全不同。
3.2 区分几种典型的502错误模式
根据错误日志,502通常可以归为以下几类,每类的排查方向不一样:
| 错误日志关键词 | 含义 | 排查方向 |
|---|---|---|
| connect() failed (111: Connection refused) | 上游端口无监听 | 检查后端进程是否存活、端口是否被防火墙拦截 |
| upstream timed out | 上游响应超时 | 检查后端处理耗时、Nginx超时配置 |
| no live upstreams | 所有后端被健康检查摘除 | 检查健康检查配置和后端实际状态 |
| upstream sent invalid header | 上游返回了非法响应头 | 检查后端框架的响应头设置 |
| recv() failed (104: Connection reset by peer) | 上游主动断开连接 | 检查后端是否崩溃、是否触发了OOM |
| worker_connections are not enough | Nginx自身连接数不够 | 调整worker_connections和worker_rlimit_nofile |
这张表建议收藏,排查的时候对着错误日志里的关键词直接定位,能省很多时间。我见过不少同学一看到502就去看后端日志,结果错误日志里明明写着Connection refused,后端进程早就挂了,看后端日志当然什么都看不到。
3.3 用stub_status模块快速看Nginx自身状态
如果错误日志里没有明显的上游错误,那就要怀疑Nginx自身是不是出了问题。开启stub_status模块后,访问一个指定路径就能看到当前连接状态:
location /nginx_status { stub_status on; allow 127.0.0.1; deny all; }返回的内容里,Active connections是当前活跃连接数,Reading是正在读取请求头的连接数,Writing是正在写响应的连接数,Waiting是保持空闲的连接数。如果Waiting特别大而Writing很小,说明大量连接卡在等待上游响应,问题大概率在上游。如果Reading很大,说明请求头读取都卡住了,可能是客户端网络问题或者Nginx worker不够用。
4. 第二层排查:连接层与系统层的问题定位
4.1 检查上游端口的连通性
确认了错误日志指向连接问题后,第一步就是在Nginx所在机器上直接测试到上游的连通性。用telnet或者nc都可以:
# 测试TCP连接是否可达 nc -zv -w 3 10.0.2.10 8080 # 或者用curl直接请求上游,绕过Nginx curl -v --connect-timeout 3 http://10.0.2.10:8080/health如果nc显示Connection refused,那基本可以确定是后端进程没起来或者端口没监听。如果nc能通但curl超时,说明TCP层通了但应用层没响应,可能是后端线程池满了或者卡在某个慢查询上。如果nc直接超时,那就要查网络层了,比如安全组规则、iptables、路由表。
注意:测试的时候一定要从Nginx所在的机器发起,不要从你自己的办公电脑测。很多网络策略是单向的或者只针对特定源IP放行的,从错误的源地址测试会得出完全错误的结论。
4.2 系统层的关键指标排查
连接层的问题很多时候根子在系统层。我整理了几个必查的系统指标:
文件描述符限制。Nginx每个连接都要占用一个文件描述符,如果worker_rlimit_nofile设置得太小,或者系统级的fs.file-max不够,新连接就建立不了。用ulimit -n看当前限制,用cat /proc/sys/fs/file-nr看系统级使用情况。
TCP连接队列。net.core.somaxconn控制accept队列的最大长度,如果这个值太小,高并发时新连接会被丢弃。net.ipv4.tcp_max_syn_backlog控制SYN队列长度。这两个值在流量突增时特别关键。
TIME_WAIT数量。用ss -s或者netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'查看。如果TIME_WAIT数量过大,会占用大量本地端口,导致Nginx无法建立新的上游连接。可以通过开启net.ipv4.tcp_tw_reuse来缓解,但要注意这个参数只对出站连接有效。
内存和OOM。用dmesg | grep -i oom检查是否有进程被OOM Killer干掉了。后端进程如果因为内存不足被杀,Nginx这边就会看到Connection refused。
4.3 一个真实的排查案例
之前遇到过一次502,错误日志里全是connect() failed (111: Connection refused),但后端进程明明活着。后来发现是后端服务的监听队列满了,新连接被内核拒绝。具体原因是后端的accept线程处理太慢,导致SYN队列溢出。用netstat -s | grep -i listen看到大量的"listen queue overflow"计数。解决办法是调大net.core.somaxconn和后端框架的backlog参数,同时优化accept线程的处理逻辑。
这个案例说明一个问题:Connection refused不一定代表进程挂了,也可能是队列满了。所以看到这个错误码,先确认进程状态,再确认队列情况,最后才下结论。
5. 第三层排查:Nginx配置与超时参数的深度检查
5.1 超时参数之间的联动关系
Nginx和上游之间的超时涉及好几个参数,它们之间有优先级和联动关系,配错了很容易导致502。核心参数有这几个:
proxy_connect_timeout:Nginx与上游建立TCP连接的超时时间,默认60秒。proxy_send_timeout:Nginx向后端发送请求的超时时间,默认60秒。proxy_read_timeout:Nginx等待后端返回响应的超时时间,默认60秒。proxy_next_upstream_timeout:重试下一个上游的总超时时间。proxy_next_upstream_tries:重试次数。
这几个参数的关系是:如果proxy_read_timeout设得太短,后端处理时间稍微长一点就会触发超时,Nginx会断开连接并返回502。如果设得太长,后端真的挂了的时候,Nginx会一直等,占用连接资源。我一般会根据接口的P99响应时间来设置,通常是P99的2到3倍。
5.2 proxy_next_upstream的配置陷阱
proxy_next_upstream决定了在什么情况下Nginx会把请求转发到下一台后端。默认值是error timeout,意思是连接错误和超时才重试。但很多人不知道的是,如果配置了proxy_next_upstream http_502,那么当一台后端返回502时,Nginx会自动重试下一台。这个配置在有多台后端的时候非常有用,但要注意它只对幂等请求安全。
proxy_next_upstream error timeout http_502 http_503 http_504; proxy_next_upstream_tries 3; proxy_next_upstream_timeout 10s;这里有个坑:如果所有后端都返回502,Nginx重试完所有后端后,最终返回给客户端的还是502。所以这个配置只能缓解单台后端故障,不能解决全局性问题。另外,proxy_next_upstream_tries设得太大,在全局故障时会放大请求量,形成雪崩。
5.3 缓冲区配置不当导致的502
proxy_buffer_size和proxy_buffers这两个参数控制Nginx读取上游响应的缓冲区大小。如果上游返回的响应头超过了proxy_buffer_size,Nginx会报upstream sent too big header错误,然后返回502。这个错误在错误日志里很明确,但容易被忽略。
proxy_buffer_size 16k; proxy_buffers 8 16k; proxy_busy_buffers_size 32k;我一般会把proxy_buffer_size设成16k,对于大多数场景够用了。如果后端返回的Cookie或者自定义头特别大,可能需要调到32k甚至64k。这个值不是越大越好,每个连接都会分配这些缓冲区,设太大在并发高的时候会吃光内存。
6. 第四层排查:上游服务自身的问题定位
6.1 后端日志与Nginx日志的时间对齐
如果Nginx错误日志显示的是upstream timed out或者recv() failed,那问题基本可以确定在后端。这时候需要把Nginx的访问日志和后端的应用日志按时间对齐来看。关键是对比同一个请求在两边的时间戳和耗时。
我通常会用request_id来做关联。在Nginx里生成一个唯一的request_id,通过请求头传给后端,后端在日志里也记录这个ID。这样排查的时候直接grep这个ID,就能把整个链路的日志串起来。没有request_id的话,只能靠时间戳和接口路径去猜,效率低很多。
6.2 后端常见的502诱因
后端导致502的原因五花八门,我按出现频率排个序:
进程崩溃或重启。这是最常见的。后端进程因为未捕获的异常、内存溢出、死锁等原因挂掉,Nginx这边立刻就是Connection refused。排查方法是看后端进程的存活监控和重启记录。
线程池或连接池耗尽。后端处理请求的线程池满了,新请求排队等待,超过Nginx的proxy_read_timeout后就变成502。这种情况后端日志里通常没有错误,因为请求根本没被处理。需要看线程池的活跃数和队列长度。
慢查询或外部依赖超时。后端在处理请求时调用数据库或者第三方接口,对方响应慢,导致整个请求处理时间超过Nginx的超时设置。这种502的特点是间歇性的,跟特定接口相关。
GC停顿。对于Java类应用,Full GC会导致所有线程停顿,停顿期间无法响应任何请求。如果GC停顿时间超过Nginx超时,就会502。需要看GC日志和停顿时间监控。
文件描述符耗尽。后端进程打开的文件或连接数达到上限,无法接受新连接。用lsof -p <pid> | wc -l查看。
6.3 用压测复现和验证
如果502是间歇性的,很难抓到现场,可以考虑在低峰期用压测工具复现。用wrk或者ab对特定接口施压,同时观察Nginx错误日志和后端监控。压测的时候要逐步增加并发,找到触发502的临界点,然后分析那个临界点对应的系统指标变化。
提示:压测一定要在预发布环境或者低峰期做,并且提前通知相关同学。我见过有人在生产环境直接压测,结果把整个服务打挂了,502变成了全面不可用。
7. 常见问题速查表与避坑经验
7.1 502排查速查表
| 现象 | 可能原因 | 快速验证方法 | 处理方向 |
|---|---|---|---|
| 错误日志大量Connection refused | 后端进程挂掉或端口未监听 | nc -zv测试端口 | 重启后端、检查进程监控 |
| 错误日志大量upstream timed out | 后端处理慢或超时配置过短 | 对比后端P99耗时和proxy_read_timeout | 优化后端或调大超时 |
| 错误日志no live upstreams | 健康检查失败摘除所有后端 | 查看健康检查日志 | 修复后端健康状态、调整检查策略 |
| 错误日志upstream sent too big header | 响应头超过缓冲区 | 检查后端返回的响应头大小 | 调大proxy_buffer_size |
| 错误日志worker_connections are not enough | Nginx连接数不够 | 查看stub_status的Active connections | 调大worker_connections和nofile |
| 无错误日志但客户端收到502 | 可能是上游返回的502 | 查看访问日志的upstream_status | 排查后端应用逻辑 |
| 间歇性502且与特定接口相关 | 后端慢查询或外部依赖超时 | 对比接口耗时监控 | 优化慢查询、增加熔断 |
| 流量突增时大量502 | 系统层队列溢出或资源不足 | 查看somaxconn和TIME_WAIT | 调内核参数、扩容 |
7.2 几个容易踩的坑
坑一:只看Nginx日志不看系统日志。有一次502排查了半天,Nginx错误日志里只有零星的timeout,最后发现是系统内存不足触发了OOM Killer,把后端进程杀了。dmesg里清清楚楚记录着,但一开始没人想到去看。
坑二:忽略DNS解析问题。如果upstream里配置的是域名而不是IP,Nginx会在启动时解析一次然后缓存。如果后端IP变了但Nginx没重载,就会一直往旧IP发请求,导致502。解决办法是配置resolver并设置合理的valid时间,或者直接用IP。
坑三:健康检查配置过于激进。健康检查间隔太短、失败阈值太低,后端稍微抖动一下就被摘除,导致所有后端都被摘掉,Nginx返回no live upstreams。我一般会把失败阈值设成3次,间隔5秒,给后端足够的恢复时间。
坑四:proxy_next_upstream重试放大了故障。全局故障时,每个请求都重试3次,请求量变成原来的3倍,把本来还有一口气的后端彻底打挂。所以重试次数要谨慎设置,并且配合熔断机制。
坑五:忘了检查Nginx的worker进程数。worker_processes设成auto通常没问题,但如果设成了固定值且小于CPU核数,高并发时worker不够用,连接处理不过来。用nproc确认核数,确保worker_processes匹配。
7.3 我个人的排查习惯
我排查502一般遵循"从外到内、从粗到细"的顺序:先看监控大盘确认影响范围和时间窗口,再看Nginx错误日志定位错误类型,然后根据错误类型决定是查连接层还是应用层,最后用具体命令验证假设。整个过程控制在15分钟内,如果15分钟还没定位到根因,就先做止损操作(比如重启后端、扩容、切流量),然后再慢慢分析。
止损和排查要并行,不要为了找到根因让服务一直挂着。我见过有同学非要找到根因才肯处理,结果502持续了半个小时,影响面扩大了好几倍。先恢复服务,再复盘根因,这个顺序不能反。
另外,每次排查完我都会把过程和结论记录到故障库里,包括错误日志片段、关键命令、根因、处理措施。下次遇到类似问题,直接搜故障库就能找到参考,效率高很多。这个习惯坚持了几年,现在团队里新同学遇到502,先查故障库,大部分常见问题都能自己解决。