分布式爬虫的代理IP调度:Scrapy-Redis集群实战
本篇讲的是Scrapy-Redis集群场景下代理IP怎么调度。多数技术团队一上来就写中间件,用random.choice从列表里随机取IP,跑几天就发现成功率往下掉。我们青果网络长期服务网站采集器、舆情监测这类分布式采集业务,在实际项目里反复看到同一个规律:调度代码写得再精巧,后端IP池的更新节奏跟不上采集任务的消耗速度,成功率照样崩。
接下来我们就沿”池供给节奏×采集负载模式”,把调度从代码层拉回到架构层。
代理IP调度的瓶颈真的在中间件代码吗?不在。绝大多数分布式采集项目的IP调度问题,根因出在三个层面的错配上,中间件代码只是最表层的执行器。
错配层面
典型表现
真实根因
供给节奏错配
集群并发200线程,IP池每分钟只更新50个
IP存活周期与提取频率不匹配采集消耗速度
质量判断缺失
新取的IP有10%-15%首次请求就失败
未做可用性预检,把”提取到”等同于”可用”
业务隔离缺失
A任务的高频请求导致B任务共用的IP触发频次门槛
多任务共用同一个IP池,互相污染
技术团队常见的误判是:成功率下降→改中间件逻辑→加重试→加随机延迟→成功率短暂回升→三天后又掉。这个循环的本质是在执行层打补丁,没有解决供给层的问题。
分布式采集的IP调度,正确的优先级是:先确认IP池的供给能力能不能撑住集群的并发消耗,再设计调度策略,最后才写中间件代码。
Scrapy-Redis集群的代理IP调度架构长什么样?Scrapy-Redis本身解决的是”多个Spider共享一个请求队列”的问题,代理IP调度不在它的默认能力范围内。需要在Scrapy-Redis之上叠加一层IP调度服务,整体架构分三层:
第一层:IP池供给层
负责从代理IP服务商的API持续提取IP,按存活周期管理IP的生命状态。这一层的核心指标是”单位时间内可用IP的净增量”,不是”池里总共有多少IP”。
以短效代理为例:IP存活时间1分钟,单次提取上限200个(来源:青果网络官网)。如果集群每分钟消耗150个IP,那提取频率至少要保证每分钟补充≥150个可用IP,扣除提取后预检失败的部分。
第二层:调度策略层
负责决定”哪个Spider的哪次请求用哪个IP”。这一层是调度的核心,下一节展开。
第三层:执行层(Scrapy中间件)
负责把策略层分配的IP写进请求的proxy字段。这一层的代码量最少,逻辑最简单,不应该承载调度决策。
三层之间通过Redis通信。IP池供给层把可用IP写入Redis的有序集合(Sorted Set),以过期时间戳为score;调度策略层从集合里按规则取IP,标记为”占用”;执行层从调度队列里读取已分配的IP。
┌─────────────┐ ┌──────────────┐ ┌──────────────┐
│ IP池供给层 │────▶│ 调度策略层 │────▶│ Scrapy中间件 │
│ (提取+预检) │ │ (分配+回收) │ │ (注入proxy) │
└──────┬──────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
└───────────────────┴─────────────────────┘
Redis (Sorted Set)
调度策略怎么设计才能匹配采集任务的实际负载?调度策略不是”随机选一个IP”那么简单。要根据采集任务的负载特征选择不同的调度模式:
模式一:消耗型调度(适合高频短任务)
每个请求从池里取一个IP,用完即弃,不复用。适合商品列表批量抓取、搜索结果页采集这类”请求之间无状态关联”的任务。
核心参数:
参数
建议值
说明
IP复用次数
1次
用完放入”冷却队列”,不再分配
提取频率
并发数÷IP存活时间×1.3
1.3是预检失败的冗余系数
冷却时间
≥IP存活周期
防止同一IP在存活期内被二次使用
这种模式下,短效代理按量计费0.0027元/IP(来源:青果网络官网),10万次请求的IP成本约270元。成本可控的前提是提取频率与消耗速度匹配,不出现”池枯竭→排队等待→超时失败”的连锁反应。
模式二:会话型调度(适合多步骤任务)
同一个采集会话内的多次请求绑定同一个IP,会话结束后释放。适合需要登录态保持、翻页连续性的场景。
核心参数:
参数
建议值
说明
会话绑定键
目标站点+任务ID
同一会话内所有请求走同一IP
最大绑定时长
≤IP存活周期的80%
留20%余量做会话迁移
故障切换
检测到连续3次失败后换IP
换IP同时标记旧IP为不可用
会话型调度对IP存活时间的要求更高。如果一个采集会话需要持续5分钟,1分钟存活的短效IP就不够用,需要考虑独享代理(存活0-1440分钟可调)或隧道代理(来源:青果网络官网)。
模式三:分池调度(适合多业务并行)
不同的采集任务使用不同的IP子池,互不污染。这是分布式采集最容易忽略、也最容易出问题的环节。
典型场景:团队同时跑舆情监测(7×24不间断)和网站采集器(白天集中采集)两类任务,共用一个IP池。白天采集高峰期,网站采集器的高频请求把共用池里的IP大量消耗,舆情监测的可用IP骤降,成功率跟着掉。
解法是在Redis里按业务维度建不同的key前缀,每个业务从自己的子池里取IP。子池的供给配额按业务优先级分配。
我们青果网络的业务分池技术在架构层面做了这件事:不同业务类型的请求走不同的后端池,池与池之间的IP不交叉,一个池触发目标站点的频次门槛不会传染到其他池(来源:青果网络官网)。在Scrapy-Redis集群里复刻这个思路,就是在调度策略层做子池隔离。
代码层面怎么接入代理IP池?调度架构和策略确定后,代码层面的接入反而是最简单的部分。以下是Scrapy-Redis集群接入代理IP的核心代码结构:
IP池供给服务(独立进程,非Scrapy组件)
import redis
import time
import requests
class ProxySupplier:
def __init__(self, redis_client, api_url, pool_key):
self.r = redis_client
self.api_url = api_url
self.pool_key = pool_key
def fetch_and_load(self):
"""从代理API提取IP,预检后写入Redis"""
resp = requests.get(self.api_url)
proxies = resp.text.strip().split('\n')
now = time.time()
pipe = self.r.pipeline()
for proxy in proxies:
# 预检:实际发一个HEAD请求验证可用性
if self._health_check(proxy):
expire_at = now + 55 # 存活60秒,留5秒余量
pipe.zadd(self.pool_key, {proxy: expire_at})
pipe.execute()
def cleanup_expired(self):
"""清理已过期的IP"""
self.r.zremrangebyscore(
self.pool_key, '-inf', time.time()
)
Scrapy下载中间件(执行层)
import redis
import time
class RedisProxyMiddleware:
def __init__(self, redis_client, pool_key):
self.r = redis_client
self.pool_key = pool_key
@classmethod
def from_crawler(cls, crawler):
settings = crawler.settings
r = redis.Redis(
host=settings.get('REDIS_HOST'),
port=settings.get('REDIS_PORT')
)
return cls(r, settings.get('PROXY_POOL_KEY'))
def process_request(self, request, spider):
now = time.time()
# 取score大于当前时间的IP(未过期)
proxies = self.r.zrangebyscore(
self.pool_key, now, '+inf', start=0, num=1
)
if proxies:
proxy = proxies[0].decode()
request.meta['proxy'] = f'http://{proxy}'
else:
spider.logger.warning('代理池暂无可用IP,等待补充')
关键实现细节:
细节
说明
IP预检
供给层提取后先发HEAD请求验证,不把”提取成功”等同于”可用”
过期清理
用Redis Sorted Set的score存过期时间,定时清理score