curl 会分段告诉你
一句话总结
使用 curl -w 的计时变量,只需一次请求就能区分瓶颈位于名称解析、建立连接、TLS,还是服务器处理。这一行命令可以终结“到底是网络问题还是应用程序问题”的争论。
为什么需要这些计时数据
部署完成后,响应突然变慢。基础设施团队认为是应用程序的问题,开发团队则认为是网络问题。双方各自的仪表盘看起来都一切正常。
此时需要的不是更多仪表盘,而是把一次请求的时间拆分成不同阶段。
curl -sS -o /dev/null -w 'dns:%{time_namelookup} conn:%{time_connect} tls:%{time_appconnect} ttfb:%{time_starttransfer} total:%{time_total}\n' https://api.example.com/health
每个值都是从请求开始计算的累计时间,所以通过相减即可得到各阶段的耗时。解释规则非常明确。
| 哪个值较大 | 结论 |
|---|---|
time_namelookup |
名称解析问题,检查 DNS 阶段 |
time_connect - time_namelookup |
TCP 连接问题,检查路径或防火墙 |
time_appconnect - time_connect |
TLS 握手问题,检查证书链与协议协商 |
time_starttransfer - time_appconnect |
服务器生成首字节所用的时间。 检查应用程序 |
time_total - time_starttransfer |
响应体传输问题,检查大小或带宽 |
如果到 time_connect 为止都很正常,只有 time_starttransfer 很大,网络就是无辜的,争论可以当场结束。
它是如何工作的
只提取状态码。 curl -s -o /dev/null -w '%{http_code}' URL 会丢弃响应体,只输出状态码,是健康检查脚本的基本形式。连接本身失败时会得到 000,必须单独处理这个值,才能区分“服务器返回了 5xx”和“根本无法到达服务器”。
只接收响应头。 curl -sI URL 会发送 HEAD 请求,只接收响应头。如果 Content-Length 与预期不同,可能存在缓存或代理。
重定向。 curl 默认不会跟随重定向,只有添加 -L 才会继续跳转。因此,同一个 URL 在没有 -L 时返回 301,加入后返回 200,属于正常现象。健康检查不了解这一点,就会把时间浪费在“为什么返回 301”上。
超时不是可选项。 如果健康检查没有设置 --max-time,对端无响应时脚本就会永远挂起。cron 每五分钟启动一次这类脚本,进程会不断堆积。健康检查必须设置最大等待时间。
实际工作中会遇到的情况
在本地复现。 即使网络受限,只需运行 python3 -m http.server,也能复现大多数 HTTP 行为,包括状态码、响应头、重定向与未实现方法的响应。这既是实验采用这种方式的原因,也是在实际工作中验证客户端代码的常用方法。
用 jq 截取 JSON 响应。 像 curl -s URL | jq -r '.items | length' 这样通过管道连接,就能在脚本中直接提取值。不过,curl 失败时,jq 可能接收到空输入并悄然结束,因此脚本中需要设置 set -o pipefail。
健康检查脚本的契约。 好的健康检查会明确区分三种情况:正常(2xx)、收到响应但状态异常(4xx/5xx)、完全无法到达(连接失败)。如果把三者合并为同一种失败,即使收到告警,也不知道应该从哪里开始排查。
用数字指出缓慢的阶段
“API 很慢”并没有告诉我们应该检查哪里。curl 的时间
字段可以把一个请求拆成五个阶段。哪个阶段膨胀,就说明问题属于哪个团队。
curl -sS -o /dev/null -w \
'dns=%{time_namelookup} tcp=%{time_connect} tls=%{time_appconnect} \
ttfb=%{time_starttransfer} total=%{time_total}\n' \
https://example.com/api/health
这些数字是累计值。 如果 time_connect 为 0.32,它表示从请求开始到连接完成共花费 0.32 秒,
并不表示建立连接本身花了 0.32 秒。每个阶段的实际耗时必须通过
相减得到。
| 阶段 | 计算 | 数值膨胀时应怀疑什么 |
|---|---|---|
| 名称查询 | namelookup |
resolv.conf 的 search 域、DNS 服务器响应 |
| TCP 连接 | connect - namelookup |
网络往返时间、防火墙、accept 队列 |
| TLS 握手 | appconnect - connect |
证书链长度、OCSP 查询、会话复用失败 |
| 服务器处理 | starttransfer - appconnect |
应用程序与数据库 |
| 响应体传输 | total - starttransfer |
响应大小、带宽、是否压缩 |
如果只有 TTFB 很大,从那里开始才是应用程序的责任。 如果前三个阶段都很短,
只有 starttransfer 很大,网络就是清白的。反过来,如果 connect 很大,
而 starttransfer 很小,无论怎样检查代码都找不到答案。
不能只测一次就下结论。 由于 TLS 会话复用、DNS 缓存与连接池的存在, 第一次请求和第二次请求的性质可能完全不同。至少运行十次,同时观察中位数和 尾部值。
for i in $(seq 20); do
curl -sS -o /dev/null -w '%{time_starttransfer}\n' https://example.com/api/health
done | sort -n | awk '{a[NR]=$1} END {print "중앙값", a[int(NR/2)], "p95", a[int(NR*0.95)]}'
关注尾部,而不是平均值。 平均响应时间很好,用户却仍然认为系统很慢, 这种情况十分常见,因为用户记住的是自己经历过的最慢请求。
下一个实验要做什么
在本地启动 HTTP 服务器,分别提取状态码、响应头、计时数据与 JSON。亲自确认跟随重定向与不跟随时的差异,以及未实现方法的响应;最后编写能够区分并报告三种情况的健康检查脚本。