后端工程师都遇到过这种场景:线上 API 平均 RT 50ms,但偶尔会冒出 2000ms+ 的尖刺。监控看着应用层日志风平浪静,数据库慢查询也没有,gc 也正常——这种"幽灵性"延迟最磨人。

本文给你一套从应用层到 ICMP 层的逐层拆解方法,附完整命令行实操和定位决策树。

问题域:为什么 RT 抖动这么难查

API 调用的总耗时实际是这样组成的(从客户端发起请求开始到收到响应):

客户端 ──┐
        ↓
    DNS 解析(可能 5ms,可能 200ms)
        ↓
    TCP 三次握手(RTT × 1)
        ↓
    TLS 握手(RTT × 2,首次)
        ↓
    HTTP 请求发送(取决于带宽)
        ↓
    服务端处理(应用层)
        ↓
    HTTP 响应返回(取决于带宽)
        ↓
    客户端接收完成

但绝大多数后端工程师的监控只覆盖最后一段——服务端处理:Nginx access_log、应用层 trace、数据库慢查询日志。

如果延迟出现在前面六段中任何一段,你的应用层监控会显示"业务逻辑只用了 30ms",但客户端实测就是 2000ms。真凶藏在网络层,但你的工具盯着应用层——这是 RT 抖动最常见的失配。

下面这套四层定位法解决的就是这个问题。

第一层:DNS 层(最常被忽略的元凶)

90% 的"接口偶发慢"问题,排查到最后是 DNS。

怎么排查

最直接的工具是 dig + 计时:

# 多测几次,看延迟稳定性
for i in {1..10}; do
  dig api.example.com +noall +stats | grep "Query time"
done

健康输出应该是这样的:

;; Query time: 18 msec
;; Query time: 16 msec
;; Query time: 17 msec
;; Query time: 19 msec

如果你看到这种:

;; Query time: 18 msec
;; Query time: 1247 msec   ← 来了
;; Query time: 17 msec
;; Query time: 22 msec
;; Query time: 893 msec    ← 又来了

恭喜,你找到问题了——DNS 服务器抽风,且这种抖动会反映为 RT 尖刺。

常见原因和修复

现象原因修复
延迟周期性飙高本地 DNS 缓存 TTL 过短,频繁发起递归查询调高 TTL(权威端控制),或用 nscd / systemd-resolved 做本地缓存
个别地区慢运营商 DNS 解析慢或污染业务方层面强制使用公共 DNS(223.5.5.5 / 1.1.1.1)或部署私有 DNS
偶发 super 慢(秒级)主 DNS 失败 timeout 后切备用/etc/resolv.conf 配置 options timeout:1 attempts:2,加快切换

如果你想验证某个域名在不同地区的解析是否一致,可以用多节点 DNS 查询工具,比如 https://www.biuping.com/dns/,直接看各地解析出的 IP 是否相同——经常会发现某些地区返回的是污染 IP,这就是部分用户反馈"接口慢"的根因之一。

第二层:TCP 层(连通性 + 握手延迟)

DNS 排查完后,TCP 层是第二高发位置。这一层有两个独立问题:端口能否连上 vs 握手要多久

测连通性:tcping

ping 走的是 ICMP 协议,不能验证 TCP 端口是否开放。很多云服务器默认禁 ICMP,但 80/443 是开的——这种环境 ping 不通但服务正常。反之,服务挂了但 ICMP 还能回,ping 仍然通。

正确姿势是用 tcping:

# Linux/macOS 常见方案 - 用 nc
nc -zv api.example.com 443
# 或者用专门的 tcping 工具(go 写的,跨平台)
tcping api.example.com 443

# Windows
Test-NetConnection api.example.com -Port 443

健康输出大致是:

Connection to api.example.com 443 port [tcp/https] succeeded!

或:

api.example.com port 443 open.
Connected to api.example.com:443 in 28.451 ms.

测握手延迟稳定性

连通性 OK 后,看握手延迟是否稳定。这一步用 curl 的内置计时器最准:

curl -o /dev/null -s -w "
DNS:       %{time_namelookup}s
TCP:       %{time_connect}s
TLS:       %{time_appconnect}s
TTFB:      %{time_starttransfer}s
Total:     %{time_total}s
" https://api.example.com/v1/health

输出会长这样:

DNS:       0.018s
TCP:       0.052s    ← TCP 握手耗时 = 0.052 - 0.018 = 0.034s,正常
TLS:       0.121s    ← TLS 握手耗时 = 0.121 - 0.052 = 0.069s,正常
TTFB:      0.187s    ← 服务端处理 = 0.187 - 0.121 = 0.066s,正常
Total:     0.198s

