1. 502 Bad Gateway到底是什么:先搞清楚Nginx在替你背什么锅
碰到502的第一个反应别急着改配置。这个错误码翻译过来是"Nginx作为网关,没能从上游服务器拿到有效响应"。也就是说,Nginx本身活得好好的,倒霉的是它后面那台真正的业务服务器——可能是Tomcat、Gunicorn、Node.js、PHP-FPM,也可能是一台内网API服务。
很多人一上来就查Nginx的error.log,结果发现日志空空如也,然后就开始怀疑Nginx配置写错了。其实方向反了。502的本质是链路问题,Nginx只是链路中间那个传话的人。上游服务器连接不上、连接上了但响应超时、响应内容非法,这三种情况最终都会以502的形式呈现在客户端面前。你要做的不是盯着502本身,而是沿着请求路径一级一级往下找,直到定位到那个真正断掉的地方。
我见过最典型的误判场景:开发环境一切正常,上线后偶尔502,过几秒自己又好了。开发同学一口咬定是Nginx配置有问题,运维查了半天发现Nginx配置压根没动过,最后定位到是后端服务在线程池耗尽时拒绝新连接,Nginx这边只能返回502。所以这句话得记住:502是结果,不是原因。
排查502前,先确认你的代理链路长什么样。一条典型链路可能是:客户端 -> Nginx(80/443) -> 反向代理目标(127.0.0.1:8080) -> 应用服务。Nginx只是中间人,它自己不会主动产生业务错误。如果Nginx与上游之间用的是TCP连接,那就要检查端口连通性;如果用的是Unix Socket,那就要检查socket文件权限;如果上游是域名,那还要检查DNS解析是否间歇性失败。不同链路的排查路径完全不同,先画清楚链路图再动手,能省掉大量无效操作。
2. 从日志到抓包:一套能复现的502排查链路
2.1 第一步:把Nginx的日志调到"会说话"的级别
默认的Nginx日志配置往往什么关键信息都看不出来,只有一行upstream prematurely closed connection while reading response header from upstream,信息量太少。排查502的第一步,先把日志格式和级别调整好,让日志告诉我们更多细节。
在nginx.conf的http块里配置一个带upstream耗时和上游状态码的log_format:
log_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_status $upstream_response_time ' '$request_time $upstream_cache_status'; access_log /var/log/nginx/access.log main;这一行配置的价值在于:$upstream_addr能直接告诉你Nginx把请求转发到了哪个上游地址;$upstream_status能告诉你上游返回了什么状态码——如果上游返回500,但Nginx返回502,那问题出在上游;如果$upstream_status是-,说明连接压根没建立成功;$upstream_response_time能帮你判断是超时还是秒回。
改完配置后用nginx -t检查语法,再nginx -s reload,然后复现一次502,打开access.log看这一行的完整输出。正常情况下你能立刻得出结论:问题在上游服务本身,还是在Nginx与上游之间的连接建立阶段。
2.2 第二步:快速判断上游是"死了"还是"活着"
假设access.log里显示$upstream_status为空,$upstream_addr正确,那基本可以确定是连接建立失败。这时候手动验证上游服务状态,别只依赖浏览器刷新,因为浏览器缓存会骗人。
用curl直接请求上游地址:
curl -v http://127.0.0.1:8080/health如果上游是Unix Socket,用:
curl -v --unix-socket /run/php-fpm/www.sock http://localhost/health这一步能区分两种情况:curl秒回正常响应,说明Nginx到上游的通道出了问题,重点查Nginx配置里的proxy_pass写法、防火墙规则、上游监听地址是否写错;curl直接connection refused或者超时,说明上游服务根本没起来,或者起来后 hang 住了,这时需要去查上游服务的日志和监听状态。
检查监听端口的典型命令:
ss -tlnp | grep 8080 ss -lx | grep php-fpm如果端口在监听但TCP连接是半开状态(比如用ss -tn state established看到大量SYN-SENT),那可能是上游服务accept队列满了,也就是经典的backlog溢出问题。
2.3 第三步:防火墙、SELinux和内核参数这三座大山
很多502故障是环境层面的,配置怎么改都没用,因为压根没走到应用那一步。在Linux服务器上,三个最常见的隐形杀手是iptables/nftables规则、SELinux策略、以及内核参数中的连接跟踪表溢出。
先检查防火墙规则是否放行了Nginx到上游的端口:
iptables -L -n | grep 8080如果服务器在云上,还要检查安全组,云安全组拦掉的流量在操作系统内部连日志都看不到。SELinux方面,如果开启的是 enforcing 模式,Nginx默认被限制只能代理到特定端口,可以临时用setsebool -P httpd_can_network_connect 1放行,或者干脆用getenforce确认状态后针对性地调整策略。
另外注意nf_conntrack表溢出问题,这个坑特别隐蔽。当服务器连接数太多、conntrack表被占满时,新连接直接丢包,表现就是客户端间歇性502、服务端日志没有任何异常。查看方式:
sysctl net.netfilter.nf_conntrack_max sysctl net.netfilter.nf_conntrack_count dmesg | grep nf_conntrack如果nf_conntrack_count接近nf_conntrack_max,就说明连接跟踪表要爆了,需要调大nf_conntrack_max并缩短超时时间。这个故障在并发量上来后特别容易触发,而且毫无规律,属于那种排查到怀疑人生的类型。
2.4 第四步:抓包一锤定音
当日志层面看不出问题时,该上抓包工具了。在Nginx服务器上抓环回接口流量,因为Nginx和上游通常在同一台机器上:
tcpdump -i lo -nn port 8080 -w /tmp/upstream.pcap然后在客户端复现一次502,Ctrl+C停止抓包后用wireshark打开。重点看TCP握手是否完成、Nginx是否发了请求数据、上游是否有响应。如果看到TCP握手三次完成但紧接着RST,那说明上游accept后立即主动断开——典型的上游服务崩溃前兆;如果握手都没完成,说明防火墙把SYN包吞了;如果三次握手正常但HTTP层没有响应,那就是应用层超时。
抓包这一步对经验不足的人来说有点门槛,但它能一次性定位到"TCP层完全正常,问题在应用层"这样的结论,少走大量弯路。如果连tcpdump都不会装,那就先在本地起一个最简单的Nginx作为上游,用curl直连,先把"上游正常时Nginx能正常代理"这个基准建立起来,再逐步替换回真实上游,通过二分法缩小范围。
3. 六类高频根因与对应修复方案
3.1 上游服务崩溃或未启动:最常见但也最好解决
这是502故障里占比最高的一类。后端进程因为OOM被kill、代码异常导致进程退出、发布脚本没拉起来新进程,都会让Nginx的proxy_pass指向一个不存在的服务。
检查方法简单粗暴:ss -tlnp | grep <端口>,端口没监听就是进程没了;端口在监听但进程处于僵死状态,用ps -ef | grep <进程名>看状态,再查上游自己的日志确认崩溃原因。
这类问题的长期解法不是修改Nginx,而是给上游配置守护机制。systemd服务的话,确保Restart=always;容器环境则配置restart policy和健康检查。Nginx这边能做的只是在504/502时返回一个更友好的错误页面,比如:
error_page 502 /502.html; location = /502.html { root /usr/share/nginx/html; internal; }但这不是根治,只是让用户看到的好看点而已。
3.2 上游连接超时:Nginx等待过久,主动放弃
Nginx默认的proxy_connect_timeout是60秒,proxy_read_timeout也是60秒。当上游处理请求很慢,比如某个接口需要执行一个90秒的数据库查询,Nginx会在60秒后主动断开连接,客户端收到502。
日志特征:upstream timed out while reading response header from upstream。
修复方式看场景。如果是上游确实需要长时间处理,那就显式调大超时:
location /api/ { proxy_pass http://backend; proxy_connect_timeout 5s; proxy_read_timeout 120s; proxy_send_timeout 30s; }注意connect_timeout不建议设太大,5秒足够,因为TCP连接建立本身就不该超过几秒。真正要大的是read_timeout和send_timeout,这两个默认值在慢业务场景下很容易成为瓶颈。
但更要反思上游为什么这么慢。如果同一个接口偶尔快偶尔慢,去查数据库慢查询;如果一直慢,说明接口本身设计有问题,Nginx调参只是缓冲,不是解药。
3.3 Unix Socket文件权限与路径问题:PHP-FPM专属大坑
Nginx代理PHP-FPM时使用的经常不是TCP端口,而是Unix Socket文件,比如/var/run/php-fpm/www.sock。这类502的经典报错是:
connect() to unix:/var/run/php-fpm/www.sock failed (13: Permission denied)原因几乎都是权限不匹配。Nginx worker进程以nginx用户运行,但socket文件的所有者却是www用户或者root,导致没有权限连接。
解决方案二选一:
- 修改php-fpm的socket文件权限,让它对nginx用户可读写。编辑
www.conf:
listen.owner = nginx listen.group = nginx listen.mode = 0660- 或者将Nginx的worker进程改为与PHP-FPM相同的用户,修改
nginx.conf的user指令:
user www;另外注意socket文件路径别写错。很多人把proxy_pass http://unix:/var/run/php-fpm/www.sock;写成了proxy_pass http://unix:/var/run/php.sock;,路径对不上,Nginx连文件都找不到。写完后用ls -l确认socket文件真实存在,而不是用test -f来判断——socket文件在shell里看起来是存在,但传统-f参数对它可能判断异常,建议直接用ls -l或者ss -lx看监听状态。
3.4 上游返回非法或空响应:最隐蔽的一类
有一种502,Nginx日志写着:
upstream prematurely closed connection while reading response header from upstream而你的上游服务日志显示什么都没发生,这通常意味着上游进程在处理请求时被操作系统强杀,或者是连接被上游主动关闭但没发送任何HTTP响应。
典型场景:PHP-FPM进程执行了超出max_execution_time限制的脚本被强杀;Java应用OOM后被JVM自杀;Python服务因为GIL问题或未捕获异常导致worker退出。这时Nginx情绪稳定,它只是如实地把上游的异常转述给了客户端。
定位这类问题需要开启上游自己的错误日志。以PHP-FPM为例,php_admin_value[error_log]和log_errors要配置好,否则报错全被吞掉。Java应用则要看jvm日志和gc日志,特别关注OutOfMemoryError和Unable to create new native thread这类关键行。这类问题光在Nginx层面打转是找不到出口的,必须联调到上游应用的日志体系。
3.5 DNS解析问题:upstream域名动态解析的坑
配置文件里proxy_pass如果填的是域名:
proxy_pass http://api.internal.example.com:8080;Nginx在启动时解析一次DNS并缓存结果。如果上游服务换了IP,域名解析已更新,但Nginx还在用旧IP连接,就会出现间歇性502。
解决方法是开启运行时DNS解析,把upstream块里的server指令配合resolver使用:
resolver 127.0.0.53 valid=30s ipv6=off; upstream backend { server api.internal.example.com:8080 resolve; }这里的resolve参数让Nginx在每次建立新连接时动态解析域名,valid=30s指定DNS缓存有效期。这个配置在Kubernetes环境里尤其关键,因为Service的ClusterIP会频繁变动,不配置动态解析的话,旧连接池里的IP全都会失效。
3.6 keepalive连接池与后端连接复用冲突
另一个让502"时而出现时而消失"的根因是连接复用。配置了upstream keepalive后,Nginx会维护与上游的空闲连接池,但上游服务如果主动关闭了这些空闲连接(比如Tomcat的keepAliveTimeout较短),Nginx不知道这条连接已经死了,继续往上面发请求,结果就是502。
表现为日志里频繁出现upstream prematurely closed connection while reading upstream,但上游服务日志里看不出异常。
处理思路有两个方向。一是让Nginx在发现连接失效时重试一次:
proxy_next_upstream error timeout http_502 http_503 http_504; proxy_next_upstream_tries 2;这能让Nginx在一个上游连接失败时自动换到下一个上游或重建连接重试,有效缓解偶发502对用户的影响。
二是严格匹配keepalive参数。以下是一个兼顾性能与稳定性的配置模板:
upstream backend { server 127.0.0.1:8080; keepalive 32; } location /api/ { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Connection ""; }使用keepalive时proxy_http_version必须设为1.1,并且要清空Connection头,否则上游会按HTTP/1.0处理,连接复用机制不生效,最终退化成每次新建连接的场景,反而更容易放大连接超时问题。
4. 上游健康检查与自动摘除:没事少折腾Nginx,让后端自己"报数"
4.1 没有健康检查时,Nginx会把请求发给"病人"
经过5个轮次的调优后,我强烈建议你顺手做一件事:给上游配置健康检查。Nginx开源版默认不带主动健康检查模块,这经常被误解为"开源版做不了健康检查",实际上通过max_fails和fail_timeout参数就能实现简单的被动健康检查:
upstream backend { server 127.0.0.1:8080 max_fails=3 fail_timeout=30s; server 127.0.0.1:8081 max_fails=3 fail_timeout=30s; }这个配置的逻辑是:如果某个上游在fail_timeout(30秒)内失败了max_fails(3次),Nginx会在接下来的30秒内自动把该上游标记为不可用,不再向其转发请求。30秒后重新尝试。这套机制虽然没有主动探测那么灵敏,但对于绝大多数业务场景够用了,而且完全免费。
如果你用的是Nginx Plus,或者愿意编译第三方模块nginx_upstream_check_module,那就可以实现主动健康检查:Nginx每隔几秒主动访问上游的health接口,根据返回码判断上游是否健康,比被动健康检查快得多。但对于团队里没有专职运维的情况,我自己通常会选择openresty自带的健康检查能力,或者干脆在Kubernetes层面用Pod的健康探针来实现,Nginx这边保持简单。
4.2 给每个服务建立一个标准的health接口
光有健康检查还不够,上游服务的/health接口质量决定了整个故障发现链路的效率。我见过太多团队把/health接口写成一个返回"hello world"的固定字符串——这个接口根本测不出服务的真实健康状态。
一个合格的健康检查接口至少应该做到三件事:检查自身依赖的数据库连接池是否还有空闲连接;检查外部依赖的缓存或消息队列是否可达;检查自身进程的线程池/协程池使用率是否接近上限。
以Spring Boot为例,一个健康检查接口可以这么实现:调用一次DataSource的getConnection()并释放,超时2秒内返回成功就算通过;连接失败则返回503。Nginx的健康检查模块收到非2xx状态码,就会自动把当前上游摘掉,后续请求自动切换到其他节点。这就形成了完整的故障自愈链路:后端自己"报数",Nginx负责"淘汰",用户无感知。
4.3 有了健康检查之后,502的故障面会被大幅压缩
配置了健康检查和自动摘除后,最简单的回报就是:偶发502会明显减少。拿我们一个实际的例子说明,某次MySQL主从切换后,从库连接数瞬间爆满,Java服务在获取数据库连接时全部超时,大量请求在接口层堆积。Nginx这边虽然没有感知到后端端口异常(端口还在监听),但健康检查接口因为数据库连接获取失败返回了503,健康检查模块迅速把该节点摘除,请求全部打到另一个健康节点上。整个过程用户最多感受到一次页面短暂卡顿,而不是铺天盖地的502报警。
这个案例说明了一个很重要的思路:502的防御不能只看Nginx和TCP层,要让健康检查探针把你业务链路的真实状态暴露出来。探针越接近真实业务链路,发现故障的速度越快,故障影响面越小。
5. 我踩过的一些真坑,以及针对性的防御性配置
5.1 关于超时时间:不是越长越好,而是按业务分级别设定
很多人遇到502就一刀切把proxy_read_timeout改成300秒甚至600秒,这种粗暴调参方式短期看起来"解决了"超时问题,实际上是把故障缓冲期拉长,用户体验更差,而且掩盖了上游真实存在的性能问题。
我现在的习惯是分location设定超时。正常API接口不用改,用默认值就行;但对于那些确实需要长时间处理的请求(比如导出报表、批量导入),单独开一个location并放长超时:
location /export/ { proxy_pass http://backend; proxy_read_timeout 300s; proxy_send_timeout 300s; proxy_request_buffering off; }proxy_request_buffering off这行很多人不熟悉。默认Nginx会先把客户端请求体完整缓冲到磁盘或内存,再转发给上游。对于大文件上传场景,如果关闭缓冲,Nginx边收边转,上游可以更早开始处理,避免上游因等待完整请求体而导致连接超时。但要注意,关闭缓冲后如果客户端上传中断,上游会收到一个不完整的请求,需要业务层做好判断。
5.2 惊群效应与backlog溢出:高并发502的隐藏推手
一次大促场景下的502排查让我铭记在心。上游服务CPU、内存、连接数全部正常,但Nginx大量报502,而且都是connect失败。后来用ss -lntp看8080端口的Recv-Q,发现队列长度持续大于socket的backlog设置。
当时上游Tomcat的acceptCount配置是100,Nginx瞬间涌入的连接远超这个数,多余连接直接被内核丢弃。解决方式是双管齐下:
Tomcat的acceptCount适当调大:
<Connector port="8080" protocol="HTTP/1.1" maxThreads="400" acceptCount="200"/>同时把内核的somaxconn调大:
sysctl -w net.core.somaxconn=1024另外还要注意Nginx自身传递连接的效率。如果Nginx和上游都在同一台机器,开启proxy_bind固定源IP和tcp_nodelay on可以稍微降低连接建立耗时,但收益相对有限,核心还是让后端吃得住流量。
5.3 日志切割别忘掉:磁盘满引起的"无脑502"
这个坑特别容易在凌晨触发。access.log和error.log在无人值守的情况下一直增长,磁盘被日志占满,Nginx尝试写入新日志时失败,行为变得异常,所有请求返回502。
哪怕从应用层面看一切正常,也要先确认磁盘使用率:
df -h一个习惯性动作:配置logrotate或每天定时任务,把旧日志压缩归档,保留最近7天即可。像logrotate做每日切割并压缩,一天的日志通常能压到原来的十分之一以下。把Nginx的/var/log/nginx目录单独挂一个分区,也能避免日志把系统盘撑爆。
5.4 故障演练:让502在你可控的时候出现一次
最后分享一个反常识的建议:主动制造一次502。在生产环境流量低峰的凌晨,手动停掉一台上游服务,观察Nginx的表现:健康检查是否及时摘除节点?摘除后请求是否平滑切换到存活节点?客户端有没有收到报错?有没有堆积报警?等恢复后对比日志,确认整个闭环链条是通的。
我见过太多的团队,502一出现就是全员扑上去救火,等故障恢复后谁也没复盘,更别说自己主动演练。生产环境的故障链路最怕的是"没遇到过",而不是"遇到了处理不好"。主动演练一次,你才知道你的监控、报警、摘除机制到底靠不靠谱,也才知道配置里的哪些参数只是摆设。
就我个人经验而言,把上面这套链路完整走一遍,你会对Nginx的502建立完全不同的认知——它不再是一个随机出现的恐怖错误码,而是链路上某个节点状态的可视化呈现。排查速度、定位精度、修复效率都会比"网上找配置改一改"高出不止一个量级。