把单机Scrapy跑顺之后,你迟早会碰上“这量级单机实在顶不住”的时刻。我之前维护的一套爬虫,单机大概跑到 8~10 个并发,每天抓几十万页面时还凑合,但某天业务方需求翻了三倍,Redis 里 pending 的 URL 越堆越多,目标站点响应稍微慢一点,整个调度就开始卡顿。那阵子我花了一周把系统改成“Scrapy框架 + Redis 任务队列”的分布式架构,才彻底解决了扩展性问题。这篇博文就是那段时间的完整复盘,从方案选型、核心改造到多机部署、问题排查,全部按实操走一遍。想从单机跨到分布式的读者,可以直接照着搭。
1. 先搞清楚:分布式到底在解决单机版的哪些瓶颈
1.1 单机Scrapy跑不动时,卡点究竟在哪
先说结论:绝大多数“单机跑不动”的案例,并不仅仅是CPU或内存不够,而是调度器和去重队列被单机资源锁死了。
Scrapy 单机版的调度器是进程内的,它把待抓取 URL 放在 Python 内存队列里,去重指纹也存在内存中。这种设计的优点是轻量、速度快,缺点则很致命:
- 进程一旦重启,所有待抓取队列和去重指纹全部丢失,想断点续爬得靠自己的 extra 逻辑。
- 内存队列上限受限于单机可用内存,几十万 URL 下来,Python 对象的开销会吃掉好几个 G。
- 去重集合(用 fingerprint 集合实现)无法跨进程共享,多开几个 Scrapy 进程时会出现重复抓取,只能靠外部存储硬扛。
而当你把队列和去重集合放到 Redis 里之后,调度行为就从“单机内存态”变成了“集中式全局态”。任何一个节点把新 URL push 进 Redis,另一个节点通过 Redis 弹出来继续抓取——这就是分布式调度最简单的模型。
1.2 哪些项目真的需要分布式,哪些是杀鸡用牛刀
我见过不少人上来就要搭分布式,其实业务量小得可怜。这里给一个参考判断标准:
- 日均请求量在几万到二十万以内,单机调优完全可行,先别动分布式。
- 日均请求量超过五十万,或者目标 URL 来源分散、单机网络带宽被占满,分布式才有实际意义。
- 多个节点抓取同一站点、需要共享 URL 去重、需要断点续爬,这时候就算量不大也建议上 Redis 队列。
另外注意:分布式只能解决“横向扩容抓取能力”的问题,如果你的瓶颈其实是目标服务器响应太慢、代理 IP 质量太差、存储写入太慢,那加机器也白搭。我踩过这个坑,后面会展开说。
1.3 核心拆分思路:抓取与调度解耦
单机版 Scrapy 里,调度器、去重器、下载器都在一个进程里。分布式改造的核心思想是解耦:
- 调度与去重:下沉到 Redis,全局共享。
- 下载器与 Spider 逻辑:保留在各节点进程中。
- 数据管道:各节点抓完数据后,写入共享存储,避免重复入库。
这种拆分的最大价值是“节点无状态”。任何一个爬虫节点挂掉,其他节点继续从 Redis 队列取任务,数据不丢,任务可续。这个特性在生产环境里非常关键——单机版进程一挂基本等于任务中断,分布式改造后你甚至可以直接在凌晨加节点扩容。
2. 方案选型:为什么推荐 Scrapy-Redis 而不是自研队列
2.1 自己用 Redis 写个 FIFO 队列,最开始确实挺开心
很多人第一步会想:既然 Scrapy 的调度器不共享,那我直接用 Redis 的lpush / rpop做成 URL 队列,各节点轮流弹出 URL 去抓不就行了?这条路确实能跑通,但会迅速遇到三个问题:
- 谁来去重?如果每个节点各自用 Scrapy 的去重,同一 URL 可能被多个节点同时弹出并抓取。
- 失败的 URL 重试逻辑怎么设计?深度、次数、延迟这些策略在裸 Redis 队列里全部要重写。
- 调度策略只有 FIFO,优先级队列、领域内优先抓取这些策略实现起来越来越复杂。
我第一版就是自己拿 Redis 做了个简单队列,跑了两天就发现重复抓取率大约在 3% 左右,数据清洗成本陡增。后来换成了 Scrapy 官方维护下的scrapy-redis方案,去重、调度器、请求队列全都在框架层面解决,省心得多。
2.2 Scrapy-Redis 的本质:把调度器插拔式替换掉
scrapy-redis并不是一个独立爬虫框架,而是一套基于 Scrapy 调度器接口的扩展。它主要替换了四个组件:
| 组件 | 单机版 | Scrapy-Redis 版 |
|---|---|---|
| 调度器 Scheduler | 进程内内存队列 | Redis 中全局队列,支持多种队列策略 |
| 去重过滤器 Dupefilter | Python 内存 set | Redis 中的指纹集合 |
| 待抓取队列 Queue | 内存 | Redis List / ZSet(按优先级) |
| Spider 启动种子 | start_urls 写在代码里 | 从 Redis 读取,支持动态追加 |
配置方式很直接,在settings.py中替换两行关键配置即可。这一步是所有改造的基础。
# settings.py 核心配置 SCHEDULER = "scrapy_redis.scheduler.Scheduler" DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter" SCHEDULER_PERSIST = True REDIS_URL = "redis://127.0.0.1:6379/0"SCHEDULER_PERSIST = True意味着爬虫结束后 Redis 中的队列和去重集合不会被清空,下次启动可以接着跑。这是实现断点续爬的关键开关,生产环境建议始终打开。
2.3 为什么不用 Celery 这类通用任务队列
Celery 是通用分布式任务队列,理论上也能用来分发抓取任务。但实践下来有两个问题:
- Celery 的粒度是“任务函数”,而 Scrapy 的粒度是“请求 Request”。把 Scrapy 的 Request 包装成 Celery task 需要额外的序列化层,Request 的 meta、headers、callback 都得自己处理。
- 抓取任务的实时调度、重试延迟、去重这些能力,Celery 默认不是为爬虫场景设计的,得做大量额外配置。
所以如果项目本身没有其他异步任务要统一管理,单纯为了爬虫引入 Celery 并不划算。Scrapy-Redis 更贴合爬虫场景,改造量也最小。
2.4 什么时候该考虑更重的方案
Scrapy-Redis 适合中小规模、最大几十个节点以内的集群。如果你面临这些场景,要考虑更重的方案:
- 每天抓取数据量在亿级以上,需要流式处理。
- 需要精确的 URL 优先级调度和图遍历算法。
- 多个团队、多种爬虫共用一套基础设施,需要统一治理。
这种级别通常会引入消息中间件(例如 Kafka)作为任务管道,并配合任务编排系统。不过大部分业务根本走不到这一步,先把 Scrapy-Redis 吃透,能解决 90% 的问题。
3. 核心改造实操:从单机到分布式的完整落地流程
3.1 环境准备:依赖安装与 Redis 启动
改造前先确认基础环境。我的环境参考如下,版本差异不大时方案通用:
- Python 3.9+,Scrapy 2.5+
scrapy-redis0.7.0 或更高版本- Redis 5.0+,建议开启持久化(RDB + AOF 至少开一个)
安装命令:
pip install scrapy scrapy-redis redis确认 Redis 服务已启动,并测试连通性:
redis-cli ping # 返回 PONG 即正常生产环境建议给 Redis 配置密码,且不要把爬虫节点和 Redis 放在同一台机器上。节点只通过内网 IP 访问 Redis,避免公网暴露风险。
3.2 settings.py 的完整配置解读
以下是我在实际项目中使用过的一套配置,每一行的作用都标注清楚。
# settings.py BOT_NAME = "distributed_spider" # 分布式核心配置 SCHEDULER = "scrapy_redis.scheduler.Scheduler" DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter" SCHEDULER_PERSIST = True SCHEDULER_QUEUE_CLASS = "scrapy_redis.queue.PriorityQueue" REDIS_URL = "redis://your-redis-host:6379/0" # 请求与下载配置 CONCURRENT_REQUESTS = 32 DOWNLOAD_DELAY = 0.5 CONCURRENT_REQUESTS_PER_DOMAIN = 16 RANDOMIZE_DOWNLOAD_DELAY = True # 重试与异常处理 RETRY_ENABLED = True RETRY_TIMES = 3 RETRY_HTTP_CODES = [500, 502, 503, 504, 522, 524, 408, 429] DOWNLOAD_TIMEOUT = 15 # 日志与统计 LOG_LEVEL = "INFO" STATS_CLASS = "scrapy_redis.stats.RedisStatsCollector"几个容易忽略的点:
SCHEDULER_QUEUE_CLASS有PriorityQueue、FifoQueue、LifoQueue三种。绝大多数场景用PriorityQueue,因为 Request 的 priority 字段还可以生效。REDIS_URL一定要写所有节点都能访问到的地址,别用127.0.0.1。- 如果多个爬虫项目共用同一个 Redis 实例,要给每个项目设置独立的 key 前缀,避免互相干扰。在 spider 中通过
redis_key属性隔离。
3.3 Spider 改造:继承 RedisSpider 还是自己写
scrapy-redis提供了两个基础 Spider 类:RedisSpider和RedisCrawlSpider。
RedisSpider适合普通爬虫,RedisCrawlSpider适合需要 LinkExtractor 的整站爬取。改造后的 spider 不再定义start_urls,而是监听 Redis 中某个 key。
import scrapy from scrapy_redis.spiders import RedisSpider class DemoSpider(RedisSpider): name = "demo" redis_key = "demo:start_urls" def parse(self, response): # 解析逻辑保持不变 title = response.css("title::text").get() if title: yield {"url": response.url, "title": title}启动方式也随之改变:
# 先往 Redis 里推入起始 URL redis-cli lpush demo:start_urls "https://example.com/page/1" # 再启动爬虫 scrapy crawl demolpush用的是左压,RedisSpider 默认从右端弹出,所以可以随时往队列里追加新 URL,爬虫会实时消费。这是分布式爬虫“动态扩展任务”的核心能力。
3.4 Pipeline 改造:幂等写入与共享存储
Spider 的解析逻辑改动很小,真正要花心思的是 Pipeline。单机版 Pipeline 可以直接写 MySQL、写本地 JSON;分布式环境下每个节点都会执行 Pipeline,必须保证写入是幂等的,否则同一份数据会被多个节点重复写入。
我的做法是:入库前先查唯一索引,存在则跳过,不存在则插入。
class MysqlPipeline: def process_item(self, item, spider): url = item["url"] # 以 url 作为唯一键,先去重再插入 if not self.exists(url): self.insert(item) return item如果数据量很大,还可以考虑引入消息中间件做异步落库,不在 Pipeline 里直接写数据库。分布式抓取环节只负责产出 item,存储环节单独抽出去,能显著降低各节点的 IO 压力。
3.5 设置合理的请求参数:不盲目调大并发
分布式改造的目标是横向扩展,但不是每台机器都能疯狂调大并发。我给出的经验值:
- 单节点
CONCURRENT_REQUESTS建议 8~32,先跑一下摸清目标站的响应水平。 DOWNLOAD_DELAY设置 0.5~1 秒起步,优先保证稳定性和正确率,再逐步压缩。- 每个域名的并发控制务必设置,
CONCURRENT_REQUESTS_PER_DOMAIN建议不超过 16。
如果你把 32 个请求同时打到一个普通站点上,响应时间反而会急剧恶化,抓取效率不升反降。分布式真正释放的是“多节点的带宽与连接池容量的叠加”,而不是单节点的极限压榨。
4. 多节点部署与运行监控:让分布式真正稳定跑起来
4.1 节点角色划分:Master / Slave 该怎么理解
Scrapy-Redis 架构里不存在严格的 Master/Slave 概念,因为所有节点都是平等的 Worker。但通常这样部署:
- 一台 Redis 服务器:作为全局调度中心,建议独立部署。
- N 台爬虫节点:部署同样的项目代码,各自执行
scrapy crawl demo。 - 一台存储节点:跑 MySQL / MongoDB / ClickHouse 等,接收各节点写入。
如果某个爬虫节点挂掉,只要 Redis 还活着,其他节点会自动分担任务。你随时可以往集群里塞新节点,立即承接受影响的队列。这种扩展方式比单机版“重启恢复”可靠得多。
4.2 部署步骤:从代码分发到启动脚本
部署时要注意代码一致性。我用的是 Git + 定时git pull,简单粗暴但有效。更规范的做法是用部署平台批量分发,但小团队其实不用整得太复杂。
每台节点的启动流程:
# 1. 拉取最新代码 git pull origin master # 2. 安装依赖(建议使用虚拟环境) pip install -r requirements.txt # 3. 启动爬虫进程 scrapy crawl demo --logfile=/data/logs/demo.log --loglevel=INFO多节点启动时要注意:不要让所有节点都从同样的 start URL 开始,而是确认 Redis 中种子 URL 已经预置,节点启动后自然消费。否则可能发生“每台机器都用手动 lpush 一遍,任务翻了几倍”的低级失误。
4.3 监控 Redis 队列与抓取进度的实用手段
分布式爬虫调试时,最怕“任务看似在跑,实际上没有新 URL 进来”。我的监控手段由简到繁:
第一步,Redis 命令行直接看:
redis-cli llen demo:start_urls redis-cli zcard demo:requests # 如果是优先级队列,这里能看到请求总数 redis-cli scard demo:dupefilter # 已去重 URL 数量第二步,用scrapy crawl demo启动后,观察日志里的统计信息。scrapy-redis的RedisStatsCollector会把统计数据周期写入 Redis 中的 hash,可以用HGETALL查看,例如:
redis-cli hgetall demo:stats第三步,如果节点多了,建议引入现成的监控面板,直接读 Redis 数据展示各节点的抓取速度、失败率、队列积压量。注意:
监控队列长度比监控爬虫进程数量更有意义。队列积压说明抓取能力不足,进程再多但队列空转就是资源浪费。
4.4 动态添加 URL 与断点续爬的细节
分布式环境下的一个高频操作是“跑着跑着往里加新任务”。用redis-cli执行lpush即可:
redis-cli lpush demo:start_urls "https://example.com/page/2"但是有个细节:直接 push 到start_urls的 URL 必须经过 spider 内部的解析入口,否则会直接进入 parse 而不是经过你期望的规则。如果需要控制新任务的优先级,可以直接往优先级队列的 key 里zadd带分数。
断点续爬的体验则完全取决于SCHEDULER_PERSIST。把这行设置为True后,即使所有爬虫节点都停掉,Redis 中未完成的请求和去重集合依然保留。下次启动时,爬虫会从上次断掉的位置继续执行。
5. 常见问题排查与避坑实录
5.1 高频问题速查表
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 爬虫启动后不抓取任何 URL | Redis 中start_urls为空,或被消费完 | 检查llen/zcard,重新 lpush 种子 URL |
| 同一条数据被入库多次 | Pipeline 没有做幂等 | 建唯一索引,先查后写 |
| 所有节点同时抓同一个 URL | 去重过滤器失效 | 确认DUPEFILTER_CLASS配置正确,且 Redis key 没有被误清除 |
| 节点进程重启后任务全丢 | SCHEDULER_PERSIST未开启 | 确认 settings 里该配置为 True |
| Redis 中队列越积越长但节点空转 | 请求全部卡在下载超时或重试中 | 下调DOWNLOAD_TIMEOUT,检查目标站响应与代理质量 |
| 不同爬虫项目数据串了 | 多个项目共用 key 没隔离 | 不同的 spider 设置不同的redis_key |
5.2 几个值得牢记的实战心得
踩坑几个月之后,有几条心得值得分享:
第一,别把所有机器都部署在同一网段下的同一交换机。曾经有一次机房网络抖动,所有节点同时失联,Redis 连接断开重连风暴直接把服务打挂。后来把节点分散到了多个可用区,才真正体会到高可用的意义。
第二,Redis 必须开启持久化,且要定期备份。虽然只是任务队列,但一旦 Redis 挂掉且没有持久化,未消费的 URL 全丢。RDB + AOF 至少开一个,并设置合理的自动保存频率。
第三,优先用内网 IP 连接 Redis,别走公网。公网延迟和带宽波动会直接影响请求消费速度,我曾经把REDIS_URL配成公网地址,平均每个 URL 弹出延迟多了 20ms,整体吞吐下降肉眼可见。
5.3 合规与稳定性:分布式不等于无节制
最后提醒一句,分布式爬虫解决的是工程侧的问题,不是合规侧的问题。节点再多、架构再漂亮,也得对目标站点的访问强度保持克制:
- 优先阅读并遵守目标站点的访问协议。
- 设置合理的抓取频率,避免对目标站点造成过大压力。
- 控制抓取范围,只采集公开数据,不碰任何需要账号权限或明显非公开的内容。
- 采集后的数据使用要符合相关法律法规,特别是涉及个人信息的数据。
我自己把这套架构最终收敛为一种“克制的分布式”:调度能力可以无限扩张,但单节点请求频率始终打到合理的顶,绝不因为资源多就压榨目标站点。毕竟爬虫工程的长期价值在于稳定可持续地拿到数据,而不是一次性把链路打崩。
从单机到分布式,最让我意外的不是性能提升,而是运维心态的转变——之前每次重启都提心吊胆,生怕队列和去重状态丢失;现在不管哪个节点挂掉,只要 Redis 健康,整个系统就像什么都没发生过一样继续运行。如果你也被单机瓶颈卡住了,照着这套流程改造一次,中途遇到任何问题欢迎对照上面的排查表过一遍,大部分坑都能避开。