计算机 · 2026 年 11 月 11 日

从一次 curl 超时说起:DNS、TCP 与 TLS 到底谁慢了

一个 20 秒的 curl 超时,可能卡在 DNS、TCP 握手、TLS 协商,也可能是 IPv6 在悄悄拖后腿。这篇文章给你一套五分钟内定位链路瓶颈的方法。

线上排障最怕一句话:「网络慢」。慢在哪一层,决定了你该改 resolv.conf、调 MTU,还是换掉那条半死不活的代理。curl 自带的分阶段计时,其实已经能回答大部分问题。

先让 curl 自己招供#

不要只看 time_total。用下面这个格式串,把一次请求拆成五个阶段:

bash
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:先区分缓存与递归#

bash
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:看握手有没有完成#

bash
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:握手卡住还是证书报错#

bash
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 连续请求同一个域名时,可以显式复用连接:

bash
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。

把排查固化成脚本#

临时命令用完就忘,不如把常用指标写进一个脚本,出问题时直接跑:

bash
#!/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 是黑洞——包发得出去、回不来——每次新建连接都要先吃一次等待。验证方法很直接:

bash
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 '*' 做一次直连对照:

bash
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 信息量大,但读起来慢。先有时计和分层结论,再抓一次有针对性的包:

bash
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 的时间点放在一起,才能判断是链路丢包还是服务端处理慢。

五分钟排查清单#

  1. 跑一遍分阶段计时,把耗时归到 DNS、TCP、TLS、TTFB 中的某一段。
  2. DNS 慢:分别用 dig 和 getent hosts 对比,检查搜索域与 NSS 顺序。
  3. TCP 慢:用 curl -4 / -6 对比,mtr 看丢包跳数,确认安全组与监听地址。
  4. TLS 慢:openssl s_client -brief 看握手与证书链,排除代理和 OCSP。
  5. TTFB 慢:问题已经不在网络,去看服务端日志、数据库和上游依赖。
  6. 仍然偶发:记录时间点,抓 40 个包,别靠重启碰运气。

结论#

排网络问题,顺序比工具重要:DNS → TCP → TLS → 首字节 → 传输。每一层都有对应的计时字段和验证命令,先把耗时归到某一层,再决定抓包还是改配置。绝大多数所谓的“网络慢”,最后都能收敛成一句很具体的话,比如“AAAA 记录指向了一台不通的机器”或者“安全组只放行了 IPv4”。

下一次再遇到超时,先别重启服务,跑一遍上面的计时模板;如果再花五分钟做一次 IPv4/IPv6 与代理的对照,大多数“偶发”就会变成可解释的规律。

完

#网络#curl#DNS#排障
回到顶部 ↑