"可用率 99%""城市覆盖充足""支持粘性会话"都是服务描述,不能直接替代交付验收。真正落到项目里,研发和采购需要拿到同一份答案:某地区、某协议、某个时间窗口内,代理连接是否成功,业务请求是否完成,重试成本有多高,出口是否符合约定,故障发生后多久恢复。

文章里的数值样例只用于说明指标计算,不代表任何服务商的实测结果。采购结论必须基于本次交付资源、同一测试配置和完整原始日志。
一、验收对象不是"一个 IP",而是一次完整请求
代理服务从客户端到目标接口至少经过三层:客户端与代理端点建立连接、代理转发请求、目标服务按接口契约返回结果。不同层的失败不能用一个"成功率"概括。
| 层次 | 观测点 | 典型失败 | 是否应直接判为资源不可用 |
|---|---|---|---|
| 代理连接层 | TCP/代理握手、认证 | DNS 失败、连接超时、ProxyError、407 |
连接失败通常需要复测;407 优先检查鉴权 |
| HTTP 传输层 | 是否收到响应、状态码 | 408、429、5xx、TLS 错误 | 不一定,目标端限流和上游故障需拆分 |
| 业务层 | 响应体是否满足约定 | HTTP 200 但返回错误字段、空数据 | 取决于接口契约,不能只看状态码 |
因此,验收表至少应同时列出连接可用率、首次业务成功率、最终业务成功率、重试率、P50/P95、地区一致率和恢复时间。把重试后的成功全部塞进"可用率",会掩盖额外请求、等待时间和任务积压成本。
二、7 项指标如何定义,分母必须写进报告
服务等级协议(SLA)是服务约定,测试得到的是某个窗口内的观测值。要让不同服务商的 PoC 结果可比较,先统一指标口径,再讨论阈值。
| 指标 | 分子 / 分母 | 建议用途 | 常见统计错误 |
|---|---|---|---|
| 连接可用率 | connection_ok / planned_requests |
验证端点、协议、认证与基础链路 | 用 HTTP 200 替代连接结果 |
| 首次业务成功率 | first_attempt_ok / planned_requests |
衡量不依赖容错时的初始质量 | 用最终成功率覆盖首次失败 |
| 最终业务成功率 | business_ok / planned_requests |
衡量当前重试策略下的任务完成情况 | 分母改成"连接成功请求" |
| 重试率 | attempts > 1 的逻辑请求 / planned_requests |
衡量额外耗时和资源消耗 | 按网络尝试次数而不是逻辑请求计数 |
| P50 / P95 | 成功逻辑请求的耗时分位数 | 观察常态与尾部延迟 | 将失败请求记为 0 ms |
| 地区一致率 | 符合约定地区的有效出口 / 可定位有效出口 | 核验地区、城市或 ASN 交付 | 只查一个定位库 |
| 恢复时间 | T5 - T0 |
衡量故障出现到连续恢复的时间 | 用客服回复时间替代恢复时间 |
"逻辑请求"指一个业务任务。例如下载一份授权报表,即使客户端重试两次,业务统计里仍只能算一个任务。日志中应保存 attempts,而不是将每一次尝试都当作独立样本。
三、测试设计:固定变量,避免一次测速得出错误结论
代理质量会受到目标端点、时段、并发、协议、超时和客户端复用策略影响。下面这些配置应随报告一起归档:
| 配置项 | 推荐做法 | 目的 |
|---|---|---|
| 测试端点 | 自建回显接口、供应商检测端点或明确授权的 API | 排除第三方站点策略对结论的干扰 |
| 请求量 | 每个地区、协议、会话类型单独统计 | 不混合不同条件得到一个总百分比 |
| 并发与间隔 | 固定并发、请求间隔、连接复用和超时 | P95 和超时率与压测方式直接相关 |
| 重试策略 | 写明最大次数、退避和允许重试的错误 | 最终成功率才有解释边界 |
| 地区约束 | 国家/城市/ASN 哪个是硬条件写入测试单 | 防止验收后才讨论定位标准 |
| 会话约束 | 固定、粘性或轮换会话单独测试 | 资源类型不同,出口变化的含义相反 |
HTTP 407(Proxy Authentication Required)代表代理要求认证,不能与网络超时混为一类。HTTP 429 通常反映目标服务限流或请求频率不匹配,也不能自动归责为代理端失效。RFC 9110 对 407 的定义和 RFC 6585 对 429 的定义,正是错误分类要保留状态码与响应头的原因。

