news 2026/10/14 5:33:13

Scrapy+Redis分布式爬虫改造实战:从单机到集群

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Scrapy+Redis分布式爬虫改造实战:从单机到集群

把单机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/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 demo

lpush用的是左压,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 高频问题速查表

现象可能原因处理方式
爬虫启动后不抓取任何 URLRedis 中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 健康,整个系统就像什么都没发生过一样继续运行。如果你也被单机瓶颈卡住了,照着这套流程改造一次,中途遇到任何问题欢迎对照上面的排查表过一遍,大部分坑都能避开。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 5:33:09

Android开发中为什么大多数控件不需要自定义背景?

你有没有被新同事问过“这个Button要不要加个背景色”“这个TextView加个圆角背景好看吗”这类问题?实际上,在Android开发里,绝大多数控件根本不需要你手动去设置背景,系统默认的那一套已经足够好用,而且比你随手写出来…

作者头像 李华
网站建设 2026/10/14 5:32:56

基于灰度/形状的2D模板匹配

最近在看图像模板匹配,之前就发现过一个基于灰度匹配的(之前博客有写),今天又看起来,找了个基于形状/特征点的目标匹配,说实话,这么高赞的项目,肯定比自己又查论文又浮现稳定性可靠多…

作者头像 李华
网站建设 2026/10/14 5:32:55

Unity图表插件XCharts 3.14集成实战:从底层原理到避坑指南

简介:XCharts 3.14是运行于Unity引擎的免费开源图表插件,定位为快速实现数据可视化,适合游戏开发、模拟驾驶舱、数据大屏等场景的开发者,官方宣称5分钟即可上手。资源包共906个文件,约5.32MB,以C#脚本为主体…

作者头像 李华
网站建设 2026/10/14 5:32:55

制造业AI落地实战:从场景拆解到工程化,避开那些坑

1. 从"AI万能论"到"场景拆解":制造业落地的认知转折2025年过半,我参与和旁听的制造业AI项目少说也有二十几个,从年产值几十亿的头部工厂到只有三条产线的小型配套企业,几乎都在谈AI。但真正让我觉得值得写这篇…

作者头像 李华
网站建设 2026/10/14 5:32:55

Go语言实战聊天系统:WebSocket+SQLite消息持久化全解析

简介:这是一套基于Go语言框架实现的全栈式聊天系统源码,面向后端开发初学者与全栈工程师,解决实时通信、用户权限控制及会话持久化等典型Web应用开发问题。资源包共293个文件,涵盖58个Go后端核心逻辑(含数据库操作、中…

作者头像 李华
网站建设 2026/10/14 5:32:24

消息工具重构:状态机、虚拟列表与设计令牌的三层解耦实践

1. 重构前的乱麻:1.x版本的交互、视觉、逻辑为什么会互相牵制MsgHelper 的1.x版本其实并不算不能用,但它属于典型的“能跑但是不敢碰”的代码。上个月完成2.0重构收尾之后,我把三个维度的改动拆出来复盘,发现最值得写的不是改了什…

作者头像 李华