跑 50 次循环看分布:

for i in {1..50}; do
  curl -o /dev/null -s -w "%{time_connect}\n" https://api.example.com/v1/health
done | sort -n | tail -5

如果尾部出现 1.5s+ 的连接耗时,说明 TCP 握手出了问题——通常是中间链路 SYN/SYN-ACK 包被丢弃,后续重传等了 RTO(Linux 默认初始 RTO 1 秒)。

常见根因

症状根因排查
TCP 时间 > 200ms 且抖动大跨地域、跨运营商访问看链路是否符合预期(下一节 traceroute)
偶发 1s+ 卡顿中间路由器 SYN 包丢弃,触发 SYN 重传`dmesg \grep -i "TCP"` 看内核日志,或在客户端抓包
TLS 时间异常高证书链大、OCSP 查询慢、协商版本回退openssl s_client -connect host:443 -servername host 看证书链长度

第三层:路由层(中间跳定位)

前两层排查完,接下来是中间链路。mtr 是这一步的核心武器——比 traceroute 强在它持续发包统计每一跳的丢包和延迟,而不是只跑一次。

跑 mtr

# Linux
mtr -n -c 100 -r api.example.com

# Windows 用 WinMTR (https://winmtr.net)

参数解释:-n 不做反向 DNS(快很多),-c 100 跑 100 轮,-r 输出最终报告而不是实时刷新。

输出会是这样:

HOST: my-server                  Loss%   Snt   Last   Avg  Best  Wrst StDev
  1.|-- 192.168.1.1               0.0%   100    0.4   0.5   0.3   1.2   0.1
  2.|-- 10.32.0.1                 0.0%   100    3.2   3.4   2.9   8.1   0.6
  3.|-- 218.xx.xx.1               0.0%   100    8.7   9.1   8.2  15.4   1.0
  4.|-- 202.xx.xx.85              0.0%   100   12.3  12.8  11.9  18.6   0.8
  5.|-- 219.xx.xx.149             0.0%   100   45.2  46.1  44.8  52.1   1.2
  6.|-- 202.xx.xx.34             67.0%   100   71.5  78.2  68.9 145.3  18.4   ← 罪魁
  7.|-- 203.xx.xx.18             65.0%   100   72.8  79.1  69.2 152.7  19.1
  8.|-- 103.xx.xx.142            65.0%   100   73.2  80.0  70.1 158.4  20.2
  9.|-- xxx.xxx.xxx.xxx          66.0%   100   72.6  79.5  69.8 155.2  19.6

怎么读

几个不要被骗的反直觉规则:

规则 1: 单跳高丢包不是问题,丢包"传染"才是问题

第 6 跳显示 67% 丢包,但如果第 7、8、9 跳都恢复 0%,说明只是第 6 跳路由器对 ICMP 做了限速——不影响真实流量。只有从某一跳开始所有后续跳都丢包,那才是真正的链路问题。

上面的例子里,丢包从第 6 跳传染到了 9 跳,说明第 6 跳之前的链路都正常,问题在第 6 跳到第 7 跳之间。

规则 2: 单看延迟没用,看延迟跳变

第 5 跳到第 6 跳延迟从 46ms 跳到 78ms——这个 30ms 跳变才是关键信号,说明数据包在这一段经历了拥塞或绕路。

规则 3: 跨网段时延迟跳变正常

第 4 跳(国内某 ISP 骨干)到第 5 跳(出境节点),延迟从 12ms 涨到 45ms 是合理的——这是物理光速决定的(出境后光纤距离变长)。这种"合理跳变"和"拥塞跳变"要区分开。

单点 mtr 的局限

最容易被忽略的一点:你机器上跑的 mtr 只反映你这条 ISP 路径。同一时刻从其他地区/运营商的视角看,问题可能完全不存在。

