代理池里明明有很多 IP,请求也在不断轮换,成功率却没有明显提高:超时之后换一个,仍然超时;刚返回 403 的节点几秒后又被另一台 worker 选中;某个 IP 访问站点 A 已经失效,却因为访问站点 B 正常而继续被分配给 A。此时继续增加轮换频率,往往只是在更快地重复失败。

问题不在“有没有轮换”,而在轮换之前是否完成了三件事:判断节点对当前目标是否健康、让短期失效节点进入冷却、把有充分证据的失败节点移出可调度集合。轮换只是选择下一个出口的动作,健康管理才决定下一次应该选谁。

一个开源代理调度工具的作者曾公开报告一组七日测试:在其自有的 549,114 次请求环境中,按近期表现选择代理的成功率高于轮询与随机选择。这个数字来自工具作者自己的负载和实现,不能视为行业平均值或服务承诺,但它揭示的机制值得验证:轮询和随机策略会持续把请求分给已经失效的节点,除非系统把请求结果反馈给调度器。

文章目录

  1. 为什么频繁轮换仍然失败
  2. 代理健康不能只看一个总分
  3. 先分类失败,再决定处理动作
  4. 健康评分应该怎样设计
  5. 冷却、半开探测与节点剔除
  6. 调度闭环怎样落地
  7. 上线指标与服务边界
  8. 上线检查与常见问题
  9. 结论

一、为什么频繁轮换仍然失败

Round-robin(轮询)保证每个节点依次获得请求,随机选择则让分配更分散。两者都能避免所有流量固定在同一个 IP 上,但它们默认代理池中的节点质量相近,而且不会根据刚发生的失败调整选择概率。真实代理池通常不满足这个前提。

只做轮换,至少会留下四个缺口:

  • 失败节点仍在候选集合里:某个 IP 连续超时后,没有状态变化,下一轮仍会被选中。
  • 所有失败被当成同一种失败:代理连接失败、目标站 429、来源不明的 5xx、页面返回错误内容,可能触发完全不同的处理动作。
  • 只维护一个“好/坏”标签:节点访问站点 A 被限制,并不代表它访问站点 B 也不可用。
  • 重试放大故障:多个 worker 同时遇到问题并立即换 IP 重试,会快速消耗代理流量、并发额度和目标站请求预算。

因此,轮换策略至少要接收四类输入:节点最近是否成功、失败发生在哪一层、失败针对哪个目标、节点当前是否处于冷却状态。如果正在从零搭建获取、验证、存储和调度的完整系统,可先看 IPWeb 的代理 IP 池搭建指南;本文只深入其中最容易被低估的一段——从请求反馈到下一次选路的控制闭环。

二、代理健康不能只看一个总分

“这个代理健康吗”不是一个完整问题。更准确的问法是:它的网络链路是否健康,以及它对当前目标站是否健康。

全局健康与目标健康分开

全局健康描述代理自身及其接入链路的总体可用性,例如代理认证、隧道建立、中立出口探测和跨目标的连接表现。一个节点在多个无关目标上都无法连接,或中立探测也持续失败,才更像全局故障。

目标健康描述 proxy_id + target_id 这一组合的表现。某个出口可能在特定电商站连续收到 403,但访问其他站点正常。把这种失败直接写进全局分数,会误伤仍有价值的节点;完全忽略,又会让调度器继续把它分给已经失败的目标。

自建代理池至少应保存:

  • global_score[proxy_id]:代理链路的总体健康度;
  • target_score[proxy_id][target_id]:该代理对特定目标的有效请求表现;
  • state[proxy_id][target_id]:健康、可疑、冷却、半开探测或剔除;
  • 连续失败次数、样本量、最近成功与失败时间、延迟分位数;
  • inflight 或带过期时间的并发租约。

粘性会话不能静默换 IP

登录、购物车和有状态分页通常需要在一段会话内保持出口稳定。绑定节点失效时,调度器既不能无条件继续返回坏节点,也不应悄悄换 IP 破坏会话。更合理的结果是返回 session_restart_required,由业务层从检查点重建会话并重新绑定出口。无状态、幂等的独立请求才适合更频繁轮换。两种模式的区别可参考动态住宅 IP 的 Per-request 与 Sticky Session 说明

