故障排查阅读约 9 分钟

一次一个阶段地排查代理超时

用 curl 计时把代理 DNS、TCP、CONNECT、TLS 和响应延迟分开,然后设置请求截止时间,并判断重试是否安全。

本页内容

起点

超时只告诉你某个计时器到期了。在修改截止时间、网关或重试策略之前,先找到最后一个完成的阶段。

画出实际超时的路径

对于经 HTTP 代理访问的 HTTPS 目标站点,通常的路径是:解析代理、通过 TCP 连接到代理、成功获得 CONNECT 隧道、完成目标站点 TLS、发送请求,然后接收响应头和响应体。HTTPS 代理会在 CONNECT 之前增加它自己的 TLS 握手。这两条 TLS 连接有各自独立的信任设置。

在这种设置下,通常由代理来解析目标站点的主机名。因此本地 DNS 测得很快,并不能说明代理侧的目标站点解析情况。SOCKS5 可以在本地或远程解析,取决于客户端配置;在解读 DNS 错误之前先弄清这一选择。

从一个获准的小型 GET 端点和一个网关开始。保持认证和路由显式化。浏览器导航或应用任务可能还要等待连接池、重定向、脚本或本地处理;它的超时并不自动等于代理超时。

在不开启跟踪的情况下捕获单次尝试

这个 shell 示例需要 curl 8.3 或更高版本,因为由 curl 自己导入认证变量。检查 curl --version,包括其 TLS 后端和特性。把 PROXY_URL 设置为服务商提供的、不含凭据的 HTTP(S) 网关;https://proxy.example.invalid:8443 是一个故意不可用的示例。私下注入 PROXY_USERNAME 和 PROXY_PASSWORD。本示例使用 Basic 代理认证;只按服务商文档中记录的方式调整认证。

保存为 proxy-timing.sh 并运行 sh proxy-timing.sh。CHECK_URL 默认为 https://example.com/;你可以换成一个你有权测试的小型 HTTPS 端点。使用不含内嵌凭据或敏感参数的 URL。该脚本会丢弃响应体、打印选定的数值结果并保留 curl 的退出状态。它不会跟随重定向,也不会重试。不要在开启 shell 跟踪的情况下运行它。--disable 保持在第一位以忽略 curlrc 设置,而值为空的 --noproxy 可防止继承的 NO_PROXY 规则绕过选定的网关。

proxy-timing.sh · curl 8.3+ · 一次 HTTPS GET

sh
: "${PROXY_URL:?Set a credential-free HTTP(S) proxy URL}"
CHECK_URL=${CHECK_URL:-https://example.com/}
case "$PROXY_URL" in
  http://*|https://*) ;;
  *) printf '%s\n' 'PROXY_URL must use http:// or https://' >&2; exit 2 ;;
esac
case "$CHECK_URL" in
  https://*) ;;
  *) printf '%s\n' 'CHECK_URL must use https://' >&2; exit 2 ;;
esac
case "$PROXY_URL $CHECK_URL" in
  *'@'*|*'?'*|*'#'*)
    printf '%s\n' 'Use URLs without credentials, queries or fragments' >&2
    exit 2 ;;
esac

curl --disable --silent --fail --http1.1 \
  --noproxy '' --proxy "$PROXY_URL" --proxy-basic \
  --variable %PROXY_USERNAME --variable %PROXY_PASSWORD \
  --expand-proxy-user '{{PROXY_USERNAME}}:{{PROXY_PASSWORD}}' \
  --connect-timeout 5 --max-time 15 --retry 0 \
  --output /dev/null \
  --write-out 'exit=%{exitcode} http=%{http_code} tunnel=%{http_connect} dns=%{time_namelookup} tcp=%{time_connect} tls=%{time_appconnect} ready=%{time_pretransfer} first=%{time_starttransfer} total=%{time_total} bytes=%{size_download}\n' \
  "$CHECK_URL"

把它们当作里程碑,而不是独立的计时器

这些时间字段是从传输开始算起的秒数。它们不是可以相加的独立时长。本示例启动一个全新的 curl 进程并要求使用 HTTP/1.1,以便更容易检查单个请求。复用的连接、多路复用、代理链和被跟随的重定向需要额外的解读。某个字段为零可能反映的是取整、未到达的阶段或不适用的阶段;它并不能证明某个操作是瞬时完成的。