排查跨地域服务时建议至少从 3-5 个不同 ASN 的节点跑 mtr 对比,我自己一般直接用在线多节点工具(类似 https://www.biuping.com/traceroute/)同时跑国内三大运营商 + 几个海外节点的 mtr,几分钟就能拿到一张完整的路径地图,比手动 SSH 进多台机器要高效得多。

第四层:服务端 / 中间件(应用层之外的最后一公里)

如果前三层都排查完确认网络层 OK,问题往往在服务端的网络栈、负载均衡、连接池这些"看不见的中间件"。

几个高频根因

1. TCP backlog 满了

服务端 listen 队列满后,新连接会被静默丢弃,客户端只能等 SYN 重传(就是上面第二层那个尾部 1s+ 的现象)。

# 看队列状态
ss -ltn | grep ':443'
# 输出最后两列分别是 当前队列 / 最大队列
# Recv-Q  Send-Q
#   0     128         ← 健康
#   128   128         ← 队列已满,新连接被丢

修复:net.core.somaxconn 调高(默认 128 太小),应用层 listen() 第二个参数也要调。

2. 连接池耗尽

应用到下游(数据库、Redis、第三方 API)的连接池满,新请求要等空闲连接,表现为"业务逻辑很快但接口很慢"。

排查方法是看你 ORM/HTTP 客户端的连接池监控。Java 的 HikariCP、Go 的 sql.DB、Node 的 axios + agent 都有相关指标。

3. 反向代理超时不一致

Nginx → 应用层超时配置如果不一致,会出现"应用还没响应,Nginx 已经返回 504"的诡异现象。

# 检查这三个值是否合理
proxy_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;

4. Keepalive 配置错位

客户端开了 keepalive 但服务端关闭超时太短,会导致部分请求踩中"连接刚被服务端关闭但客户端还在用"的窗口期,触发重连。

# 服务端检查
sysctl net.ipv4.tcp_keepalive_time   # 默认 7200s,部分场景太长

决策树:30 分钟根因定位流程

把上面四层串起来,得到这个流程:

RT 抖动告警触发
    ↓
[1] 应用层监控看,服务端处理慢吗?
    ├─ 是 → 走传统应用层排查(GC / DB / 锁)
    └─ 否 ↓
[2] dig 跑 10 次,DNS 延迟稳定吗?
    ├─ 不稳定 → DNS 问题(本地缓存 / 运营商 DNS / TTL)
    └─ 稳定 ↓
[3] curl 看 TCP/TLS 耗时分布,有尾部 1s+ 吗?
    ├─ 有 → SYN 丢包重传,继续看 mtr
    ├─ TLS 异常高 → 证书链 / OCSP 问题
    └─ 全部正常 ↓
[4] mtr 100 轮,从某跳开始连续丢包吗?
    ├─ 是 → 链路拥塞,联系 ISP 或换出口
    └─ 否 ↓
[5] 服务端 ss 看队列、看连接池、看反向代理日志
    └─ 大概率在这里

按这个流程走,一般 30 分钟内能定位 90% 的 RT 抖动根因。剩下 10% 是真的疑难杂症(比如某个交换机硬件 bug、某段海缆抖动),那种情况就只能拿数据找运营商扯皮了。

几个反常识的总结

写了这么多,几个核心观点压缩一下:

ping 通 ≠ 服务正常。ICMP 和 TCP 是两个独立的东西,监控只挂 ping 探针是非常脆弱的——服务挂了 ping 还能通的场景太多。生产环境的健康检查必须用 tcping 或 HTTP 探针

单点视角是排查最大的陷阱。你 SSH 进的那台机器看到的链路状态,只是你这条 ISP 路径——同一时刻其他地区的视角可能完全不同。重要排查至少要 3 个不同 ASN 的节点对比。

网络层延迟比应用层延迟更难发现。应用层延迟有日志、有 APM、有 trace,但 DNS 慢、TCP 重传、路由抖动都不会进你的应用日志。专门给网络层加监控(SmokePing、Prometheus blackbox_exporter)是值得投入的。

保留历史基线最重要。同一个目标在正常时候 mtr 长什么样、每跳延迟多少,平时多看几眼,出问题时一眼就看出"这条路径不对劲"。没基线的排查是猜,有基线的排查是看图说话。


如果你团队已经被 RT 抖动困扰很久,先从最简单的开始:在你监控里加 4 条曲线——DNS 解析时间、TCP 连接时间、TLS 握手时间、服务端处理时间,各自独立记录。光这一步,80% 的"幽灵抖动"都能露出马脚。

工具推荐:命令行用 mtr + dig + curl -w 这套组合最稳,我自己跑跨地域排查时会用 biuping.com 的多节点版本看全国视角,一次能拿到几十个节点的对比数据,比手动开 SSH 高效太多。但工具不是关键,思维方式是关键——把"接口慢"这个模糊问题拆解成 DNS / TCP / TLS / 应用层四个独立维度,问题就解了一半。

标签: none

添加新评论