LabHub
学习 学习路径 课程

Linux 网络诊断

curl 会分段告诉你

在 LabHub 中继续学习

一句话总结

使用 curl -w 的计时变量,只需一次请求就能区分瓶颈位于名称解析、建立连接、TLS,还是服务器处理。这一行命令可以终结“到底是网络问题还是应用程序问题”的争论。

流程图: 名称解析、建立连接、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 很大,网络就是无辜的,争论可以当场结束。

把一次耗时 945 毫秒的请求拆成五个阶段的条形图。名称查询 4 毫秒、TCP 连接 28 毫秒、TLS 39 毫秒、服务器生成首字节 859 毫秒、响应体传输 15 毫秒。前三个阶段合计只有 71 毫秒,占总时长的 7%,而服务器处理占 91%

它是如何工作的

只提取状态码。 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。亲自确认跟随重定向与不跟随时的差异,以及未实现方法的响应;最后编写能够区分并报告三种情况的健康检查脚本。