四、可运行的探测脚本:每个逻辑请求只写一条 JSONL 日志
下面脚本使用 requests,通过环境变量接收代理地址和授权测试端点,不在代码中保存账号、密码或密钥。脚本默认执行两次尝试:只对连接/读取超时与 502、503、504 做一次重试;407、403、429 不自动重试,因为它们通常需要调整鉴权、业务策略或遵循目标端的 Retry-After 指示。
安装依赖:
python -m pip install requests
运行示例:
set PROXY_URL=http://user:password@proxy.example.com:8000
set TEST_URL=https://your-authorized-endpoint.example/health
python proxy_probe.py
from __future__ import annotations
import json
import os
import time
from dataclasses import asdict, dataclass
from pathlib import Path
import requests
PROXY_URL = os.environ["PROXY_URL"]
TEST_URL = os.environ["TEST_URL"]
REQUESTS = 50
MAX_ATTEMPTS = 2
TIMEOUT = (5, 15) # connect timeout, read timeout
OUTPUT = Path("proxy_probe.jsonl")
@dataclass
class ProbeResult:
request_id: int
connection_ok: bool
business_ok: bool
status_code: int | None
attempts: int
elapsed_ms: float | None
first_error: str | None
final_error: str | None
def error_name(exc: requests.RequestException) -> str:
if isinstance(exc, requests.exceptions.ProxyError):
return "proxy_error"
if isinstance(exc, requests.exceptions.ConnectTimeout):
return "connect_timeout"
if isinstance(exc, requests.exceptions.ReadTimeout):
return "read_timeout"
if isinstance(exc, requests.exceptions.SSLError):
return "tls_error"
return type(exc).__name__.lower()
def retryable(status_code: int | None, error: str | None) -> bool:
return error in {"connect_timeout", "read_timeout", "proxy_error"} or status_code in {
502, 503, 504
}
def probe(session: requests.Session, request_id: int) -> ProbeResult:
first_error = None
final_error = None
status_code = None
connection_seen = False
for attempt in range(1, MAX_ATTEMPTS + 1):
started = time.perf_counter()
try:
response = session.get(
TEST_URL,
proxies={"http": PROXY_URL, "https": PROXY_URL},
timeout=TIMEOUT,
allow_redirects=False,
)
elapsed_ms = (time.perf_counter() - started) * 1000
status_code = response.status_code
connection_seen = True
business_ok = 200 <= status_code < 300
final_error = None if business_ok else f"http_{status_code}"
if attempt == 1 and final_error:
first_error = final_error
if business_ok or not retryable(status_code, final_error):
return ProbeResult(
request_id, True, business_ok, status_code, attempt,
round(elapsed_ms, 1) if business_ok else None,
first_error, final_error,
)
except requests.RequestException as exc:
final_error = error_name(exc)
if attempt == 1:
first_error = final_error
if not retryable(None, final_error):
break
if attempt < MAX_ATTEMPTS:
time.sleep(0.5)
return ProbeResult(
request_id, connection_seen, False, status_code, attempt,
None, first_error, final_error,
)
with requests.Session() as session, OUTPUT.open("w", encoding="utf-8") as file:
for request_id in range(1, REQUESTS + 1):
result = probe(session, request_id)
file.write(json.dumps(asdict(result), ensure_ascii=False) + "\n")
time.sleep(0.2)
脚本有意不使用 response.raise_for_status():验收需要把 407、429、5xx 作为可统计的结果写入日志,而不是全部落入同一种异常。business_ok 目前以 2xx 为例;接入真实 API 时,应按接口契约检查响应 JSON 中的状态字段、数据完整性或签名校验结果。
requests 的官方文档建议在单次请求中显式传入 proxies,因为 Session.proxies 可能被环境变量中的代理设置覆盖。这个细节会直接影响 PoC 的可复现性。
五、离线汇总:P95、重试率和错误分布如何同时读取
探测脚本生成 JSONL 后,可以用下面这段汇总代码得到报告核心指标。它不重新发起任何请求,便于在同一份原始日志上反复调整统计规则。
import json
import statistics
from collections import Counter
with open("proxy_probe.jsonl", encoding="utf-8") as file:
rows = [json.loads(line) for line in file if line.strip()]
total = len(rows)
connected = sum(row["connection_ok"] for row in rows)
business_ok = sum(row["business_ok"] for row in rows)
retried = sum(row["attempts"] > 1 for row in rows)
latencies = [row["elapsed_ms"] for row in rows if row["business_ok"]]
errors = Counter(row["final_error"] for row in rows if row["final_error"])
print("连接可用率:", f"{connected / total:.2%}" if total else "n/a")
print("业务成功率:", f"{business_ok / total:.2%}" if total else "n/a")
print("重试率:", f"{retried / total:.2%}" if total else "n/a")
print("错误分布:", dict(errors))
if len(latencies) >= 20:
print("P50:", f"{statistics.median(latencies):.1f} ms")
p95 = statistics.quantiles(latencies, n=100, method="inclusive")[94]
print("P95:", f"{p95:.1f} ms")
else:
print("P50/P95: n/a(成功样本少于 20)")
statistics.quantiles(..., method="inclusive") 使用样本端点参与插值,适合对当前日志做描述性汇总;无论采用哪一种百分位算法,都应在不同候选之间保持一致。少于 20 条成功样本时,P95 的数值容易被个别请求主导,报告应直接标记为样本不足。
下表展示的是指标之间的判读关系,而不是固定淘汰阈值:
| 现象 | 更可能的问题位置 | 下一步动作 |
|---|---|---|
连接可用率低,错误集中在 proxy_error |
端点、路由或代理服务 | 保留异常栈与时段,联系服务商复核 |
| 407 占比高 | 用户名密码、白名单、协议或授权范围 | 核对鉴权配置,不把它算作资源质量 |
| 429 占比高 | 目标端限流、并发或速率策略 | 降频,遵循 API 文档与响应头后复测 |
| P50 正常、P95 很高 | 拥塞、连接复用、尾部慢请求 | 对照并发、时段与超时,观察错误分布 |
| 最终成功率高但重试率高 | 客户端容错掩盖初始波动 | 将首次成功率和重试成本写入决策表 |
六、地区、会话与故障恢复:三项独立验收,不要借一个指标代替另一个
地区校验应采用至少两个独立定位来源,并保存查询日期、出口地址和原始响应。若订单只约定国家级出口,城市级差异应记为复测项;若城市是交付条件,测试文件必须明确城市名称、允许的行政区范围和争议处理规则。IP 地理库存在更新差异,单一截图不是足够证据。
会话保持也需要按资源类型解释。固定或粘性会话可按 session_id 汇总:在约定窗口内,exit_ip 去重数为 1 的会话才计为保持成功。轮换资源则不应拿"出口不变"作为目标,重点应改为并发下的成功率、错误分布和替换后的恢复速度。
故障恢复建议使用统一时间线:
| 时间点 | 记录内容 | 计算结果 |
|---|---|---|
| T0 | 连续失败达到故障阈值的时间和错误类型 | 故障开始 |
| T2 | 工单创建时间、请求 ID、日志附件 | 支持计时起点 |
| T3 | 首次有效人工响应与处理动作 | 首次响应时间 T3 - T2 |
| T5 | 连续 3 次探测恢复成功 | 技术恢复时间 T5 - T0 |
| T6 | 根因说明、影响范围和关闭时间 | 完整闭环时间 T6 - T2 |
客服"已收到"不等于技术恢复;端口恢复也不等于业务恢复。报告应把这两层分别记录。
七、决策路径与可复制验收清单
[协议、地区、会话类型是否满足交付条件?]
|
否 ------> [记录缺项或配置问题,限范围复测]
|
是
|
[连接可用率、首次成功率、P95 是否达到内部目标?]
|
否 ------> [按 DNS / 407 / 429 / 5xx / 超时归因]
|
是
|
[重试率与会话保持是否符合业务容忍度?]
|
否 ------> [调整重试策略或测试对应资源类型]
|
是
|
[地区双定位与故障恢复记录是否完整?]
|
否 ------> [补齐证据后复测]
|
是
|
[进入候选集合,再比较报价、条款和支持等级]
候选服务商只应在同一份测试表、同一端点、同一并发和同一错误分类规则下比较。IPdodo代理IP支持 HTTP、HTTPS、SOCKS5 接入,可以作为候选对象进入 PoC;最终是否满足项目要求,仍应以本文脚本生成的实际日志、地区校验和故障演练记录为准,而非直接采用页面数字。
- 测试端点为自建、授权或明确允许的检测端点;
- 一条日志代表一个逻辑请求,重试次数记录在
attempts; - 连接可用率、首次成功率、最终业务成功率、重试率分开呈现;
- 407、429、5xx、DNS、代理连接失败和读取超时分开统计;
- P50/P95 只基于业务成功请求,不使用失败请求的伪造耗时;
- 城市或 ASN 是硬门槛时,已保留两个定位来源的原始结果;
- 已记录 T0、T2、T3、T5、T6,能够回算恢复与响应时间;
- 原始 JSONL、脚本版本、环境变量说明和测试配置已一同归档。
FAQ
为什么 HTTP 200 不能直接算成功?
HTTP 200 只表示 HTTP 层成功返回。许多 API 会在响应体中给出业务错误、空数据或未完成状态,验收脚本应依据接口契约补充业务判断。
P95 要用平均延迟替代吗?
不能。平均值反映整体水平,P95 反映尾部慢请求。两者应同时保留,并结合超时与重试率解读。
公开 SLA 可以替代 PoC 吗?
不能。公开 SLA 描述服务范围和目标口径,PoC 验证的是特定地区、协议、会话类型、业务端点和负载下的实际表现。两份证据共同构成采购判断。