三、先分类失败,再决定处理动作

健康管理最容易出错的地方,不是评分公式,而是把不同原因都记成 failure。一个 407、一个 429 和一个 503 不应该对代理产生相同影响。

观测结果 判断重点 建议动作 是否直接全局剔除
DNS 解析失败 先确认域名在客户端还是代理侧解析;HTTP 代理、SOCKS5 本地解析与远端解析的故障归属不同 记录解析位置;结合本地直连、中立探测和其他节点复测,再决定影响客户端告警还是代理全局分数
TCP 连接或代理隧道建立失败 代理入口、出口节点或网络链路 用中立地址和多个目标复测;连续、跨目标失败后进入全局冷却 否,先达到样本阈值
407 Proxy Authentication Required 账号、白名单或认证配置 检查凭据、授权 IP 和接入参数;不要把配置错误批量归罪于出口 IP
连接或读取超时 代理、目标站、客户端或网络拥塞均可能 记录超时阶段;结合中立探测、同目标其他代理和延迟基线判断 否,先中低权重扣分
429 Too Many Requests 当前目标的速率限制 按目标冷却;若响应包含 Retry-After,优先按其等待;降低目标并发
403、验证页或访问限制页 通常与当前目标、会话或请求环境有关 先记入目标健康;对比其他目标与中立探测,再决定是否扩大隔离范围 通常不
502、503、504 等 5xx 可能来自目标站、CDN、代理网关或中间负载均衡器 结合响应头、错误模板、服务商错误码和同时段其他代理表现判断来源;确认来自目标站时不直接惩罚单个代理
HTTP 200,但内容缺失或不属于目标实体 目标限制、会话、地区、页面改版或解析层 计为业务失败;若大量不同节点同时出现相同错误,先暂停代理扣分并检查页面与解析规则

RFC 6585 对 429 的定义允许服务端用 Retry-After 告知等待时间,但服务端也可能直接丢弃连接而不返回 429。因此,“没有看到 429”不能证明没有限流;仍要结合同时段、同目标和不同代理的失败分布判断。

增加目标级群体异常保护

如果同一目标在多个供应商、地区和大量代理上同时出现相同的 content_invalid,更可能是页面改版、实验分组、登录要求或解析器失效。此时应打开目标级事件,暂时冻结这类错误对代理分数的影响。否则反馈系统会把一个解析问题放大成“整个代理池都坏了”。

四、健康评分应该怎样设计

健康评分不必一开始就做成复杂模型。先用可解释、能回放的规则,通常比一个无法说明扣分原因的“智能分数”更有用。在状态变化较快的代理池中,近期短窗口通常比很久以前的历史样本更能反映当前状态,但窗口大小仍要根据请求频率和节点变化速度回测。

近期有效率使用 EWMA

m_t = α × x_t + (1 - α) × m_(t-1)

x_t 可以是本次请求是否通过业务校验的 0 或 1,α 决定新样本权重。α 越大,分数对近期变化越敏感。它不是固定推荐值,应结合实际波动回测。

业务有效性的 0/1 结果适合更新目标级近期有效率;全局健康应主要使用代理认证、连接、隧道、中立探测和跨目标链路结果,不能直接使用所有业务失败。

先把每个分量统一到 0~1

latency_score = max(0, 1 - min(P95_latency / latency_limit, 1))
stability_score = 1 - min(failure_rate_variation / allowed_variation, 1)

latency_limit 是当前目标能够接受的 P95 延迟上限;failure_rate_variation 可以用相邻短窗口失败率的差值或滚动标准差表示。团队必须固定计算方式,不能一会儿衡量延迟波动、一会儿衡量成功率波动。

在分量统一后,可以从一个可解释的示例开始:

health_score = 0.60 × recent_valid_rate + 0.25 × latency_score + 0.15 × stability_score
final_weight = global_score × target_score / (1 + inflight)

权重只是启动测试的示例,不是通用最优解。实际系统还要设置最低样本量:刚成功一次的新节点不能立刻获得与长期稳定节点相同的权重。每次更新应保存旧分数、新分数、触发事件和规则版本,以便回放误判。

