Scrapy-Redis分布式架构 - 构建高性能分布式爬虫集群

📂 所属阶段:第五阶段 — 战力升级(分布式与进阶篇)
🔗 相关章节:DownloaderMiddleware · Spider中间件深度定制 · 分布式去重与调度

当爬虫任务量超过单机承受范围时,纵向增加 CPU 或带宽总会有天花板。Scrapy-Redis 把这层天花板打破——它基于 Redis 实现了共享请求队列分布式去重集合,让多台机器上的多个 Scrapy 实例像一支训练有素的团队一样协同工作。本文将带你从架构理解到动手部署,一步步搭建起稳定、可扩展的爬虫集群。


1. 架构总览:分布式爬虫是如何“分工”的

Scrapy-Redis 的核心思想很简单:用 Redis 充当调度中心和去重中心,各个爬虫节点只管抓取和处理数据。所有节点共享同一套请求队列和已抓取的 URL 指纹集合,从而避免重复抓取,并天然实现了负载均衡。

下面的流程图展示了一次完整的分布式抓取过程:

graph TB
    A[初始URL<br/>Redis键] --> B{Redis集群}
    subgraph B
        Q[共享请求队列<br/>支持优先级/FIFO/LIFO]
        DF[共享去重集合<br/>RFPDupeFilter]
    end
    Q --> C1[爬虫节点1]
    Q --> C2[爬虫节点2]
    Q --> C3[爬虫节点N]
    C1/C2/C3 --> DF
    C1/C2/C3 -->|新URL入队| Q
  • 共享请求队列:保存在 Redis 中,所有节点都从这里取任务,并把自己发现的新链接放回去。
  • 共享去重集合:同样存在 Redis 里,用来存放每个 URL 的指纹。任何节点在抓取前都会先检查指纹是否已存在,从而彻底避免重复抓取。
  • 无中心调度:Redis 本身不主动分配任务,而是靠节点们“自觉”去阻塞式地拉取,无形中实现了按处理速度的负载分担。

2. 快速上手:三步搭起第一个分布式集群

2.1 安装依赖

pip install scrapy-redis redis

确保所有爬虫节点都安装相同版本的依赖。

2.2 配置 Scrapy 项目

在你的 settings.py 中完成以下替换,这是从单机到分布式的核心三个动作

# settings.py
import os

# 1. 启用 Redis 调度器,替代默认的内存调度器
SCHEDULER = "scrapy_redis.scheduler.Scheduler"

# 2. 启用 Redis 指纹去重,替代默认的内存去重
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"

# 3. 配置 Redis 连接(支持哨兵/集群)
REDIS_URL = os.getenv('REDIS_URL', 'redis://localhost:6379/0')
# 也可以分开配置:
# REDIS_HOST = 'localhost'
# REDIS_PORT = 6379
# REDIS_PASSWORD = 'your_pwd'
# REDIS_DB = 0

# 4. 强烈推荐启用断点续爬
SCHEDULER_PERSIST = True  # 爬虫关闭后保留队列,重启可继续

# 5. 选择队列类型(默认优先级队列)
SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.PriorityQueue'

提示SCHEDULER_PERSIST = True 会保持 Redis 中的队列数据不丢失,非常适合长时间运行的爬虫;如果你希望每次启动都全新开始,可以设为 False

2.3 改造 Spider

我们需要将原生 Spider 改为从 Redis 获取起始 URL,同时不再使用 start_urls。这里以 RedisSpider 为例:

# spiders/distributed_spider.py
from scrapy_redis.spiders import RedisSpider
from scrapy.http import Request
import time

class DemoDistributedSpider(RedisSpider):
    name = 'demo_distributed'
    redis_key = f'{name}:start_urls'   # 共享的初始URL键,所有节点都会监听它

    custom_settings = {
        'CONCURRENT_REQUESTS': 32,     # 单节点并发,视目标网站承受能力调整
        'DOWNLOAD_DELAY': 1,
    }

    def parse(self, response):
        # 提取数据
        yield {
            'url': response.url,
            'title': response.css('title::text').get(),
            'node_id': self._get_node_id(),
            'timestamp': time.time()
        }

        # 提取新链接并生成请求(自动进入Redis队列和去重)
        for link in response.css('a::attr(href)').getall()[:10]:  # 示例仅取前10条
            absolute_url = response.urljoin(link)
            yield Request(url=absolute_url, callback=self.parse)

    def _get_node_id(self):
        import socket
        return socket.gethostname()

注意RedisSpider 会忽略类变量 start_urls,起始任务必须通过 Redis 键(如 demo_distributed:start_urls)手动注入。


3. 启动集群:一条命令,多端并行

3.1 启动 Redis 服务

确保所有爬虫节点都能访问到 Redis。生产环境建议采用哨兵模式Redis 集群,避免单点故障。

3.2 注入起始 URL

打开 Redis 命令行,将第一条种子 URL 推入队列:

redis-cli lpush demo_distributed:start_urls https://example.com

可以一次性推入多条种子,每个种子对应一个或多个起始页面。

3.3 启动爬虫节点

在每个节点上执行相同的命令:

scrapy crawl demo_distributed

节点数量不限,你可以随时添加新机器,爬虫会自动分担任务。所有节点输出中都可以看到来自不同机器的 node_id 标识,验证分布式协同已经生效。


4. 核心机制深度解析

4.1 共享请求队列:三种队列的选型策略

