使用应用层成功率、响应时间分布、连接建立和会话行为来比较代理。在收集结果之前,先为每个目标站点定义什么是成功。下面的示例包括一次实测的直连基线,以及一个检查响应标记并计算成功率和延迟的小型测试脚本。我们尚未发布可比较的住宅代理与数据中心代理结果。
在测量速度之前先定义成功
当失败触发重试时,较低的响应时间可能掩盖每个成功请求的高成本。除了耗时,还要统计完成的工作量,并把所有重试流量计入成本估算。
成功需要针对每个目标站点分别定义:预期的状态码、必需的内容以及可接受的重定向结果。包含 CAPTCHA 质询的 HTTP 200 响应可能不符合该定义。缺失字段或不完整的下载同样可能不符合。简单的文本标记适合作为入门检查;生产工作负载可能需要解析后的记录、模式验证或完整的多步骤流程。
分别记录成功的响应和每一类失败,然后调查原因。超时可能发生在路径上的多个环节。单凭 403 无法识别是哪个中间节点或源站实施了限制。响应时间和状态码都不足以归责。
记录首字节时间和总时间
curl 报告的时间节点从传输开始计时。首字节时间(TTFB)告诉你响应何时开始;总时间包括接收响应体。两者都要记录,因为首字节很快,之后仍可能是缓慢或不完整的下载。
2026 年 9 月 11 日,从赫尔辛基的一台 VPS 使用 curl 8.18 对 https://example.com/ 连续发出三次直连请求,得到以下数值。未使用代理。下面的标签明确了各值的累计含义:
http=200 tcp_complete=0.009780s tls_complete=0.023224s ttfb=0.044821s total=0.044983s
http=200 tcp_complete=0.008289s tls_complete=0.023851s ttfb=0.039902s total=0.039966s
http=200 tcp_complete=0.008149s tls_complete=0.031935s ttfb=0.061614s total=0.061688s这是一个实测的首字节约 40–62 ms 的基线,而不是未来请求的下限。三次观测无法确立稳定的分布。请在临近代理比较的时间,在同一台机器和同一目标站点上重新采集基线,并保留运行日期、客户端版本和配置。
下面这条有界命令为一次新的直连请求收集相同的字段:
curl --disable --silent --show-error --noproxy '*' \
--connect-timeout 5 --max-time 20 --retry 0 \
--output /dev/null \
--write-out 'http=%{http_code} tcp_complete=%{time_connect}s tls_complete=%{time_appconnect}s ttfb=%{time_starttransfer}s total=%{time_total}s exit=%{exitcode}\n' \
'https://example.com/'要通过 HTTP(S) 代理发出同样的请求,请使用 curl 8.3 或更高版本。把 PROXY_URL 设为供应商的、不含凭据的网关,包括其支持的协议和端口。让密钥管理器在环境中提供 PROXY_USERNAME 和 PROXY_PASSWORD。此示例遵循 curl 设置指南:凭据由 curl 自行导入,因此展开后的密码不会成为 shell 参数。不要开启 shell 跟踪。
: "${PROXY_URL:?Set a credential-free HTTP(S) gateway URL}"
curl --disable --silent --show-error --noproxy '' \
--proxy "$PROXY_URL" --proxy-basic \
--variable %PROXY_USERNAME --variable %PROXY_PASSWORD \
--expand-proxy-user '{{PROXY_USERNAME}}:{{PROXY_PASSWORD}}' \
--connect-timeout 5 --max-time 20 --retry 0 \
--output /dev/null \
--write-out 'http=%{http_code} tunnel=%{http_connect} tcp_complete=%{time_connect}s tls_complete=%{time_appconnect}s ttfb=%{time_starttransfer}s total=%{time_total}s exit=%{exitcode}\n' \
'https://example.com/'--disable 放在最前面以忽略个人 curl 配置。直连命令的 --noproxy '*' 绕过继承的代理;代理命令的空绕过列表则防止 NO_PROXY 把目标站点排除在外。这些命令每个 curl 进程使用一个新连接,不跟随重定向,也不重试。由于命令有意省略了 --fail,HTTP 错误时 curl 退出码仍可能为零;请单独检查 HTTP 状态。路由选择参见 curl 手册,截止时间参见超时指南。
理解连接时序包含了什么
对于经 HTTP 代理访问的 HTTPS 目标站点,curl 先建立到代理的连接,请求 CONNECT 隧道,然后通过该隧道与目标站点协商 TLS。RFC 9110 定义了 CONNECT。
time_connect 与 time_appconnect 之间的间隔包含隧道建立和 TLS 建立。减去相应的直连间隔并不能隔离出 CONNECT 开销:路由和握手条件也会变化。HTTPS 代理还会增加一次 TLS 握手。静态 CDN 对象并不能消除这些差异。
利用这些时间节点定位延迟出现在哪里,然后借助受控端点或经过适当插桩的客户端和代理日志进行调查。只对同一次简单传输中已完成的节点做减法。失败的请求、复用的连接和重定向需要单独解读;零值可能意味着某个阶段尚未到达。
%{http_connect} 报告 HTTP CONNECT 响应码,与传输的 HTTP 状态分开。--proxy-header 向代理发送请求头;它不报告隧道状态。优先使用选定的状态和时序字段,而不是可能包含身份验证数据的详细跟踪。这些解读遵循 curl 的时序字段文档。
对照承诺的策略检查轮换
出口 IP 是目标站点观察到的公网地址。查询供应商支持的诊断端点,或你自己控制的、能报告调用方地址的端点。ipify 是一个公开选项;在自动化请求之前请查看其当前的使用指引。
在把结果归约为唯一地址计数之前,先保留带时间戳的结果序列。对每次尝试,记录所选地区、会话设置、传输结果以及经过验证的 IPv4 或 IPv6 地址。把失败的请求和无效的响应体排除在地址计数之外,同时单独报告这些失败。在 Python 中,ipaddress.ip_address(value.strip()) 可以验证纯文本的地址响应。
把该序列与供应商的轮换策略进行比较。轮换可能按请求、按连接或按时间间隔发生,池也可能重用某个地址。除非产品明确承诺,否则十个请求并不要求十个不同的出口。粘性会话旨在在其文档说明的时长内保留同一出口;在把重新分配视为故障之前,先检查其过期和断开行为。
使用你的工作负载所需的国家或地区以及会话设置。在相关的会话窗口内重复测试。轮换代理与粘性代理指南解释了应评估哪些行为。下面的内容基准测试不测量轮换。
选择样本量和观测窗口
十个请求可以发现基本故障,但对延迟分布几乎提供不了什么证据。缓慢的请求可能拖住队列;单独的平均值可能掩盖缓慢的离群值。
我们建议的起点是每个目标站点、每种配置 200 个请求,分布在至少一小时内。这是一个测试设计上的选择,不是通用的可靠性阈值。小样本的第 95 百分位取决于极少数观测。请报告样本量、失败次数和观测窗口,然后在与工作负载相关的不同时间和条件下重复。
比较供应商时,在同一窗口内交错发送它们的请求。这能减少目标站点负载变化造成的差异,但不保证条件完全相同。保持请求内容、客户端设置和成功标准一致。连接复用和生产环境的并发应与下面这个顺序执行、每次新建进程的示例分开测试。
一个带汇总的小型内容基准测试
将其保存为 proxy-bench.py,并运行 python3 proxy-bench.py。它需要 Python 3 和 curl 8.3+。它使用与上面相同的私有代理环境变量。直连运行时设置 BENCH_MODE=direct,否则保持为 proxy。每次运行都会创建一个新的结果目录并打印其路径。
默认目标站点是一个小型示例页面。要进行实际比较,请把 BENCH_URL 设为一个获准的 HTTPS 端点,并把 BENCH_MARKER 替换为有意义的预期字符串。不要使用包含凭据或敏感参数的 URL。在引用应用层成功率之前,先根据你的真实响应格式调整内容检查。
import collections
import datetime
import json
import math
import os
from pathlib import Path
import statistics
import subprocess
import tempfile
import time
url = os.environ.get("BENCH_URL", "https://example.com/")
marker = os.environ.get("BENCH_MARKER", "<h1>Example Domain</h1>").encode()
mode = os.environ.get("BENCH_MODE", "proxy")
if not url.startswith("https://") or not marker or mode not in {"direct", "proxy"}:
raise SystemExit("Use an HTTPS URL, a nonempty marker and direct or proxy mode")
route = ["--noproxy", "*"]
if mode == "proxy":
route = ["--noproxy", "", "--proxy", os.environ["PROXY_URL"], "--proxy-basic",
"--variable", "%PROXY_USERNAME", "--variable", "%PROXY_PASSWORD",
"--expand-proxy-user", "{{PROXY_USERNAME}}:{{PROXY_PASSWORD}}"]
fields = ["http", "tunnel", "tcp_complete", "tls_complete", "ttfb", "total"]
write_out = "%{http_code} %{http_connect} %{time_connect} %{time_appconnect} %{time_starttransfer} %{time_total}"
count, window = 200, 3600.0
directory = Path(tempfile.mkdtemp(prefix="proxy-bench-", dir="."))
rows = []
started = time.monotonic()
with (directory / "results.jsonl").open("x") as log:
for i in range(count):
due = started + i * window / (count - 1)
time.sleep(max(0, due - time.monotonic()))
timestamp = datetime.datetime.now(datetime.timezone.utc).isoformat()
with tempfile.TemporaryDirectory() as scratch:
body = Path(scratch) / "body"
result = subprocess.run(
["curl", "--disable", "--silent", "--connect-timeout", "5",
"--max-time", "20", "--retry", "0", *route,
"--output", str(body), "--write-out", write_out, "--url", url],
capture_output=True, text=True)
values = result.stdout.split()
timings = dict(zip(fields, values)) if len(values) == len(fields) else {}
if result.returncode:
outcome = f"curl_{result.returncode}"
elif not timings:
outcome = "missing_measurements"
elif timings["http"] != "200":
outcome = "http_" + timings["http"]
elif not body.exists() or marker not in body.read_bytes():
outcome = "content_mismatch"
else:
outcome = "success"
row = dict(attempt=i + 1, timestamp=timestamp, mode=mode,
exit_code=result.returncode, outcome=outcome, **timings)
rows.append(row)
log.write(json.dumps(row) + "\n")
log.flush()
successful = [row for row in rows if row["outcome"] == "success"]
summary = {"attempts": len(rows), "successes": len(successful),
"success_rate": len(successful) / len(rows),
"outcomes": dict(collections.Counter(row["outcome"] for row in rows))}
for metric in ("ttfb", "total"):
samples = sorted(float(row[metric]) for row in successful)
summary[metric] = ({"median_seconds": statistics.median(samples),
"p95_seconds": samples[math.ceil(0.95 * len(samples)) - 1]}
if samples else None)
(directory / "summary.json").write_text(json.dumps(summary, indent=2) + "\n")
print(directory)
print(json.dumps(summary, indent=2))第一个请求立即开始;第 200 个请求安排在第一个请求之后 3,600 秒。请求保持顺序执行。如果某个请求耗时超过间隔,后续的开始时间会顺延,整个运行也会变长。实际的开始时间戳保留在日志中。没有自动重试,也不跟随重定向,因此在这个示例中 3xx 响应算作 HTTP 失败。如果你的工作负载需要重定向,请在扩展脚本之前定义哪些目标站点和最终内容是可接受的。
每一行日志包含尝试编号、UTC 开始时间戳、路由模式、curl 退出码、结果、HTTP 和 CONNECT 状态,以及四个以秒为单位的累计时间节点。该脚本的结果文件既不保留响应体也不保留凭据。请另行保存一份私有的运行记录,记下供应商、池、目标、客户端版本和会话配置。
成功率的分母是所有尝试过的请求,包括超时和内容失败。curl 退出码(如 28(超时)、7(连接失败)和 56(接收失败))描述的是宽泛的失败类别,而不是已确认的故障组件。调查时请使用官方退出码参考。
延迟统计只使用成功的响应,汇总中在其旁边保留失败计数。中位数是中间值,必要时取中间两个值的平均。第 95 百分位使用最近秩方法:对成功样本排序,选取从 1 开始计数的第 ceil(0.95 × sample_count) 位。没有成功时,两个延迟汇总均为 null。
例如,五次尝试中四次成功,成功率为 80%。如果它们的 TTFB 值分别为 0.10、0.12、0.20 和 0.40 秒,则中位数为 0.16 秒,最近秩 p95 为 0.40 秒。这些是用于说明计算方法的虚构数值,不是实测的代理结果。
先对脚本和你的内容规则做一次简短的受控检查,然后运行约定的观测窗口。在任何延迟比较旁边一并公布配置、样本量、失败情况和成功定义。上面的直连基线仅作示例;它不构成代理排名。