代理池健康度监控:怎么判断一条线路该不该踢出

采集跑久了会发现一个问题:池子里明明有几十条线路,实际能用的只有那么几条,剩下的要么一直失败,要么半死不活地拖着整体成功率。

这篇讲怎么给代理池加一层健康度监控,把劣质线路自动识别出来并隔离。

为什么不能只看成功率

最直觉的做法是统计每条 IP 的失败次数,超过阈值就踢。这个做法有两个问题。

第一,样本量不够时不准。 一条线路被用了 3 次,失败 1 次,成功率 67%,看起来很差。但 3 次样本说明不了任何事,可能只是赶上目标站抖了一下。

第二,不同类型的失败要区别对待。 403 是目标站拒绝,可能是这条 IP 被拉黑了;429 是频率超限,换 IP 也没用,该降速;超时可能是线路本身慢,也可能是目标站响应慢。

所以健康度至少要看三个维度:成功率、延迟分布、失败类型。

数据结构设计

每条线路的记录不能只存一个计数器,要存足够支撑判断的信息。

import time
from collections import deque

class ProxyStat:
    """单条线路的运行统计"""
    __slots__ = ("addr", "window", "fail_streak", "cool_until",
                 "total", "ok", "latency_sum")

    def __init__(self, addr, window=50):
        self.addr = addr
        self.window = deque(maxlen=window)   # 最近 N 次结果,True/False
        self.fail_streak = 0                 # 连续失败次数
        self.cool_until = 0.0                # 冷却到期时间戳
        self.total = 0
        self.ok = 0
        self.latency_sum = 0.0

    def record(self, success, latency=0.0, cooldown=0):
        self.total += 1
        self.window.append(success)
        if success:
            self.ok += 1
            self.fail_streak = 0
            self.latency_sum += latency
        else:
            self.fail_streak += 1
            if cooldown:
                self.cool_until = time.time() + cooldown

    @property
    def recent_rate(self):
        """滑动窗口成功率,比全局成功率更能反映当前状态"""
        if not self.window:
            return 1.0
        return sum(self.window) / len(self.window)

    @property
    def avg_latency(self):
        return self.latency_sum / self.ok if self.ok else 0.0

用 deque(maxlen=N) 做滑动窗口是关键。全局成功率会被历史数据稀释——一条线路前 100 次都成功,最近 20 次全失败,全局成功率还是 83%,但实际已经不能用了。滑动窗口能反映"最近怎么样"。

冷却而不是直接删除

发现一条线路有问题,不要立刻从池子里删掉。网络抖动、目标站临时限流都是会恢复的。

正确的做法是给冷却期,到期后自动放回池子重新测试。

