分布式爬虫与任务队列
前置知识
本文基于模型知识整理,建议核对官方文档(见 参考资料)。
一句话定义
分布式爬虫 = 多个 worker 节点从一个共享队列取任务;换来的是水平扩展的吞吐、多出口 IP 与故障隔离,代价是集中去重与状态管理的新复杂度。
为什么重要
单机有三个天花板:CPU/带宽、出口 IP、单点故障。分布式不是炫技而是这三件事的解;但过早分布式是采集工程最常见的过度设计——先确认 asyncio 单机(kp-019)真的不够。
前置知识
kp-021(任务状态与去重)、kp-019(异步 worker)。
核心概念
- 中央队列:Redis(list/zset,轻量首选)或 RabbitMQ/Kafka(强消息语义)。
- 共享去重:去重集合集中放 Redis(SET/BF),所有 worker 共用。
- worker 无状态化:worker 只做「取任务 → 抓取 → 回写」,自身不存状态,随时可扩可缩。
- scrapy-redis:把 Scrapy 的 Scheduler 与去重器替换为 Redis 实现的现成方案。
原理与机制
最小分布式拓扑:
┌──────────────┐
种子/新增任务 → │ Redis 队列 │ ← 共享去重集合(SET/BF)
│ LPUSH/BRPOP │
└──┬────┬─────┘
BRPOP │ │ BRPOP
┌──────┴┐ ┌┴───────┐
│worker1│ │worker2 │ (workerN 同构, 各绑代理出口, kp-020)
└──────┬┘ └┬───────┘
↓ ↓
共享存储(SQLite/PG/对象存储) → 结果侧统一清洗
worker 骨架(Redis 阻塞取任务):
import redis, requests
r = redis.Redis()
while True:
_, url = r.blpop("crawler:queue") # 阻塞式取, 无任务时挂起
if r.sismember("crawler:seen", url): # 共享去重
continue
try:
resp = requests.get(url, timeout=(5, 20))
save(resp) # 结果回写共享存储
r.sadd("crawler:seen", url)
except requests.RequestException:
r.lpush("crawler:retry", url) # 失败进重试队列, 不丢任务
容量估算经验:单 worker 峰值受限于限速(kp-017)与出口质量;扩容优先加 worker(IP 分散),队列与去重的 Redis 单实例足以支撑千万级任务。
实例或案例
全站 50 万页面存档任务:单机 asyncio 跑 26 小时且中途一次崩溃全部重来;改为「Redis 队列 + 4 个容器化 worker」后 7 小时完成,且中途 2 次 worker 崩溃只损失各自在途批。分布式带来的不只是快,更是故障半径的缩小。
常见误区
- 误区一:需求千页级就上分布式。 千页任务 asyncio + 良好限速单机分钟级完成;分布式的运维成本(Redis、部署、监控)远超收益。
- 误区二:去重留在各 worker 内存里。 多 worker 必然重复抓取且互相不知道;去重集合必须集中。
- 误区三:忽略登录态与环境一致性。 多 worker 同时用同一账号高频访问是自曝;账号轮换与 kp-035 环境问题在分布式下被放大。
自测题
- 分布式解决单机三个什么问题?
答:吞吐天花板(水平加机器)、出口 IP 单一(worker 各绑代理)、单点故障(worker 挂了任务仍在队列)。
- 为什么去重必须集中化?
答:各 worker 独立去重会互不知情地重复抓同一 URL,浪费请求并加剧风控暴露。
- 判断「是否需要分布式」的第一问?
答:单机 asyncio + 限速已把资源用满且仍不达标/不可靠吗?不是则不要上分布式。
与其他知识点的关系
kp-018 + Redis 队列 ≈ scrapy-redis;kp-020 提供 worker 出口;kp-036 是本篇的容器化/云端形态。
延伸阅读
scrapy-redis 项目 README;Redis 官方文档 list 命令族(BLPOP 语义)。