ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Nginx反向代理502 Bad Gateway排查实战:从原理到解决方案

Nginx反向代理502 Bad Gateway排查实战:从原理到解决方案 做运维和技术支持的这些年Nginx反向代理的502 Bad Gateway应该是我遇到频率最高、也最容易被误判的报错之一。不管你是刚搭好环境调试还是线上突然报警只要看到502心里基本就明白客户端到Nginx这段是通的但Nginx往后端转发的链路出了问题。这篇文章把我这么多年排查502的完整思路、常见诱因和实操命令整理出来适合正在踩坑的运维、后端开发也适合刚接触Nginx的同学照着一步步定位。1. 502 Bad Gateway是什么先搞清楚问题发生在哪一环1.1 从一次完整的请求链路说起要解决502首先得理解反向代理模式下一次请求是怎么走的。客户端发起请求先打到NginxNginx根据你配置的location匹配规则把请求转发给upstream定义的后端服务器可以是一台Tomcat、一个PHP-FPM进程、一个Node服务也可以是内网其他端口上的应用。后端处理完把响应原路返回给NginxNginx再回给客户端。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 TimeoutNginx等待上游响应的时长超过了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 weight1; server 192.168.1.12:8080 weight1; } server { listen 80; location /api/ { proxy_pass http://backend; } }这个配置本身没问题但如果你修改过防火墙、或者某台后端机器宕机后没有从upstream里摘掉Nginx默认会按fail_timeout周期内连续失败max_fails次后把该节点标记为不可用。问题在于很多人默认配置的max_fails1fail_timeout10s意味着某一台机器只要失败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连接复用导致的诡异502keepalive引发的问题属于那种偶发、难重现、让人怀疑人生的类别。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比如后端设置75sNginx设置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:8080connect() 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_buffersconnect() 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复用这些更深的层次去挖了。
返回列表