代理 IP 服务商 SLA 怎么验?7 项指标与 Python 探测脚本实战

"可用率 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 验证的是特定地区、协议、会话类型、业务端点和负载下的实际表现。两份证据共同构成采购判断。

参考资料

相关推荐
海绵宝宝转agent12 小时前
learn-claude-code第1-5章开源学习笔记分享
人工智能·笔记·python·学习
Ruiery12 小时前
Linux 6.6内核 CPU 深度解析(七):调度器启动 — 从单核到多核,调度器分阶段点亮
linux·运维·服务器
小奇不哭12 小时前
ROS2 单节点,RGB-D 深度相机局部路径提取 + B 样条平滑路径 + YOLOv8 障碍物检测 + 深度测距,用于移动机器人视觉局部循迹。
python·yolov8·ros2·cv2·路径探索循迹
50万马克的面包13 小时前
CSDN-栈队列数组-知识点整理
linux·运维·服务器
小小龙学IT13 小时前
Python scikit-learn 机器学习库深度解析
python·机器学习·scikit-learn
MicrosoftCloud13 小时前
用户权限 04|/etc/sudoers 安全配置:从最小授权到「别把 ALL 随便给人」
运维·安全·ubuntu·sudo·sudoers
网络小江13 小时前
上网第三十九课:TCP 的可靠性——滑动窗口、重传与拥塞控制
网络协议
2601_9620715713 小时前
类变量和全局变量的查找路径有什么区别?
开发语言·python
꯭自꯭闭꯭14 小时前
达梦事物特性及MVCC
linux·运维·数据库
卷无止境14 小时前
独立开发者的"富矿地带":哪些垂直领域值得你押注一辈子?
后端·python