一、故障现象
生产环境系统调用测试平台的接口(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 返回响应之前主动断开了连接"。
处理过程还原:
- OkHttp 请求到达 nginx(日志确认收到);
- nginx 开始转发给上游测试平台,一直等不到上游响应;
- 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)即恢复。
七、教训总结
- 误导性日志比没有日志更糟糕。 "接口调用成功"四个字让排查先走了弯路;catch 里 close 不判空产生的 NPE 又把真实异常彻底埋掉。写工具类时:资源释放判空、日志区分成败、异常如实抛出。
- "ping 通"≠ 网络通。 用 curl 的
time_connect / time_appconnect / time_starttransfer四个时间点,可以快速定位卡在 TCP、TLS 还是应用层。 - nginx 499 = 客户端先放弃了。 排查方向应该往上游查,而不是怀疑客户端。
- proxy_pass 写域名 ≠ 动态解析。 上游是 GTM/CDN 类调度域名时,必须 resolver + 变量形式 proxy_pass,否则对端换 IP 必死。
- 整条链路每个环节看起来都"正常"(TCP 通、TLS 通、DNS 通、对端服务正常),只有把它们分段拆开逐一对比,才能找到坏掉的那一段。