线上排障最怕一句话:「网络慢」。慢在哪一层,决定了你该改 resolv.conf、调 MTU,还是换掉那条半死不活的代理。curl 自带的分阶段计时,其实已经能回答大部分问题。
先让 curl 自己招供#
不要只看 time_total。用下面这个格式串,把一次请求拆成五个阶段:
curl -o /dev/null -sS \
-w 'dns=%{time_namelookup}s tcp=%{time_connect}s tls=%{time_appconnect}s ttfb=%{time_starttransfer}s total=%{time_total}s\n' \
https://example.com/health
各字段的含义和常见嫌疑:
| 字段 | 含义 | 数值异常时优先怀疑 |
|---|---|---|
time_namelookup |
DNS 解析完成 | 本地 DNS、上游解析、搜索域 |
time_connect |
TCP 三次握手完成 | 路由、防火墙、IPv6、SYN 重传 |
time_appconnect |
TLS 握手完成 | 证书链、OCSP、TLS 版本、代理 |
time_starttransfer |
收到首字节 | 服务端处理、数据库、上游依赖 |
time_total |
整个请求结束 | 响应体大小、带宽、连接复用 |
如果 dns 就花了 5 秒,后面的 TCP 和 TLS 再快也没意义。反过来,dns 只有 2ms、tcp 却要 3 秒,问题基本就在网络可达性上。
加上 -4 或 -6 分别强制走 IPv4 / IPv6,能快速验证「Happy Eyeballs」是不是在帮倒忙。很多“偶发慢”其实是 AAAA 记录存在、但 IPv6 实际不通。
分层验证,不要一上来就抓包#
1. DNS:先区分缓存与递归#
dig +stats example.com A
dig @223.5.5.5 example.com A
getent hosts example.com
dig默认直接问配置里的 DNS,绕过部分系统缓存;getent hosts走的是 glibc 的 NSS 链路,更接近程序真实行为;- 如果
dig很快、getent很慢,检查/etc/nsswitch.conf里是否有mdns、resolve之类的顺序问题。
2. TCP:看握手有没有完成#
curl -v --connect-timeout 5 https://example.com/ 2>&1 | sed -n '1,20p'
ss -tanp | grep -E 'SYN-SENT|ESTAB'
一直停在 Trying 1.2.3.4...,说明 SYN 没有回包。常见原因:安全组没放行、目标端口没监听、运营商丢包、MTU 黑洞。用 mtr -rwzbc 20 example.com 能看到丢包发生在哪一跳。
3. TLS:握手卡住还是证书报错#
openssl s_client -connect example.com:443 -servername example.com -brief </dev/null
如果 TCP 很快、TLS 很慢,重点看证书链是否过大、是否在拉取 OCSP、服务端是否强制了老旧套件。把 time_appconnect 与 time_connect 相减,就是 TLS 的真实开销。
一个容易被忽略的元凶:连接没有复用#
单次请求很快,并不代表批量请求也快。每次新建 TCP + TLS,在跨地域链路上可能要付出 3 到 5 个 RTT。用 curl 连续请求同一个域名时,可以显式复用连接:
curl -sS -o /dev/null \
-w '%{time_connect} %{time_appconnect} %{time_total}\n' \
https://example.com/a \
https://example.com/b \
https://example.com/c
后两次的 time_connect 和 time_appconnect 应该接近 0。如果仍然很大,检查客户端是否每次都在重建连接池,或者中间代理是否强制关闭了 keep-alive。
把排查固化成脚本#
临时命令用完就忘,不如把常用指标写进一个脚本,出问题时直接跑:
#!/usr/bin/env bash
set -euo pipefail
host="${1:?usage: probe.sh <url>}"
curl -o /dev/null -sS --max-time 20 \
-w "url=%{url_effective}\ndns=%{time_namelookup}\ntcp=%{time_connect}\ntls=%{time_appconnect}\nttfb=%{time_starttransfer}\ntotal=%{time_total}\nhttp=%{http_code}\n" \
"$host"
再配一条 date -Is,把时间点一起记下来。很多“偶发”问题,只有把时间、目标、各阶段耗时放在一起看,才会露出规律。
两个高频变体#
AAAA 记录存在,但 IPv6 不通#
很多「时快时慢」来自 Happy Eyeballs:客户端先尝试 IPv6,等待一小段时间没有结果,再回退到 IPv4。如果 IPv6 是黑洞——包发得出去、回不来——每次新建连接都要先吃一次等待。验证方法很直接:
curl -4 -o /dev/null -sS -w 'v4=%{time_connect}\n' https://example.com/
curl -6 -o /dev/null -sS -w 'v6=%{time_connect}\n' https://example.com/
IPv4 很快、IPv6 卡住,就基本确定了。处理方式不是删掉 AAAA 记录(那会误伤真正有 IPv6 的用户),而是修路由、防火墙或上游通告;临时绕行可以在客户端显式 --ipv4,但要把它当成止血,不是长期方案。
代理只处理了部分流量#
http_proxy、https_proxy、all_proxy 可能来自 shell、systemd、容器编排或桌面系统,各管一段;curl 会读环境变量,而你的应用未必读。用 --noproxy '*' 做一次直连对照:
curl --noproxy '*' -o /dev/null -sS -w 'direct=%{time_total}\n' https://example.com/
curl -o /dev/null -sS -w 'proxy=%{time_total}\n' https://example.com/
直连快、代理慢,问题在代理链路或它的出口;反过来,检查 NO_PROXY 是否把内网域名也送进了代理。容器场景还要确认 sidecar 的拦截规则,别只查宿主机的环境变量。
抓包放在最后,不是第一步#
tcpdump 信息量大,但读起来慢。先有时计和分层结论,再抓一次有针对性的包:
sudo tcpdump -ni any -vv 'host example.com and (port 53 or port 443)' -c 40 -w /tmp/curl.pcap
只看三件事:SYN 有没有重传、TLS ClientHello 之后有没有 ServerHello、连接有没有被 RST。抓包回答的是「到底有没有回包」,不负责解释业务逻辑。把 pcap 和 curl -w 的时间点放在一起,才能判断是链路丢包还是服务端处理慢。
五分钟排查清单#
- 跑一遍分阶段计时,把耗时归到 DNS、TCP、TLS、TTFB 中的某一段。
- DNS 慢:分别用
dig和getent hosts对比,检查搜索域与 NSS 顺序。 - TCP 慢:用
curl -4/-6对比,mtr看丢包跳数,确认安全组与监听地址。 - TLS 慢:
openssl s_client -brief看握手与证书链,排除代理和 OCSP。 - TTFB 慢:问题已经不在网络,去看服务端日志、数据库和上游依赖。
- 仍然偶发:记录时间点,抓 40 个包,别靠重启碰运气。
结论#
排网络问题,顺序比工具重要:DNS → TCP → TLS → 首字节 → 传输。每一层都有对应的计时字段和验证命令,先把耗时归到某一层,再决定抓包还是改配置。绝大多数所谓的“网络慢”,最后都能收敛成一句很具体的话,比如“AAAA 记录指向了一台不通的机器”或者“安全组只放行了 IPv4”。
下一次再遇到超时,先别重启服务,跑一遍上面的计时模板;如果再花五分钟做一次 IPv4/IPv6 与代理的对照,大多数“偶发”就会变成可解释的规律。
完