五、冷却、半开探测与节点剔除

不要只给 IP 保存永久的 available=true/false。网络状态会变化,目标限制也可能在一段时间后解除。更稳妥的做法,是让节点在有限状态之间转换。

代理节点从候选、健康、可疑、冷却到半开探测、恢复或剔除的状态机
失败节点先短期隔离,再通过受控探测决定恢复或剔除。
  1. 候选:新加入或长期未使用的节点,只进入独立探测队列。
  2. 健康:样本量和近期有效率达到阈值,可以进入正常调度。
  3. 可疑:出现高权重故障或短窗口失败率上升,降低分配权重。
  4. 冷却:连续失败达到阈值,在指定目标或全局范围内暂停业务分配。
  5. 半开探测:冷却到期后只允许少量独立探测请求,用来判断是否恢复。
  6. 恢复或剔除:探测成功则逐步恢复权重;多轮仍失败且证据指向代理自身,才退出活动池。

这与分布式系统中的 circuit breaker(熔断器)思路相近。AWS 的 Circuit Breaker 指南同样使用打开、等待和恢复探测阶段,避免持续调用已经失败的下游服务。代理池不必照搬实现,但可以借用状态转换。

冷却需要上限,也需要独立唤醒

cooldown = min(base × 2^(consecutive_failures - 1), max_cooldown) + jitter

连续失败后逐步延长冷却,但始终设置最大值;随机抖动避免大量 worker 在同一秒恢复同一批节点。AWS 关于重试控制的建议也强调指数退避、随机抖动和最大重试限制,用来降低同步重试造成的请求风暴。

如果目标明确返回 Retry-After,应优先采用该等待时间,并只作用于对应目标。多个 worker 共享状态时,冷却截止时间还应幂等更新,避免每个失败事件都把截止时间无休止向后延长。

冷却到期检查不能依赖新的请求结果上报。节点进入冷却后已不再接收业务请求,如果只在 report() 中检查到期时间,它可能永远无法离开冷却。应由定时状态扫描器,或调度器在获取节点前,把到期节点转为半开并放入探测队列。

剔除也要区分范围:只在站点 A 失败时做目标隔离;跨目标和中立探测均失败时做全局冷却;经过足够样本和多轮恢复探测仍确认过期或不可达,才全局剔除。认证配置错误进入运维告警,不应清理整个 IP 段。

六、调度闭环怎样落地

评分如果只出现在监控面板里,不会改变下一次请求。每次结果必须经过分类,更新全局或目标证据,再参与候选过滤和权重计算。

请求结果经过错误分类、全局与目标健康评分更新,再进入加权调度的反馈闭环
调度器只有接收到请求结果并更新评分,轮换才会形成闭环。

一次正常调度可以按以下顺序执行:

  1. 刷新已经到期的冷却状态,并将节点放入独立探测队列;
  2. 检查粘性会话绑定,失效时返回“需要重建会话”,不静默换 IP;
  3. 排除全局剔除、目标冷却、过期和达到并发上限的节点;
  4. 按全局分数、目标分数和当前并发计算权重;
  5. 在合格节点中加权随机,并以原子方式获取带过期时间的并发租约;
  6. 请求完成或抛出异常时都释放租约;
  7. 分类结果,识别目标级群体异常,再决定是否更新节点分数和状态。

候选和半开节点的探索默认不使用正式交付流量,而应进入独立探测队列。只有幂等、可重试、不会污染正式数据且业务明确接受失败的 GET 任务,才可以配置受控业务探索;探索比例属于系统参数,不存在通用的固定百分比。

可落地的最小实现

TARGET_WIDE_PROTECTED_ERRORS = {
    "content_invalid",
    "wrong_page",
    "parser_error",
    "target_5xx",
    "unexpected_layout",
}