Scrapy-Redis 通过 SCHEDULER_QUEUE_CLASS 支持三种队列实现,本质都是利用 Redis 的不同数据结构:

队列类型Redis 实现行为特点适用场景
PriorityQueue(默认)ZSet(score = -优先级)数字越小优先级越高;同等优先级按加入顺序需要优先抓取重要页面,如首页、核心目录
FifoQueueList(LPUSH + BRPOP)先进先出,类似普通队列常规广度优先爬取,稳定有序
LifoQueueList(LPUSH + LPOP)后进先出,类似栈快速探测新链接,但要注意避免深层递归

示例切换为先进先出队列:

SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.FifoQueue'

4.2 共享指纹去重:你的爬虫如何记住“去过的地方”

默认的 RFPDupeFilter 会将每个请求的 method + url + body 计算成一个 SHA1 指纹,存入 Redis 的 Set 中。其简化逻辑如下:

import hashlib

def default_fingerprint(request):
    fp = hashlib.sha1()
    fp.update(request.method.encode('utf-8'))
    fp.update(request.url.encode('utf-8'))
    fp.update(request.body or b'')
    return fp.hexdigest()

正因为所有节点共用同一个 Set,任何一个链接被处理过一次后,其他节点都不会再重复抓取。

4.3 负载均衡:自动的“能者多劳”

Scrapy-Redis 的负载均衡是被动式的,不需要额外配置:

  • 节点通过 BRPOP 等阻塞命令等待新请求。
  • 处理速度快的节点会更快地取走任务,慢节点或临时卡顿的节点自然获得的请求就少。
  • 若需按域名或某些规则更精细地分片,需要自定义调度器或增加路由逻辑。

5. 生产环境部署与运维

5.1 Redis 连接与重试策略

推荐在 settings.py 中通过 REDIS_PARAMS 精细控制连接行为:

REDIS_PARAMS = {
    'socket_timeout': 30,
    'socket_connect_timeout': 30,
    'retry_on_timeout': True,
    'encoding': 'utf-8',
    'health_check_interval': 30,  # 定期检查连接有效性
    'max_connections': 20,        # 每个节点连接池大小
    # 哨兵模式示例(解注释并填写实际信息)
    # 'sentinel': [('sentinel1', 26379), ('sentinel2', 26379)],
    # 'sentinel_service_name': 'mymaster',
    # 'sentinel_password': 'sentinel_pwd',
}

5.2 断点续爬与队列清理

  • 续爬:只需保证 SCHEDULER_PERSIST = True,爬虫中断后重启即会继续处理剩余任务。
  • 重新开始:如果想清空历史重新爬取,需要手动删除 Redis 中的相关键:
    redis-cli
    > DEL demo_distributed:requests     # 清理请求队列
    > DEL demo_distributed:dupefilter   # 清理去重集合
    > DEL demo_distributed:start_urls   # 清理初始种子

5.3 监控 Redis 内存

分布式爬虫会持续向 Redis 写入请求和指纹,建议在 Redis 配置中设置合理的内存上限和淘汰策略:

maxmemory <字节数>
maxmemory-policy allkeys-lru

定期关注队列长度和指纹集合大小,必要时可以定时清理过期的去重指纹(需自行扩展)。


6. 最佳实践与常见坑点

6.1 最佳实践

  • 限制并发与延迟:根据目标网站承受能力,合理设置 CONCURRENT_REQUESTSDOWNLOAD_DELAYCONCURRENT_REQUESTS_PER_DOMAIN 等参数,避免被封。
  • 开启 Redis 持久化:混合使用 RDB 和 AOF,防止意外重启导致任务丢失。
  • 规划种子策略:起始 URL 应尽可能覆盖关键页面,可结合 Sitemap 或数据库按批次推入 Redis。
  • 日志与监控:为每个节点增加唯一标识(如 node_id),并接入集中式日志(ELK/Loki)和监控(Prometheus),方便查看各节点状态和抓取进度。

6.2 常见避坑指南

  • ❌ 不要同时配置 start_urlsredis_key:一旦使用 RedisSpiderstart_urls 就会被忽略,所有起始任务必须从 Redis 注入。
  • ❌ 不要在代码中硬编码 Redis 键名:通过 name 变量或配置文件动态构造,避免多爬虫项目键名冲突。
  • ❌ 不要忽视 Redis 连接异常:务必配置合理的超时和重试参数,并配合健康检查,防止因连接僵死导致爬虫停滞。
  • ❌ 不要让请求队列无限膨胀:定期检查队列长度,对异常增长的 URL 规则进行过滤或限流,避免 Redis 内存溢出。

7. 扩展思路

完成以上基础架构后,你可以根据实际需求进一步扩展:

  • 自定义去重逻辑:继承 RFPDupeFilter 并重写 request_fingerprint,实现更灵活的 URL 去重(如忽略参数排序)。
  • 动态种子注入:编写脚本从数据库或 API 获取种子,定期推入 Redis,实现持续性增量抓取。
  • 节点自动扩缩:结合 Docker/Kubernetes,根据队列长度指标自动增加或减少爬虫实例。
  • 分布式数据管道:使用 scrapy-redis 自带的 RedisPipeline 或将数据直接写入 Kafka、MongoDB,实现抓取与处理解耦。

现在你已经掌握了 Scrapy-Redis 从原理到部署的完整链路。按照本文的步骤,你可以在短时间内搭建起一个稳定、可横向扩展的分布式爬虫集群,将爬虫效率提升数倍甚至数十倍。