用 curl 排查线上问题:几个比 -I 更有用的参数

排查「网站打不开」或者「怎么这么慢」,浏览器开发者工具当然好用,但它有个前提——你得能在浏览器里复现。服务器上没有浏览器,CI 里也没有。

curl 能覆盖大部分场景,而且比开发者工具更精确。

-w:把耗时拆开看

这是 curl 最被低估的参数。它按模板输出请求的各阶段耗时:

curl -s -o /dev/null \
  -w 'code=%{http_code} dns=%{time_namelookup}s connect=%{time_connect}s tls=%{time_appconnect}s ttfb=%{time_starttransfer}s total=%{time_total}s size=%{size_download}B\n' \
  https://example.com/

输出:

code=200 dns=0.020640s connect=0.037888s tls=0.058692s ttfb=0.076903s total=0.077414s size=4903B

-s 关掉进度条,-o /dev/null 丢弃正文,只留统计。

这些时间是累计值,不是各段独立耗时,相减才得到每段:

阶段算法上例
DNS 解析time_namelookup20.6 ms
TCP 握手connect - namelookup17.2 ms
TLS 握手appconnect - connect20.8 ms
服务端处理starttransfer - appconnect18.2 ms
内容传输total - starttransfer0.5 ms

拆开之后结论就很直接了:

  • DNS 特别慢 → 解析器问题,换个 DNS 试试
  • connect 慢 → 网络链路或距离问题,跟应用无关
  • TLS 慢 → 证书链太长、OCSP 装订没配、或者协商到了低效的加密套件
  • ttfb 慢而 total 接近 ttfb → 服务端处理慢,去查应用和数据库
  • total 远大于 ttfb → 传输慢,看内容体积和压缩有没有生效

只看一个总时间,这些都区分不出来。

—resolve:绕过 DNS 直连指定 IP

部署新服务器、DNS 还没切换,想先验证新机器工作正常:

curl -s -o /dev/null -w '%{http_code} remote=%{remote_ip}:%{remote_port}\n' \
  --resolve example.com:443:203.0.113.10 \
  https://example.com/
200 remote=203.0.113.10:443

它让 curl 把 example.com:443 强制解析到指定 IP,但 SNI 和 Host 头仍然发送 真实域名——所以服务端的虚拟主机匹配和证书校验都按正常路径走。

这比改 hosts 文件干净得多:不影响系统其他程序,用完即弃,也不会忘了改回来。

%{remote_ip} 顺便确认了到底连到哪台机器,排查 CDN 或负载均衡时很有用。

-I 与 -L:重定向链要分开看

-I 只发 HEAD 请求看响应头,默认不跟随重定向

$ curl -sI -o /dev/null -w '%{http_code}\n' http://example.com/
301

-L 跟随,配合几个变量能看清整条链路:

$ curl -sIL -o /dev/null \
    -w '%{http_code} 经过 %{num_redirects} 次跳转 最终 %{url_effective}\n' \
    http://example.com/
200 经过 1 次跳转 最终 https://www.example.com/

排查跳转配置时,这两条要对照着看:只用 -L 你只知道最终能到,不知道中间 绕了几圈;只用 -I 你不知道它最终去了哪。

跳转次数异常(比如 3 次以上)通常意味着配置里有重复规则,每多一跳就多一个 完整的往返延迟。

看响应头验证配置

改完缓存策略或安全头,别只在浏览器里刷新——浏览器有自己的缓存层,容易骗人。

$ curl -sI https://example.com/ | grep -iE 'cache-control|content-encoding|strict-transport'
cache-control: no-cache, must-revalidate
strict-transport-security: max-age=31536000

grep -i 是必要的,HTTP 头大小写不敏感,不同服务器输出风格不一样。

验证压缩要主动声明支持,否则服务器不会压:

curl -sI -H 'Accept-Encoding: gzip, br' https://example.com/ | grep -i content-encoding

-v 看握手细节

TLS 出问题时,-v 的输出比错误信息有用得多:

curl -sv https://example.com/ 2>&1 | grep -E '^\*' | head -20

^\* 开头的是 curl 的诊断信息(> 是请求头,< 是响应头)。里面能看到证书 主体、签发者、有效期、协商出的 TLS 版本和 ALPN 结果。

证书还没签发好的时候,你会看到:

* LibreSSL/3.3.6: error:1404B438:SSL routines:ST_CONNECT:tlsv1 alert internal error

服务端在握手阶段就拒绝了——它手上没有这个域名的证书。这和「证书过期」「证书 域名不匹配」是完全不同的错误,别混为一谈。

—max-time 一定要加

curl --max-time 10 https://example.com/

不加的话,遇到黑洞路由(包被丢弃且不返回 ICMP)curl 会一直等下去。写进脚本 或者 CI 里,一个卡住的 curl 能把整条流水线拖死。

顺带说个诊断线索:超时和拒绝是不同的信号

  • Connection refused —— 包到了,但没有进程监听那个端口
  • Timeout was reached —— 包发出去了,没有任何回应

前者说明网络通、服务没起;后者说明中间有东西在丢包,防火墙、安全组或者路由 问题居多。看到超时就去查应用日志,方向多半是错的。

常用组合

存成 shell 函数,排查时随手就能用:

# 耗时拆解
timing() {
  curl -s -o /dev/null -w \
    'dns=%{time_namelookup} connect=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total}\n' \
    --max-time 15 "$1"
}

# 跳转链
chain() {
  curl -sIL -o /dev/null -w '%{http_code} <- %{num_redirects} 跳 -> %{url_effective}\n' \
    --max-time 15 "$1"
}

比每次现敲一长串好。