def acquire(target_id, session_id=None, now=None):
    refresh_expired_cooldowns(now)  # 到期节点转为 half_open,并加入探测队列

    if session_id and has_bound_proxy(session_id):
        proxy = get_bound_proxy(session_id)
        if proxy_is_usable(proxy, target_id):
            return atomic_acquire_lease(proxy, ttl=LEASE_TTL)
        raise SessionRestartRequired(session_id)

    candidates = [
        p for p in active_proxies(target_id)
        if p.global_state != "ejected"
        and not p.is_cooling_for(target_id)
        and p.inflight < p.max_inflight
    ]

    if not candidates:
        raise NoHealthyProxyAvailable(target_id)

    for p in candidates:
        p.weight = (
            p.global_score
            * p.target_score[target_id]
            / (1 + p.inflight)
        )

    return atomic_weighted_acquire(candidates, lease_ttl=LEASE_TTL)


def execute_task(task, target_id, session_id=None):
    lease = acquire(target_id, session_id)
    try:
        outcome = request_with_proxy(task, lease.proxy)
    except NETWORK_ERRORS as exc:
        outcome = outcome_from_exception(exc)
        report(lease.proxy, target_id, outcome)
        raise
    else:
        report(lease.proxy, target_id, outcome)
        return outcome
    finally:
        release_lease(lease)


def report(proxy, target_id, outcome):
    error_class = classify(outcome)

    if (
        error_class in TARGET_WIDE_PROTECTED_ERRORS
        and target_wide_failure_spike(target_id, error_class)
    ):
        open_target_incident(target_id, error_class)
        freeze_target_penalties(target_id, error_class)
        return

    if affects_global_health(error_class):
        update_global_score(proxy, error_class, outcome)

    if affects_target_health(error_class):
        update_target_score(proxy, target_id, error_class, outcome)

    if should_cooldown(proxy, target_id, error_class):
        enter_cooldown(
            proxy,
            target_id,
            exponential_backoff_with_jitter()
        )


def refresh_expired_cooldowns(now):
    for proxy, target_id, cooldown_version in cooling_entries_expired_before(now):
        changed = atomic_transition(
            proxy,
            target_id,
            expected_state="cooling",
            expected_version=cooldown_version,
            new_state="half_open"
        )

        if changed:
            probe_key = make_probe_key(
                proxy.id,
                target_id,
                cooldown_version
            )
            schedule_probe_once(
                proxy,
                target_id,
                idempotency_key=probe_key
            )

cooling 转换到 half_open 同样必须使用原子比较并交换或分布式锁。只有成功完成状态转换的 worker 才能创建探测任务;探测任务应使用 proxy_id + target_id + cooldown_version 作为幂等键,避免高并发扫描产生重复探测。

atomic_weighted_acquire 必须在同一原子操作中选中节点并增加并发租约,避免多个 worker 同时看到旧的 inflight 后挤到同一节点。租约要设置过期时间,以便 worker 崩溃后自动释放容量。

还要避免“请求函数内部换 IP,业务层外部再重试”的双重循环。应使用统一的 retry budget,并以目标任务为单位记录总尝试次数。

七、上线指标与服务边界

最终成功率可能是靠大量重试堆出来的,因此不能单独用来验收。测试前要先定义“目标单元”:它可以是一个详情页、关键词任务、门店任务、分页任务或一次完整会话,但同一份报表不能混用不同单位。

指标 计算或观察方式 能回答的问题
首次有效成功率 第一次尝试即通过业务校验的目标单元数 ÷ 目标单元总数 调度器是否优先选到了合适节点
最终有效成功率 在统一重试预算内最终完成的目标单元数 ÷ 目标单元总数 故障恢复能力是否足够
重试恢复率 重试后恢复成功的目标单元数 ÷ 首次失败且进入重试的目标单元数 重试是否真的有效
每个完成目标平均请求次数 总请求次数 ÷ 最终完成的目标单元数 成功率背后付出了多少流量和尝试次数
无效请求占比 重复命中已知异常节点或得到无效内容的请求数 ÷ 总请求数 健康状态是否及时影响调度
半开恢复率 半开探测后恢复健康的节点数 ÷ 半开节点数 冷却时间和恢复条件是否合理
剔除后恢复率 被剔除后在后续独立复测中恢复的节点数 ÷ 被复测的剔除节点数 节点故障是否经常具有可恢复性;它不等于当时被误剔除
P95 延迟与超时分布 明确统计首次请求耗时或包含重试的端到端耗时,并在各方案中保持一致 慢点集中在少数节点还是整个目标

