Scrapy-Redis集群中的代理调度链路是什么?
标准链路是任务入队、节点消费、请求取代理、结果回收和失败重试。
Scrapy-Redis能够让多个爬虫节点共享待处理请求,但不会自动管理代理IP。一次请求通常经过以下步骤:
- 任务生产端将URL和请求参数写入Redis。
- 爬虫节点从共享队列中领取请求。
- 下载器中间件从代理池领取一个可用代理。
- 请求携带代理发送到目标地址。
- 中间件根据响应或异常更新代理状态。
- 成功请求归还代理,代理异常则进入冷却队列。
- 超过重试次数的请求进入失败队列。
因此,代理池应该作为独立资源层接入,而不是把代理地址直接写入每个请求的回调函数。
settings.py应该配置哪些内容?
基础配置要先保证任务队列可共享,再接入代理中间件和重试机制。
# settings.py
REDIS_URL = "redis://127.0.0.1:6379/0"
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
SCHEDULER_PERSIST = True
RETRY_ENABLED = True
RETRY_TIMES = 3
RETRY_HTTP_CODES = [
408,
429,
500,
502,
503,
504,
]
CONCURRENT_REQUESTS = 32
CONCURRENT_REQUESTS_PER_DOMAIN = 8
DOWNLOAD_TIMEOUT = 20
DOWNLOADER_MIDDLEWARES = {
"project.middlewares.DistributedProxyMiddleware": 550,
"scrapy.downloadermiddlewares.retry.RetryMiddleware": 550,
}生产环境不要把代理账号、密码和固定地址直接提交到代码仓库,可以通过环境变量注入:
import os
REDIS_URL = os.getenv(
"REDIS_URL",
"redis://127.0.0.1:6379/0",
)
PROXY_API_URL = os.getenv("PROXY_API_URL")
PROXY_USERNAME = os.getenv("PROXY_USERNAME")
PROXY_PASSWORD = os.getenv("PROXY_PASSWORD")需要注意的是,不同Scrapy版本的中间件执行顺序可能存在差异。部署前应通过测试任务确认代理注入、异常捕获和重试逻辑是否按照预期执行。
Redis中应该怎样设计代理池键?
代理池至少要区分可用、租约、冷却和统计四类状态。
| Redis键 | 类型 | 用途 |
|---|---|---|
proxy:ready | List | 等待分配的代理 |
proxy:leased | List | 已被节点领取的代理 |
proxy:cooldown | Sorted Set | 按恢复时间保存异常代理 |
proxy:dead | Set | 暂停使用的代理 |
proxy:stats:<id> | Hash | 保存代理成功和失败统计 |
proxy:heartbeat:<node> | String | 记录节点心跳 |
proxy:request:<id> | Hash | 记录请求与代理的关系 |
代理进入冷却队列时,可以将预计恢复时间作为分数:
import time
cooldown_until = int(time.time()) + 60
redis_client.zadd(
"proxy:cooldown",
{proxy: cooldown_until},
)恢复任务周期性取出已经到期的代理:
def restore_cooldown_proxies(redis_client):
now = int(time.time())
proxies = redis_client.zrangebyscore(
"proxy:cooldown",
0,
now,
)
for proxy in proxies:
redis_client.zrem("proxy:cooldown", proxy)
redis_client.rpush("proxy:ready", proxy)生产环境应使用Lua脚本或其他原子操作,将查询、删除和回收合并处理,避免多个恢复进程同时取出同一条代理记录。
代理中间件应该怎样编写?
下载器中间件负责请求前领取代理,响应后归还代理,复杂的评分逻辑则交给独立的状态处理函数。
下面是一个简化的中间件示例:
import time
import redis
from scrapy.exceptions import IgnoreRequest
class DistributedProxyMiddleware:
def __init__(self, redis_url):
self.redis = redis.Redis.from_url(
redis_url,
decode_responses=True,
)
@classmethod
def from_crawler(cls, crawler):
return cls(
crawler.settings.get("REDIS_URL")
)
def acquire_proxy(self):
# 原子地将代理从可用队列移动到租约队列
return self.redis.rpoplpush(
"proxy:ready",
"proxy:leased",
)
def release_proxy(self, proxy, healthy=True):
if not proxy:
return
self.redis.lrem(
"proxy:leased",
1,
proxy,
)
if healthy:
self.redis.rpush(
"proxy:ready",
proxy,
)
return
cooldown_until = int(time.time()) + 60
self.redis.zadd(
"proxy:cooldown",
{proxy: cooldown_until},
)
def process_request(self, request, spider):
if request.meta.get("proxy"):
return None
proxy = self.acquire_proxy()
if not proxy:
request.meta["proxy_waiting"] = True
raise IgnoreRequest(
"proxy pool is temporarily empty"
)
request.meta["proxy"] = proxy
request.meta["proxy_lease"] = proxy
return None
def process_response(self, request, response, spider):
proxy = request.meta.get("proxy_lease")
if response.status in (200, 206):
self.release_proxy(proxy, healthy=True)
elif response.status in (407, 502, 503):
self.release_proxy(proxy, healthy=False)
elif response.status in (403, 429):
# 目标限制不一定代表代理失效
self.release_proxy(proxy, healthy=True)
else:
self.release_proxy(proxy, healthy=True)
return response
def process_exception(self, request, exception, spider):
proxy = request.meta.get("proxy_lease")
self.release_proxy(proxy, healthy=False)
return None这段代码用于展示接入结构。生产环境还需要补充以下逻辑:
- 代理池为空时,将请求延迟后重新入队,不能直接丢失。
- 代理领取、租约写入和过期时间设置应保持原子性。
- 节点退出后,租约中的代理要能够自动回收。
- 代理账号和密码不能通过日志输出。
- Redis同步操作不能长时间阻塞Scrapy事件循环。
如果代理池规模较大,可以把代理调度单独拆成服务,由爬虫节点通过接口领取和归还代理,避免每个Scrapy进程直接承担复杂的状态管理。
请求重试怎样避免无限循环?
重试次数要同时记录请求次数、代理次数和任务总时长。
只配置RETRY_TIMES还不够。一次请求可能不断切换代理,如果没有额外限制,任务仍可能长时间循环。
可以在请求的meta中记录重试状态:
def build_retry_request(request, reason):
retry_count = request.meta.get(
"proxy_retry_count",
0,
)
if retry_count >= 3:
return None
retry_request = request.copy()
retry_request.dont_filter = True
retry_request.meta[
"proxy_retry_count"
] = retry_count + 1
retry_request.meta[
"retry_reason"
] = reason
retry_request.meta.pop("proxy", None)
retry_request.meta.pop("proxy_lease", None)
return retry_request不同错误应采用不同策略:
| 错误类型 | 是否更换代理 | 处理建议 |
|---|---|---|
| 连接超时 | 是 | 代理进入短暂冷却 |
| 代理认证失败 | 是 | 检查凭证,必要时暂停代理 |
| 目标返回429 | 视情况 | 降低请求频率并延迟重试 |
| 目标返回403 | 视情况 | 检查请求参数和访问节奏 |
| 页面内容为空 | 否 | 优先检查解析逻辑 |
| 字段缺失 | 否 | 检查页面结构和选择器 |
| DNS解析失败 | 通常是 | 检查代理和节点网络 |
页面内容为空或字段缺失时,直接切换代理通常不能解决问题,反而会增加无效请求和代理消耗。
多个爬虫节点怎样共享代理质量评分?
代理评分必须写入Redis,不能只保存在单个节点的内存中。
每个代理可以维护以下字段:
success_count
timeout_count
auth_error_count
target_error_count
last_success_at
last_failure_at
cooldown_until评分可以采用时间窗口,避免历史数据掩盖当前状态:
近期调度分数 =
最近10分钟成功率 × 0.6
+ 最近1小时成功率 × 0.3
+ 历史成功率 × 0.1这个分数只用于调度,不等同于最终业务成功率。真正的采集质量还需要结合有效数据率、页面完整度和重复内容比例。
当单个代理连续失败时,可以延长冷却时间;当多个代理同时出现相同错误时,应优先检查任务参数、目标响应和节点网络,不能一次性把整个代理池全部判定为失效。
集群部署时怎样验证代理配置?
先用小规模任务验证状态闭环,再逐步增加节点和并发。
建议按以下顺序测试:
- 启动两个爬虫节点,确认它们消费同一个Redis任务队列。
- 为每个节点设置不同的
NODE_ID。 - 检查同一个请求是否只被一个节点处理。
- 检查代理是否从
ready进入leased。 - 验证请求结束后代理能否正确归还。
- 注入一个失效代理,确认它进入冷却队列。
- 关闭一个节点,确认租约过期后代理能够回收。
- 模拟429响应,确认任务会延迟重试而不是快速循环。
- 让代理池暂时为空,确认请求会进入等待或延迟队列。
- 提高并发后观察Redis队列、代理库存和业务成功率。
日志至少应包含以下字段:
request_id
node_id
target_host
proxy_id
retry_count
response_status
elapsed_ms
result_status只记录URL和状态码,后续很难判断问题来自代理、节点、目标站点还是解析器。
哪些指标能判断代理调度是否正常?
代理连接指标只能说明链路是否可用,业务有效数据指标才反映采集结果。
建议同时监控三类指标:
| 指标类别 | 关注指标 | 判断目的 |
|---|---|---|
| 任务指标 | 队列长度、消费速度、重试次数 | 判断任务是否积压 |
| 代理指标 | 可用数、租约数、冷却数、回收数 | 判断代理池是否枯竭 |
| 连接指标 | 超时率、认证失败率、平均响应时间 | 判断网络和代理质量 |
| 业务指标 | 有效页面率、字段提取率、空响应率 | 判断采集是否有效 |
| 集群指标 | 节点数量、单节点吞吐、重启次数 | 判断节点是否均衡 |
| Redis指标 | 内存、连接数、慢查询、阻塞命令 | 判断中心组件是否成为瓶颈 |
例如,代理连接成功率较高,但有效字段提取率明显下降,问题可能来自页面结构变化或解析器失效,而不是代理池。反过来,如果多个目标同时出现连接超时,则应优先检查代理库存和网络链路。
总结
代理IP池接入Scrapy-Redis时,应把任务队列和代理状态分开设计,通过Redis键空间、下载器中间件、租约机制、失败冷却和重试上限完成统一调度,再用任务、代理、连接和业务指标验证集群是否真正稳定。
FAQ
Q:代理IP池应该由每个爬虫节点单独维护吗?
不建议完全独立维护。每个节点各自保存代理列表,容易出现代理重复使用、失败状态不同步和资源利用率不均。更合理的方式是由Redis或独立调度服务保存共享状态,爬虫节点只负责领取和归还。
Q:代理池没有可用IP时,应该直接丢弃请求吗?
不应该。可以把请求放入延迟队列,等待代理恢复后重新执行,并设置最大等待时间和最大重试次数。超过限制后再写入失败队列,方便后续检查。
Q:一个代理应该承担多少并发请求?
没有固定值,需要结合代理类型、目标响应速度、请求体大小和访问频率进行测试。建议从较低并发开始,观察超时率、429比例和有效数据率,再逐步调整。
Q:目标返回403或429时,是否一定要更换代理?
不一定。403或429可能与请求频率、请求参数、会话状态和目标访问规则有关。应先区分代理连接失败和目标限制,再决定是降速、延迟重试还是短暂更换代理。
Q:节点异常退出后,代理会不会一直处于占用状态?
如果没有租约过期和回收机制,确实可能一直处于占用状态。因此每次领取代理都应写入节点ID、请求ID和过期时间,并通过心跳或定时任务回收异常租约。