class ProxyPool:
    def __init__(self, addrs):
        self.stats = {a: ProxyStat(a) for a in addrs}

    def acquire(self):
        """取一条可用线路"""
        now = time.time()
        alive = [s for s in self.stats.values() if s.cool_until <= now]
        if not alive:
            return None
        # 优先选近期成功率高的
        alive.sort(key=lambda s: s.recent_rate, reverse=True)
        # 在前几名里随机,避免总是用同一条
        import random
        return random.choice(alive[:max(1, len(alive) // 3)])

    def report(self, addr, success, latency=0.0):
        st = self.stats.get(addr)
        if not st:
            return
        # 连续失败 3 次,冷却 60 秒
        cooldown = 60 if (not success and st.fail_streak >= 2) else 0
        st.record(success, latency, cooldown)
        # 冷却期随失败次数指数增长,最长 30 分钟
        if cooldown:
            st.cool_until = time.time() + min(60 * (2 ** st.fail_streak), 1800)

冷却时长用指数退避:连续失败越多,冷却越久。这样避免"失败→冷却 60 秒→恢复→又失败"的循环浪费。

失败类型要分开处理

这一步很多人忽略,但对成功率影响很大。

import requests

def classify(exc=None, status=None):
    """把失败归类,决定要不要换 IP"""
    if status == 403:
        return "blocked"      # 目标站拒绝,换 IP 可能有用
    if status == 429:
        return "rate_limit"   # 频率超限,换 IP 没用,该降速
    if status and 500 <= status < 600:
        return "server_error" # 目标站问题,换 IP 没用,该重试等
    if isinstance(exc, requests.exceptions.Timeout):
        return "timeout"      # 线路慢或目标站慢,可以先降速试试
    if isinstance(exc, requests.exceptions.ProxyError):
        return "proxy_dead"   # 线路本身挂了,直接冷却
    return "unknown"


def should_switch_proxy(kind):
    """只有这几种情况值得换出口"""
    return kind in ("blocked", "proxy_dead")

关键判断:不是所有失败都该换 IP。

  • 403 → 换,可能这条被拉黑了
  • 429 → 不换,换多少条都一样,该降低频率
  • 5xx → 不换,目标站自己的问题
  • 超时 → 先不换,可能是并发太高
  • ProxyError → 换,线路死了

只对 blocked 和 proxy_dead 换出口,其它情况先降速重试。这一条能让成功率提升非常明显,因为大部分"失败"其实是自己把目标站打急了。

退避策略

失败后的重试间隔要随失败次数增长,而且要有随机抖动,避免多个任务同时重试形成尖峰。

import random

def backoff(attempt, base=1.0, cap=60.0, jitter=0.3):
    """指数退避 + 抖动"""
    delay = min(base * (2 ** attempt), cap)
    return delay * (1 + random.uniform(-jitter, jitter))

没有抖动的话,一批任务同时失败会同时重试,对目标站形成规律的冲击波,很容易触发风控。加了抖动之后重试时间散开,看起来更像正常流量。

怎么接入 Scrapy

在 downloader middleware 里做,不要太早也不要太晚。

from scrapy import signals

class HealthMiddleware:
    def __init__(self, pool):
        self.pool = pool

    @classmethod
    def from_crawler(cls, crawler):
        mw = cls(crawler.settings.get("PROXY_POOL"))
        crawler.signals.connect(mw.spider_closed, signal=signals.spider_closed)
        return mw

    def process_request(self, request, spider):
        if request.meta.get("no_proxy"):
            return None
        addr = self.pool.acquire()
        if not addr:
            raise Exception("代理池枯竭,全部线路都在冷却")
        request.meta["proxy"] = "http://" + addr
        request.meta["_proxy_addr"] = addr
        request.meta["_t0"] = time.time()
        return None

    def process_response(self, request, response, spider):
        addr = request.meta.get("_proxy_addr")
        if not addr:
            return response
        latency = time.time() - request.meta.get("_t0", time.time())
        kind = classify(status=response.status)
        if response.status in (200, 301, 302):
            self.pool.report(addr, True, latency)
        elif should_switch_proxy(kind):
            self.pool.report(addr, False)
        # 429 / 5xx 不惩罚线路,交给 retry 中间件
        return response

    def process_exception(self, request, exception, spider):
        addr = request.meta.get("_proxy_addr")
        if not addr:
            return None
        kind = classify(exc=exception)
        if should_switch_proxy(kind):
            self.pool.report(addr, False)
        return None

    def spider_closed(self, spider):
        # 输出健康度报告
        rows = sorted(self.pool.stats.values(), key=lambda s: s.recent_rate)
        for s in rows[:10]:
            spider.logger.info(
                "proxy=%s rate=%.2f avg=%.0fms total=%d",
                s.addr, s.recent_rate, s.avg_latency * 1000, s.total)

注意 process_response 里对 429 和 5xx 不做惩罚——这是前面说的"失败分类"的直接应用。

监控指标

跑起来之后,这几个数值得盯着:

指标 健康范围 异常说明
池子可用率 > 70% 低于这个说明线路质量差或冷却设置过激
平均成功率 > 90% 低说明目标站风控紧或并发过高
平均延迟 < 800ms 高说明线路调度有问题
冷却线路占比 < 30% 长期高说明池子需要换血

如果可用率一直很低,先别急着加线路,检查两件事:冷却时长是不是设太长了,以及是不是把 429 也当成线路问题在惩罚。

大部分"池子不够用"的问题,实际上是惩罚逻辑写错了。

最后

这套东西的价值不在于复杂,在于把"感觉这条线路不行"变成可量化的判断。

用滑动窗口看近期表现,用失败分类决定要不要换,用指数退避控制重试节奏——三件事做完,池子的有效利用率能提升一个档次。

至于线路本身从哪来,那是另一回事。我用的 58无极 的进程代理方案,按进程分流,多个采集任务可以各走各的出口,官网 58wuji.com 上有免费的出口体检工具,可以先测一下自己的环境再决定。

代码可以直接跑,ProxyStat 和 classify 这两个类跟具体供应商无关,换成任何代理服务都能用。

posted @ 2026-11-09 16:24  58无极  阅读(2)  评论(0)    收藏  举报