只有当两个里程碑在这个简单请求中都已完成时,才把差值当作线索。例如,对于 HTTP 代理,tls 减去 tcp 包含了 CONNECT 交换和目标站点握手;它不是单独针对目标站点 TLS 的测量。first 减去 ready 包含了发送请求以及在网络和服务器上的等待。它无法单独分离出目标站点的 CPU 时间。

  • dns = time_namelookup:客户端侧查询完成。在这种代理配置下,相关的主机名是网关,而不是目标站点在代理侧的查询。
  • tcp = time_connect:到代理的连接已完成。这并不能证明隧道或目标站点连接可用。
  • tls = time_appconnect:TLS 建立完成。ready = time_pretransfer:协议准备已到达可以开始传输的时刻。HTTPS 代理会引入另一次握手;这些字段无法为每一跳分别提供计时。
  • first = time_starttransfer:第一个响应字节,包含之前的准备过程。要结合 tunnel 和 http 一起解读;收到代理的响应并不是目标站点成功的证据。
  • total = time_total 和 bytes = size_download:传输耗时和下载的响应体字节数。HTTP 200 加上 exit 28 仍然可能是一次未完成的下载。

沿着最后一个完成的阶段追查

对于单请求示例,按下面的顺序进行。在第一个未解决的阶段停下。建议的检查能缩小排查范围;仅凭计时无法确定是哪台机器丢了包,也无法解释服务商内部的排队。

  • 没有代理地址:exit 5 表示 curl 无法解析代理。检查网关拼写,以及实际容器或服务内可用的解析器。exit 6 涉及 curl 尝试在本地解析的主机名;在归咎于代理侧目标站点 DNS 之前,先重新审视路由和 DNS 模式。
  • 没有 TCP 连接:exit 7 表示连接失败;在 tcp 完成之前出现 exit 28 可能表示连接预算已耗尽。在同一运行时中检查配置的端口、出站路由和防火墙。立即被拒绝与静默的网络丢弃需要不同的检查。
  • TCP 已完成,tunnel 仍为 000:没有记录到可用的 CONNECT 响应。对于 HTTPS 代理,未完成的阶段可能是它自身的 TLS 建立。对于 HTTP 代理,排查 CONNECT 协商、网关可用性和允许的目标端口。要区分其目标站点 DNS、连接和策略失败,需要服务商侧的诊断信息。
  • tunnel 为 407:按照代理认证指南处理。加大超时不会修正凭据。其他非 2xx 的 CONNECT 响应需要检查代理策略以及服务商文档中记录的错误含义;它们不是目标站点的 HTTP 状态码。
  • tunnel 为 200,目标站点 TLS 未完成:代理接受了隧道,但 HTTPS 建立没有完成。exit 35 表示 TLS 握手失败,exit 60 表示证书校验失败。检查目标站点名称、获准的 CA 信任和时钟;保持证书校验开启。此处的超时也可能源于隧道路径停滞。
  • TLS 和 ready 已完成,http 为 000:排查对目标站点响应的等待,包括远端应用以及经代理返回的路径。先通过同一网关检查一个获准的对照端点,再重新检查原始端点;一次只改一个变量。
  • http 为 200,bytes 停止增长且 exit 为 28:响应头已到达,但响应体没有在限制内完成。比较预期响应大小和传输进度。提高连接超时无法解决这个阶段的问题。
  • http 为 504:某个 HTTP 网关报告了它自己的上游超时。带 --fail 时,本示例通常以 exit 22 退出。这与 curl exit 28 不同,后者是客户端侧的时间限制到期;在修改客户端截止时间之前,先查明是哪个网关生成了该响应。

让每次尝试分得任务截止时间的一部分

在 curl 中,--connect-timeout 覆盖连接建立过程,包括 DNS 和必需的协议协商;它不只是一个 TCP 计时器。--max-time 覆盖整个传输,包括连接建立和响应体传输。连接预算包含在传输预算之内。示例中的 5 秒和 15 秒是小型诊断的起始值,不是服务保证,也不是普适的生产默认值。

