curl 请求慢,先拆开 DNS、TCP、TLS 和首字节时间

196 次浏览12 条回复

适用于 curl 7.61+。先确认版本,并从与应用相同的网络出口访问一个允许探测的地址;示例 URL 要替换,认证信息不要直接写进命令或贴到输出里。

curl --version
curl -sS -o /dev/null \
  --connect-timeout 5 --max-time 30 \
  -w 'remote_ip=%{remote_ip}\nhttp_code=%{http_code}\ndns=%{time_namelookup}\ntcp=%{time_connect}\ntls=%{time_appconnect}\npretransfer=%{time_pretransfer}\nttfb=%{time_starttransfer}\ntotal=%{time_total}\n' \
  https://example.com/health

这些时间都是从请求开始累计的,不是每段独立耗时。TCP 建连大约是 time_connect - time_namelookup,TLS 握手大约是 time_appconnect - time_connect,请求发出到首字节大约看 time_starttransfer - time_pretransfer。HTTP 地址没有 TLS 时,time_appconnect 通常为 0。

DNS 明显慢,可以在确认目标 IP 后用 --resolve 'example.com:443:IP' 做一次对照;IPv4、IPv6 路径有差异时分别用 -4-6 对照。不要用 -k 把证书问题掩盖掉。若加了 -L 跟随跳转,还应输出 %{num_redirects}%{time_redirect},否则中间跳转会让首字节时间不好解释。

至少连续采几次,并同时记录 UTC 时间、目标主机、HTTP 状态码和最终 IP。修复后用同一出口、同一 URL 和相同超时参数复测,确认慢的那一段下降,而不是只看单次 total

再补一个容易把分段看错的情况:如果环境里配置了 HTTP(S)_PROXYALL_PROXYtime_connect 量到的是到代理的 TCP;HTTPS 下,time_appconnect - time_connect 还可能包含 CONNECT 隧道协商,不再是纯 TLS 握手。排查前最好先确认 curl 实际是否走代理,也检查 NO_PROXY,但别把带认证信息的代理变量贴出来。只有网络策略允许直连时,才用 --noproxy '*' 做一次对照;否则应该在同一代理路径上复测。

还要区分冷连接和复用连接。每次单独启动一次 curl,基本量到的是重新做 DNS、TCP、TLS 的路径;实际应用如果用了 HTTP/1.1 keep-alive 或 HTTP/2,后续请求可能直接复用连接。对照时可以在同一个 curl 进程里连续请求同一主机,并输出 %{num_connects}%{http_version}(curl 7.61 已支持)。后续传输里 num_connects=0,同时 time_connecttime_appconnect 接近 0,通常表示复用了已有连接,不是计时失效。这样能避免拿冷连接的 total 直接对比连接池内请求。

time_starttransfer - time_pretransfer 也别直接当成服务端执行时间。这里还混着请求体上传、网络往返和服务端排队,POST/PUT 的 body 较大时尤其明显。做对照最好保持方法、请求体和认证路径一致,别拿轻量的 GET/HEAD 代替真实请求;如果服务端可控,再用访问日志、链路追踪或 Server-Timing 把应用内耗时拆出来,curl 这边只负责确认端到端哪一段变慢。

再看一下退出语义:上面的 -sS 不含 -f,curl 7.61+ 收到 HTTP 4xx/5xx 时通常仍返回进程码 0,所以 http_code 要和 shell 退出码一起记录;http_code=000 则表示没有拿到 HTTP 响应码,常见于解析、连接、TLS 或超时失败,需要结合 stderr 判断。若自动探测要把 4xx/5xx 判为失败,7.61+ 可用 --fail,curl 7.76+ 才有 --fail-with-body。改完可以用受控测试地址分别制造一次 404 和一次连接失败:前者应保留真实 HTTP 状态,后者应是非零退出码,别把两类情况都归成请求慢。

再补个口径:time_namelookup 更准确地说是 curl 的名称解析阶段,不一定只是一次 DNS 往返。/etc/hosts、NSS、本地缓存,以及 curl 是否带 AsynchDNS 都会改变路径,所以 dig 很快也不能直接证明 curl 解析正常。先看 curl -V;Linux/glibc 环境可对照 getent ahosts example.com,systemd 239+ 且启用 systemd-resolved 时可用 resolvectl query example.com。这些命令要从与应用相同的网络 namespace 和用户上下文执行,连续记录解析结果并和 curl 的 remote_ip 对照;地址族或目标 IP 不同,耗时就别直接横比。

