nginx 转发超时故障排查实录

一、故障现象

生产环境系统调用测试平台的接口(https://10.0.0.10:38088/order/...,其中 10.0.0.10 是内网 nginx 代理,负责转发到 https://api.example-test.com)全部失败,无一成功。

应用日志的表现非常诡异------每次调用都打三行:

java 复制代码
ERROR OkHttpUtils timeout
INFO  OkHttpUtils post 接口调用成功》》》https://10.0.0.10:38088/order/...
ERROR NullPointerException: Cannot invoke "okhttp3.Response.close()" because "response" is null

二、分层定位:用 curl 的四个时间点拆链路

运维反馈:部署 jar 的服务器 ping 10.0.0.10 是通的。

但 ping 只证明 ICMP 通,不代表 TCP 端口能建立连接,更不代表应用层有响应。用 curl 的计时参数做分层测试:

bash 复制代码
curl -vk --connect-timeout 5 --max-time 60 https://10.0.0.10:38088/order/ \
  -w '连接:%{time_connect}s TLS:%{time_appconnect}s 首字节:%{time_starttransfer}s 总:%{time_total}s'

结果:

text 复制代码
连接耗时:0.001521s  TLS握手完成:0.032000s  首字节:0.000000s  总耗时:60.000850s
curl: (28) Operation timed out after 60000 milliseconds with 0 bytes received
  • TCP 连接 1.5ms,TLS 握手 32ms ------ 到代理的链路飞快
  • 首字节永远为 0,60 秒零字节 ------ 代理收到请求后没有任何下文

正常服务就算路径不对也会立刻回 404/401,一个字节都不回,说明代理收到请求后卡死在某个环节。

同时在代理服务器本机直连目标服务验证:

bash 复制代码
curl -vk --max-time 15 https://api.example-test.com/
# 秒回 HTTP/1.1 404(根路径未配置 API,属正常响应)

代理 → 测试平台的出网、DNS、对端服务全部正常。问题锁定在代理软件自身的转发环节。

四、第二重陷阱:nginx 499

查看代理的 access log,清一色的 499:

java 复制代码
10.0.0.50 - - [24/Aug/2026:10:13:17 +0800] "POST /order/list HTTP/1.1" 499 0 "-" "okhttp/4.9.1"

499 是 nginx 特有状态码,含义是"客户端在 nginx 返回响应之前主动断开了连接"。

处理过程还原:

  1. OkHttp 请求到达 nginx(日志确认收到);
  2. nginx 开始转发给上游测试平台,一直等不到上游响应;
  3. OkHttp 等满 10 秒超时,主动断开 → nginx 记一条 499。

卡点确认在 nginx → 测试平台的转发环节。但本机 curl 测试平台又是秒回的------差别必然在 nginx 自己使用的解析/转发路径上。

五、水落石出:error log 一行破案

查看 nginx error.log:

java 复制代码
2026/08/24 08:22:09 [error] connect() failed (110: Connection timed out)
while connecting to upstream, ...,
upstream: "https://198.51.100.10:443/gateway/lark/open/oauth2/auth?..."

nginx 实际转发到了 198.51.100.10------但配置里写的明明是域名:

nginx 复制代码
location /order/ {
    proxy_pass https://api.example-test.com/;
    proxy_ssl_server_name on;
    proxy_set_header Host api.example-test.com;
    ...
}

坑在这里:proxy_pass 写静态域名时,nginx 只在启动/reload 时解析一次 DNS(走系统 resolver),之后一直使用当时缓存的 IP,直到下次 reload/restart。

而 api.example-test.com 的 DNS 解析是:

text 复制代码
api.example-test.com → CNAME → gtm-test.gtm-example.net → 198.51.100.20

测试平台走 GTM 全局流量调度,IP 会随时切换。nginx 启动时解析到的 198.51.100.10 在 GTM 切换后失效,nginx 还抱着旧 IP 不放,SYN 发出去石沉大海------connect timeout,永久挂起。

强制用旧 IP 验证,实锤:

bash 复制代码
# 旧 IP:超时
curl -vk --resolve api.example-test.com:443:198.51.100.10 https://api.example-test.com/ --max-time 10
# curl: (28) Connection timed out after 10001 milliseconds
新 IP(正常 DNS 解析):正常响应
curl -vk --resolve api.example-test.com:443:198.51.100.20 https://api.example-test.com/ --max-time 10
HTTP/1.1 404(秒回)

故障时间线也吻合:error log 最早 08:22,应用日志最早 08:17 开始超时。

六、修复

应急恢复(无需改配置):

bash 复制代码
nginx -s reload

reload 会让 nginx 重新解析域名,拿到当前有效 IP,业务立即恢复。

根治:改成变量形式的 proxy_pass,让 nginx 在请求时按 resolver 动态解析。

注意一个坑:原配置 proxy_pass https://api.example-test.com/; 末尾的 / 作用是把 /order/ 前缀剥掉(error log 中上游路径为 /gateway/... 可证实),而变量形式的 proxy_pass 不允许带 URI ,需要用 rewrite ... break 替代:

nginx 复制代码
location /order/ {
    rewrite ^/order/(.*)$ /$1 break;
    resolver 10.0.0.53 valid=60s ipv6=off;   # 内网 DNS
    set $order_upstream https://api.example-test.com;
    proxy_pass $order_upstream;
    proxy_ssl_server_name on;
    proxy_set_header Host api.example-test.com;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

改动点:

  • rewrite ^/order/(.*)$ /$1 break; 替代原 proxy_pass 末尾 / 的剥前缀作用;
  • proxy_pass 改为变量(不带任何路径),配合 resolver 实现运行时解析;
  • valid=60s 控制解析缓存,GTM 切换后最多 1 分钟自动生效;
  • proxy_ssl_server_name on 必须保留,否则回源握手不带 SNI,CDN 会拒绝。

生效并验证:

bash 复制代码
nginx -t && nginx -s reload
curl -vk --max-time 15 https://10.0.0.10:38088/order/list

能拿到任何 HTTP 响应(哪怕 404)即恢复。

七、教训总结

  1. 误导性日志比没有日志更糟糕。 "接口调用成功"四个字让排查先走了弯路;catch 里 close 不判空产生的 NPE 又把真实异常彻底埋掉。写工具类时:资源释放判空、日志区分成败、异常如实抛出。
  2. "ping 通"≠ 网络通。 用 curl 的 time_connect / time_appconnect / time_starttransfer 四个时间点,可以快速定位卡在 TCP、TLS 还是应用层。
  3. nginx 499 = 客户端先放弃了。 排查方向应该往上游查,而不是怀疑客户端。
  4. proxy_pass 写域名 ≠ 动态解析。 上游是 GTM/CDN 类调度域名时,必须 resolver + 变量形式 proxy_pass,否则对端换 IP 必死。
  5. 整条链路每个环节看起来都"正常"(TCP 通、TLS 通、DNS 通、对端服务正常),只有把它们分段拆开逐一对比,才能找到坏掉的那一段。
相关推荐
Lvan的前端笔记1 天前
docker:每个前端项目一个 Nginx 容器还是只有一个Nginx容器
前端·nginx·docker
姜鱼问生1 天前
Nginx 缓存命中率监控:从 X-Cache-Status 到实时统计
运维·nginx·缓存
Apipi*1 天前
30天速通Linux 第六章信号及信号处理
linux·运维·信号处理
运维行者_1 天前
网络性能监控怎么做?从自动发现到根因分析的4个环节
运维·服务器·网络·人工智能·支持向量机
此时不提桶,更待何时1 天前
06-14-A-Kafka集群运维与迁移实战详解
运维·kafka
IT研究所1 天前
AI-ITR平台如何减少客户问题反复升级?
大数据·运维·人工智能·低代码·自然语言处理·安全架构·企微
AIgorithmGEEK1 天前
[Linux]从手写报头到内核套路:序列化、反序列化与自定义协议全链路
linux·运维·服务器·网络·序列化·反序列化
Nil2081 天前
leetcode 139单词拆分
linux·运维·服务器
Starry-sky(jing)1 天前
BUG: unable to handle kernel paging request 完整排查:dmesg 四要素与三路定罪
linux·运维·服务器·内核·排障
mounter6251 天前
从硬件互连到操作系统变革:CXL 技术演进与 Linux 内核工程挑战
linux·运维·服务器