应用仍然需要一个覆盖连接池等待、各次尝试、退避、响应体处理和清理的总截止时间。从调用方的限制倒推。以一个 20 秒的示例任务为例,你可以为本地工作预留 2 秒,允许首次尝试 8 秒,等待 1 秒,然后在剩余时间充足时最多允许一次 8 秒的重试。使用单调时钟,并用剩余预算限制每次后续尝试;重试时不要重置任务截止时间。

套接字读取超时通常限制的是等待数据的一个间隔,而不是整个任务。缓慢但持续的进展可能一再避开它。curl 的 --speed-limit 配合 --speed-time 可以中止持续缓慢的传输,但那是一种最低吞吐量策略,不是应用读取空闲超时的精确替代。长轮询和流式传输需要与其预期停顿相匹配的策略,并在适当时加上取消机制和总生存期。

只在重复既安全又有用时才重试

基线刻意不做任何重试。定位失败之后,对于应用语义安全且幂等的获准 GET 或 HEAD,以及你预计是临时性的失败,有界的重试是合理的。HTTP 方法语义只是起点;一个虽然使用 GET 却会触发动作的端点需要单独审查。写操作之后的超时会让结果处于不确定状态。在重新提交之前,先检查该操作的状态或文档中记录的幂等机制。

对于经审查的安全 GET,添加 --retry 1 --retry-max-time 20 最多允许对 curl 支持的临时性状况重试一次。它并不会让 20 秒成为严格的任务截止时间:--max-time 会为每次尝试重新计时,并且在重试窗口内开始的尝试可能在该窗口之后才结束。当调用方需要硬性限制时,保留一个外层截止时间。遵守 Retry-After,并在所需等待无法容纳时停止;应用的重试循环应使用带抖动的有界退避和并发限制。

  • 不要对未改变的认证、证书、URL 或策略错误反复重试。先修正配置。
  • 不要把 --retry-all-errors 作为超时修复手段加到通用客户端上。那会把重复扩大到那些恢复方式和副作用尚未审查的失败。
  • 不要仅仅因为响应丢失就重放支付、表单提交、消息发送或其他会改变状态的请求。响应丢失并不能证明操作失败。
  • 不要用重试来突破封锁或增加对已过载目标站点的负载。降低并发并遵守该服务的限制。

本地检查确认了什么

于 2026 年 9 月 11 日在 macOS 上使用 curl 8.7.1 及其报告的 SecureTransport/LibreSSL 构建进行了审查。受控的回环测试夹具演练了一次 HTTP 代理的 CONNECT 交换,以及一个使用显式信任的测试证书的本地 HTTPS 目标站点。停滞的 CONNECT 和停滞的目标站点 TLS 握手都在约 0.3 秒的连接预算附近以 28 退出,但只有后者的 tunnel=200。单独测试的停滞 HTTPS 代理 TLS 握手同样在 CONNECT 之前就退出了。

TLS 完成之后,停滞的响应头和停滞的响应体则触及了 1.2 秒的总预算。响应体的情形保留了 http=200 和已下载的 1 个字节。人为提供的 HTTP 504 返回了 exit 22。延迟的 CONNECT 增大了 tls 减 tcp 的差值,证实该差值包含隧道建立。一个重试窗口为 2 秒、每次尝试 0.7 秒的重试夹具运行了约 2.4 秒,证实仅靠重试窗口并不是严格的总截止时间。它最终的 total 字段描述的是最后一次尝试,因此带重试的任务还需要一个外层的耗时测量。

这些夹具确立的是客户端在受控失败下的行为。它们不是对服务商的基准测试,没有演练真实的 DNS 故障,没有验证每一种 curl 构建,也不能证明 HTTP/2、SOCKS 或多跳代理的行为。把运行时版本、阶段、状态码、计时、字节数和尝试次数与事件一起保存。只分享脱敏后的细节;目标站点 URL、请求头和跟踪记录可能包含私密数据。这些示例不能确立 ipvolt 的服务可用性或网关支持。

上线前检查

  • 记录实际的运行时、代理 scheme、目标站点 scheme 和 DNS 模式。
  • 在添加重定向、并发或重试之前,先捕获一次显式的尝试。
  • 结合状态码和已完成的计时里程碑来选择下一步检查。
  • 让连接预算和传输预算都处于总任务截止时间之内。
  • 只在剩余预算内重试获准的安全、幂等工作。

参考来源与延伸阅读

本指南参考的技术资料。请以你所安装版本的文档以及代理服务商支持的配置为准。