还要注意两个超时不是按分段分别给额度。--connect-timeout 5 限制的是整个建连阶段,通常包括名称解析、到目标或代理的 TCP,以及 TLS/QUIC 等握手,不是“TCP 最多 5 秒”;--max-time 30 则覆盖整个操作。排查时若触发 curl 退出码 28,应同时保留 stderr 和这些计时值,单凭 28 不能判断卡在解析、握手还是传输。自动探测里也要给传输阶段留出预算,否则 --max-time 太紧,只会把不同原因统一表现成超时。

如果分段时间只能看出 TLS 或 TTFB 那一段偏大,可以给一次无敏感信息的受控请求加 --trace-time --trace-ascii /tmp/curl.trace,这两个选项在文中的 curl 7.61+ 可用。trace 会记下协议细节,请求头、Cookie 等也可能进入文件,所以不要直接对带凭据的真实请求开启,更不要原样贴出文件。重点对照连接完成、TLS、请求发出和首个响应头的时间顺序,再与 -w 的累计值核对;复测时保持 URL、出口、地址族和代理路径一致。这样能继续判断慢点落在哪个协议事件附近,但 trace 仍不是服务端内部耗时。

还有一段容易漏掉:响应体下载。time_starttransfer 只到首字节,首字节很快并不代表整个请求快。当前命令用了 -o /dev/null,响应体仍会被接收;可以再输出 size_download=%{size_download}speed_download=%{speed_download},并把 time_total - time_starttransfer 作为首字节后的端到端耗时参考。

小响应里固定开销占比高,speed_download 容易波动,最好对允许探测、大小固定且足够大的对象连续测;同时确认没有 --limit-rate,并保持压缩协商和 HTTP 版本一致。若首字节正常但后段慢,再查链路丢包或拥塞、出口限速,以及服务端发送是否中途停顿。

如果要把这组计时送进采集脚本,curl 7.70+ 可以直接用 -w '%{json}\n',比按等号和换行拆字段稳一些。先用 curl --version 确认版本;装有 jq 时,可对一个允许探测的地址执行 curl -sS -o /dev/null --connect-timeout 5 --max-time 30 -w '%{json}\n' URL | jq -e .,先验证输出始终是合法 JSON。\n\n不过管道最后的退出状态默认来自 jq,不能替代 curl 的退出码,自动采集仍要单独保存 curl 的 $? 和 stderr。%{json} 还会带最终 URL、IP 等字段,URL 含查询参数或内部地址时应先筛字段、脱敏,再进日志或指标系统;curl 7.61 到 7.69 则继续显式列出需要的变量。

BrianLv1#5

再补个口径:time_namelookup 更准确地说是 curl 的名称解析阶段,不一定只是一次 DNS 往返。/etc/hosts、NSS、本地缓存,以及 curl 是否带 AsynchDNS 都会改变路径,所以 dig 很快也不能直接证明 curl 解析正常。先看 curl -V;Linux/glibc 环境可对照 getent ahosts example.com,systemd 239+ 且启用 systemd-resolved 时可用 resolvectl query example.com。这些命令要从与应用相同的网络 namespace 和用户上下文执行,连续记录解析结果并和 curl 的 remote_ip 对照;地址族或目标 IP 不同,耗时就别直接横比。

双栈目标还要留意 Happy Eyeballs。curl 7.61 的默认请求可能在 IPv6 连接不够快时再尝试 IPv4,time_connect - time_namelookup 因而会混入地址族竞速和回退等待,不一定全是某条链路的 TCP 时间。连续采样时把 %{remote_ip} 一起留着;某些样本固定多出一小段时间,就对照实际命中的 IP,再用 -4-6 分别复测。

--happy-eyeballs-timeout-ms 在 curl 7.59+ 可用,但更适合受控诊断,别先靠调小它掩盖 IPv6 路径异常。验证时保持出口、代理、URL 和连接冷暖状态一致,确认默认路径的波动与命中地址对应。