上线前可以做对照实验:保持目标、时间窗口、请求规则和业务校验一致,一组使用轮询或随机,一组启用健康评分、冷却和加权调度。同时比较请求次数、重试数、流量、P95 延迟和每个完成目标的平均请求次数,才能判断改善是否来自调度策略。

自建代理池与统一动态网关的职责不同

前文的节点级评分、冷却和剔除,主要适用于原始 IP 列表、自建静态代理池,或能够识别并再次选择同一出口节点的系统。使用统一动态住宅网关时,用户通常控制的是国家或城市、Session ID、网关端点和轮换模式,未必拥有单个出口 IP 的完整调度权。

此时,应用侧可以把 proxy_id 替换为可控制的 session_id、网关路由或地区池标识。节点级健康和底层剔除主要由服务商完成,应用侧重点维护目标级有效率、会话状态、内容校验与总重试预算。

对于需要按地区访问公开页面、并希望减少自建节点池维护的团队,可以用 IPWeb 动态住宅代理进行小规模对照测试。验收时不要只记录“代理是否连通”,还应比较首次有效成功率、每个完成目标的平均请求次数和目标级内容有效性。

八、上线检查与常见问题

上线前检查清单

  • 是否分别维护全局健康和目标健康,而不是一个通用分数?
  • 是否区分 DNS 解析位置、连接失败、407、超时、429、403、5xx 来源和内容无效?
  • 目标整体异常时,是否会暂停对单个代理的惩罚?
  • 冷却到期是否由定时扫描或调度器唤醒,而不是依赖新的 report()
  • 半开节点是否进入独立探测队列,而不是直接恢复正式流量?
  • 粘性会话节点失效时,是否由业务层显式重建会话?
  • 并发是否采用原子租约,并有异常退出后的过期释放机制?
  • 是否统一了目标单元、重试预算和 P95 延迟口径?
  • 是否保存规则版本和分数变更原因,支持回放误判?

常见问题

代理连续失败几次就应该剔除?

没有适用于所有目标的固定次数。应同时看失败类型、样本量、时间窗口、中立探测和其他目标表现。连续的代理链路错误可以快速进入冷却;永久剔除需要更充分的跨目标或多轮探测证据。

冷却时间应该设置多久?

没有统一固定值。应结合失败类型、Retry-After、连续失败次数和历史恢复时间,并设置最大冷却上限。目标限流与代理链路故障也不应共用完全相同的冷却规则。

403 是否说明这个 IP 已经不能用了?

不一定。403 通常只能证明该请求在当前目标和当前条件下被拒绝。先把它记入目标健康,再检查相同 IP 对其他目标和中立探测是否正常。

代理健康评分多久更新一次?

目标级证据可以在每次请求结果返回后增量更新;P95、波动率等聚合指标可按时间窗口异步计算。高并发系统不应在每次请求后执行昂贵的全量统计。

是否应该让低分节点承担正式请求?

默认不应该。候选、低分和半开节点应进入独立探测队列。只有幂等、可重试、不涉及登录或写操作,并且失败不会污染正式数据的任务,才适合开启受控业务探索。

服务商自动轮换后,为什么仍然会失败?

服务商可以管理底层节点可用性和轮换,但无法完全知道某个页面、字段、地区或业务任务是否有效。应用侧仍需维护目标级内容校验、会话约束和重试预算。

结论:把“换一个 IP”升级为可验证的调度闭环

代理轮换解决的是出口变化,健康管理解决的是出口选择。有效的闭环应该是:请求产生结果,结果被准确分类;群体异常先被拦截,节点级证据再更新全局和目标健康;评分影响下一次调度;失败节点进入有上限的冷却,并由独立探测决定恢复或剔除。

如果现有系统只有轮询列表和一个总成功计数器,可以先做三件事:拆分可行动的失败类别;为每个可控的“出口或会话 + 目标”建立独立状态;让连续失败对象短期冷却并经过受控探测再返回。完成这三步,轮换才从“换出口”变成真正减少无效请求的调度策略。