把单机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 中全局队列支持多种队列策略去重过滤器 DupefilterPython 内存 setRedis 中的指纹集合待抓取队列 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/0SCHEDULER_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.9Scrapy 2.5scrapy-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 --loglevelINFO多节点启动时要注意不要让所有节点都从同样的 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 高频问题速查表现象可能原因处理方式爬虫启动后不抓取任何 URLRedis 中start_urls为空或被消费完检查llen/zcard重新 lpush 种子 URL同一条数据被入库多次Pipeline 没有做幂等建唯一索引先查后写所有节点同时抓同一个 URL去重过滤器失效确认DUPEFILTER_CLASS配置正确且 Redis key 没有被误清除节点进程重启后任务全丢SCHEDULER_PERSIST未开启确认 settings 里该配置为 TrueRedis 中队列越积越长但节点空转请求全部卡在下载超时或重试中下调DOWNLOAD_TIMEOUT检查目标站响应与代理质量不同爬虫项目数据串了多个项目共用 key 没隔离不同的 spider 设置不同的redis_key5.2 几个值得牢记的实战心得踩坑几个月之后有几条心得值得分享第一别把所有机器都部署在同一网段下的同一交换机。曾经有一次机房网络抖动所有节点同时失联Redis 连接断开重连风暴直接把服务打挂。后来把节点分散到了多个可用区才真正体会到高可用的意义。第二Redis 必须开启持久化且要定期备份。虽然只是任务队列但一旦 Redis 挂掉且没有持久化未消费的 URL 全丢。RDB AOF 至少开一个并设置合理的自动保存频率。第三优先用内网 IP 连接 Redis别走公网。公网延迟和带宽波动会直接影响请求消费速度我曾经把REDIS_URL配成公网地址平均每个 URL 弹出延迟多了 20ms整体吞吐下降肉眼可见。5.3 合规与稳定性分布式不等于无节制最后提醒一句分布式爬虫解决的是工程侧的问题不是合规侧的问题。节点再多、架构再漂亮也得对目标站点的访问强度保持克制优先阅读并遵守目标站点的访问协议。设置合理的抓取频率避免对目标站点造成过大压力。控制抓取范围只采集公开数据不碰任何需要账号权限或明显非公开的内容。采集后的数据使用要符合相关法律法规特别是涉及个人信息的数据。我自己把这套架构最终收敛为一种“克制的分布式”调度能力可以无限扩张但单节点请求频率始终打到合理的顶绝不因为资源多就压榨目标站点。毕竟爬虫工程的长期价值在于稳定可持续地拿到数据而不是一次性把链路打崩。从单机到分布式最让我意外的不是性能提升而是运维心态的转变——之前每次重启都提心吊胆生怕队列和去重状态丢失现在不管哪个节点挂掉只要 Redis 健康整个系统就像什么都没发生过一样继续运行。如果你也被单机瓶颈卡住了照着这套流程改造一次中途遇到任何问题欢迎对照上面的排查表过一遍大部分坑都能避开。