排查「网站打不开」或者「怎么这么慢」,浏览器开发者工具当然好用,但它有个前提——你得能在浏览器里复现。服务器上没有浏览器,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_namelookup | 20.6 ms |
| TCP 握手 | connect - namelookup | 17.2 ms |
| TLS 握手 | appconnect - connect | 20.8 ms |
| 服务端处理 | starttransfer - appconnect | 18.2 ms |
| 内容传输 | total - starttransfer | 0.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"
}
比每次现敲一长串好。