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

Source: https://ipvolt.com/zh/guides/proxy-timeout-troubleshooting
Markdown: https://ipvolt.com/zh/guides/proxy-timeout-troubleshooting.md
Language: zh-CN

[ipvolt 首页](https://ipvolt.com/zh.md) / [指南](https://ipvolt.com/zh/guides.md) / 一次一个阶段地排查代理超时

故障排查
审校于: 2026-09-11
阅读约 9 分钟
作者： ipvolt

用 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 模式。
- 在添加重定向、并发或重试之前，先捕获一次显式的尝试。
- 结合状态码和已完成的计时里程碑来选择下一步检查。
- 让连接预算和传输预算都处于总任务截止时间之内。
- 只在剩余预算内重试获准的安全、幂等工作。

## 参考来源与延伸阅读

- [Everything curl: proxy connections and DNS responsibility](https://everything.curl.dev/transfers/conn/proxies.html)
- [curl manual: variables, numeric output and retry-window limits](https://curl.se/docs/manpage.html)
- [libcurl: connection-completion timing](https://curl.se/libcurl/c/CURLINFO_CONNECT_TIME.html)
- [libcurl: TLS-completion timing](https://curl.se/libcurl/c/CURLINFO_APPCONNECT_TIME.html)
- [libcurl: pre-transfer timing](https://curl.se/libcurl/c/CURLINFO_PRETRANSFER_TIME.html)
- [libcurl: time to first response byte](https://curl.se/libcurl/c/CURLINFO_STARTTRANSFER_TIME.html)
- [Everything curl: exit-code meanings](https://everything.curl.dev/cmdline/exitcode.html)
- [libcurl: connection timeout within the total timeout](https://curl.se/libcurl/c/CURLOPT_CONNECTTIMEOUT.html)
- [libcurl: average-speed timeout policy](https://curl.se/libcurl/c/CURLOPT_LOW_SPEED_LIMIT.html)
- [Requests: read timeouts are not whole-download limits](https://requests.readthedocs.io/en/latest/user/quickstart/#timeouts)
- [Everything curl: retry behavior](https://everything.curl.dev/usingcurl/downloads/retry.html)
- [RFC 9110: idempotent retry semantics and gateway statuses](https://www.rfc-editor.org/rfc/rfc9110.html)

## 相关指南

- [在 curl 中使用代理：-x、环境变量、SOCKS5 与认证](https://ipvolt.com/zh/guides/curl-proxy-setup.md)
- [不靠猜测修复代理错误 407](https://ipvolt.com/zh/guides/fix-proxy-error-407.md)
- [代理环境变量：HTTP_PROXY 与 NO_PROXY](https://ipvolt.com/zh/guides/proxy-environment-variables.md)

## 关于 ipvolt

示例使用通用的代理设置，并附上原始技术文档链接。具体产品的行为请向你的服务商确认。ipvolt 仍在开发中。

[阅读英文原文](https://ipvolt.com/guides/proxy-timeout-troubleshooting.md)

## 了解何时开放体验。

ipvolt · 开发中

我们正在为开发者和数据团队打造代理基础设施。加入意向名单，ipvolt 就绪时第一时间收到通知。

开放体验时仅发一封通知邮件，不发送其他邮件。

[申请抢先体验](https://ipvolt.com/zh/guides/proxy-timeout-troubleshooting#waitlist-closing)

[隐私政策](https://ipvolt.com/privacy)

