代理池里明明有很多 IP,请求也在不断轮换,成功率却没有明显提高:超时之后换一个,仍然超时;刚返回 403 的节点几秒后又被另一台 worker 选中;某个 IP 访问站点 A 已经失效,却因为访问站点 B 正常而继续被分配给 A。此时继续增加轮换频率,往往只是在更快地重复失败。
问题不在“有没有轮换”,而在轮换之前是否完成了三件事:判断节点对当前目标是否健康、让短期失效节点进入冷却、把有充分证据的失败节点移出可调度集合。轮换只是选择下一个出口的动作,健康管理才决定下一次应该选谁。
一个开源代理调度工具的作者曾公开报告一组七日测试:在其自有的 549,114 次请求环境中,按近期表现选择代理的成功率高于轮询与随机选择。这个数字来自工具作者自己的负载和实现,不能视为行业平均值或服务承诺,但它揭示的机制值得验证:轮询和随机策略会持续把请求分给已经失效的节点,除非系统把请求结果反馈给调度器。
文章目录
一、为什么频繁轮换仍然失败
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_scorefinal_weight = global_score × target_score / (1 + inflight)
权重只是启动测试的示例,不是通用最优解。实际系统还要设置最低样本量:刚成功一次的新节点不能立刻获得与长期稳定节点相同的权重。每次更新应保存旧分数、新分数、触发事件和规则版本,以便回放误判。
五、冷却、半开探测与节点剔除
不要只给 IP 保存永久的 available=true/false。网络状态会变化,目标限制也可能在一段时间后解除。更稳妥的做法,是让节点在有限状态之间转换。
- 候选:新加入或长期未使用的节点,只进入独立探测队列。
- 健康:样本量和近期有效率达到阈值,可以进入正常调度。
- 可疑:出现高权重故障或短窗口失败率上升,降低分配权重。
- 冷却:连续失败达到阈值,在指定目标或全局范围内暂停业务分配。
- 半开探测:冷却到期后只允许少量独立探测请求,用来判断是否恢复。
- 恢复或剔除:探测成功则逐步恢复权重;多轮仍失败且证据指向代理自身,才退出活动池。
这与分布式系统中的 circuit breaker(熔断器)思路相近。AWS 的 Circuit Breaker 指南同样使用打开、等待和恢复探测阶段,避免持续调用已经失败的下游服务。代理池不必照搬实现,但可以借用状态转换。
冷却需要上限,也需要独立唤醒
cooldown = min(base × 2^(consecutive_failures - 1), max_cooldown) + jitter
连续失败后逐步延长冷却,但始终设置最大值;随机抖动避免大量 worker 在同一秒恢复同一批节点。AWS 关于重试控制的建议也强调指数退避、随机抖动和最大重试限制,用来降低同步重试造成的请求风暴。
如果目标明确返回 Retry-After,应优先采用该等待时间,并只作用于对应目标。多个 worker 共享状态时,冷却截止时间还应幂等更新,避免每个失败事件都把截止时间无休止向后延长。
冷却到期检查不能依赖新的请求结果上报。节点进入冷却后已不再接收业务请求,如果只在 report() 中检查到期时间,它可能永远无法离开冷却。应由定时状态扫描器,或调度器在获取节点前,把到期节点转为半开并放入探测队列。
剔除也要区分范围:只在站点 A 失败时做目标隔离;跨目标和中立探测均失败时做全局冷却;经过足够样本和多轮恢复探测仍确认过期或不可达,才全局剔除。认证配置错误进入运维告警,不应清理整个 IP 段。
六、调度闭环怎样落地
评分如果只出现在监控面板里,不会改变下一次请求。每次结果必须经过分类,更新全局或目标证据,再参与候选过滤和权重计算。
一次正常调度可以按以下顺序执行:
- 刷新已经到期的冷却状态,并将节点放入独立探测队列;
- 检查粘性会话绑定,失效时返回“需要重建会话”,不静默换 IP;
- 排除全局剔除、目标冷却、过期和达到并发上限的节点;
- 按全局分数、目标分数和当前并发计算权重;
- 在合格节点中加权随机,并以原子方式获取带过期时间的并发租约;
- 请求完成或抛出异常时都释放租约;
- 分类结果,识别目标级群体异常,再决定是否更新节点分数和状态。
候选和半开节点的探索默认不使用正式交付流量,而应进入独立探测队列。只有幂等、可重试、不会污染正式数据且业务明确接受失败的 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”升级为可验证的调度闭环
代理轮换解决的是出口变化,健康管理解决的是出口选择。有效的闭环应该是:请求产生结果,结果被准确分类;群体异常先被拦截,节点级证据再更新全局和目标健康;评分影响下一次调度;失败节点进入有上限的冷却,并由独立探测决定恢复或剔除。
如果现有系统只有轮询列表和一个总成功计数器,可以先做三件事:拆分可行动的失败类别;为每个可控的“出口或会话 + 目标”建立独立状态;让连续失败对象短期冷却并经过受控探测再返回。完成这三步,轮换才从“换出口”变成真正减少无效请求的调度策略。