{T}

Scrapy 分布式与生产部署

本文档面向具备 Scrapy 单机开发经验的 Python 开发者,系统讲解如何将 Scrapy 爬虫扩展为分布式架构,并完成从开发到生产部署的全流程。阅读本文前,建议先掌握 Scrapy 基础用法。

概述

当你的 Scrapy 爬虫在单台机器上运行得不错时,迟早会遇到瓶颈:目标网站数据量太大,单机带宽和 CPU 不够用,爬取速度达不到业务要求。这就是分布式爬虫要解决的问题。

本章从三个层面展开:

  • 为什么需要分布式:理解单机瓶颈的本质,以及分布式如何突破这些限制
  • 分布式爬虫原理与实现:基于 Scrapy-Redis 实现 Master-Worker 分布式爬虫,包括 Bloom Filter 内存优化
  • 生产部署方案:从 Scrapyd 命令行部署,到 Docker 容器化,再到 Gerapy 可视化管理平台

一、为什么需要分布式爬虫

1.1 单机瓶颈分析

单台主机的爬虫存在以下天然限制:

瓶颈类型具体表现影响
CPU 瓶颈解析大量页面时 CPU 满载解析速度跟不上下载速度,队列堆积
带宽瓶颈出口带宽被占满(如 100Mbps)下载速率成为整个爬虫的天花板
IP 封禁单 IP 高频请求触发反爬爬虫被封后整个任务中断
内存瓶颈去重集合和爬取队列占用内存持续增长长时间运行后 OOM 崩溃
磁盘 I/O大量数据写入时磁盘成为瓶颈存储速度跟不上采集速度

1.2 分布式如何解决问题

分布式爬虫的核心思路是将爬取任务拆分到多台主机上协同执行,通过共享队列和共享去重集合来协调各节点:

  • N 台主机共享一个爬取队列:每台主机从同一个 Redis 队列中获取 Request,不会重复调度
  • N 台主机共享一个去重集合:爬过的 URL 指纹存入 Redis Set,所有节点共享去重结果
  • N 台主机使用不同 IP:每台主机独立出口 IP,绕开单 IP 频率限制
  • 理论上 N 倍效率:实际受 Redis 队列存取延迟、网络瓶颈、目标网站限速影响,通常为单机的 0.7N ~ 0.9N 倍

1.3 什么时候需要分布式

不是所有场景都需要分布式。决策依据:

图表渲染中…

二、分布式爬虫架构

2.1 Master-Worker 模式

Scrapy-Redis 采用的是对等架构(Peer-to-Peer),而非传统的主从(Master-Slave)架构。每个节点地位平等,自行从 Redis 队列中获取任务,没有中心调度节点。

2.2 分布式架构图

图表渲染中…

2.3 分布式爬虫需要解决的三个核心问题

问题一:共享爬取队列

在 Scrapy 单机架构中,爬取队列使用 Python 的 deque 实现,存储在本地内存中。分布式架构下,需要将队列外移到所有节点都能访问的存储中。Redis 是最理想的选择——基于内存、存取微秒级延迟,且支持多种队列结构。

问题二:共享去重集合

Scrapy 原生去重使用本地 set 集合存储 Request 指纹。分布式架构下,需要将去重集合外移。Redis 的 Set 数据结构天然支持去重,SADD 命令的返回值可以直接判断指纹是否已存在。

问题三:中断恢复

单机 Scrapy 使用 JOBDIR 参数将队列持久化到磁盘。分布式架构天然支持中断恢复——Redis 中的队列和去重集合在爬虫进程退出后依然存在,重启爬虫即可从上次中断处继续。

三、Scrapy-Redis 原理与配置

Scrapy-Redis 是 Scrapy 分布式最成熟的方案,它通过替换 Scrapy 的三个核心组件来实现分布式。

3.1 核心组件替换

组件原生 ScrapyScrapy-Redis存储位置
调度器 (Scheduler)scrapy.core.scheduler.Schedulerscrapy_redis.scheduler.Scheduler-
去重过滤器 (DupeFilter)scrapy.dupefilter.RFPDupeFilterscrapy_redis.dupefilter.RFPDupeFilter-
爬取队列 (Queue)本地 dequeRedis List / Sorted SetRedis
去重集合本地 setRedis SetRedis

3.2 三种队列实现

Scrapy-Redis 提供了三种队列,对应 Redis 不同数据结构:

python
# Python 3.10+
# 优先级队列(默认)—— 基于 Redis Sorted Set,支持请求优先级调度
SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.PriorityQueue'

# 先进先出队列 —— 基于 Redis List(lpush + rpop),广度优先
SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.FifoQueue'

# 后进先出队列 —— 基于 Redis List(lpush + lpop),深度优先
SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.LifoQueue'

优先级队列的工作原理:Request 的 priority 属性(数值越大优先级越高)取反后作为 Redis Sorted Set 的 score。因为 Sorted Set 按 score 从小到大排序,取反后高优先级的 score 最小,自然排在队首。

3.3 去重过滤器 RFPDupeFilter

核心逻辑极其简洁:

python
# Python 3.10+
def request_seen(self, request):
    """判断 Request 是否重复"""
    fp = self.request_fingerprint(request)  # 计算 SHA1 指纹
    added = self.server.sadd(self.key, fp)  # 向 Redis Set 添加指纹
    return added == 0  # added=0 表示已存在(重复),added=1 表示新增

指纹计算使用 SHA1 哈希,输入包括 Request 的 Method、URL、Body 和 Headers,任意一个字段不同都会产生不同的指纹。

3.4 调度器 Scheduler 数据流

图表渲染中…

3.5 完整配置示例

python
# Python 3.10+
# === settings.py ===
# 核心:替换调度器和去重过滤器
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"

# Redis 连接(推荐使用 URL 方式)
REDIS_URL = 'redis://:your_password@your_redis_host:6379'

# 持久化:爬取结束后不清空队列和去重集合
SCHEDULER_PERSIST = True

# 注意:分布式环境下不要设置 SCHEDULER_FLUSH_ON_START = True
# 否则每台 Worker 启动时都会清空队列,导致数据丢失

# 统一存储目标
MONGO_URI = 'mongodb://user:password@your_mongo_host:27017'
配置注意事项
  • 设置了 REDIS_URL 后,Scrapy-Redis 会忽略 REDIS_HOSTREDIS_PORTREDIS_PASSWORD 等分项配置
  • SCHEDULER_FLUSH_ON_START = True 在分布式环境中是危险的,每台 Worker 启动都会清空队列
  • 所有 Worker 的 MongoDB 连接必须指向同一台服务器,否则数据会分散存储

3.6 运行分布式爬虫

每台 Worker 主机上执行相同的命令:

bash
# 确保每台主机的 Python 环境和依赖一致
scrapy crawl your_spider_name

启动后,在 Redis 中可以看到两个 key:

  • {spider_name}:requests —— 爬取队列
  • {spider_name}:dupefilter —— 去重指纹集合

四、Bloom Filter 去重优化

4.1 为什么需要 Bloom Filter

Scrapy-Redis 默认使用 Redis Set 存储去重指纹。每个指纹是 40 个十六进制字符(SHA1),占用 20 字节。按此计算:

指纹数量Redis Set 内存占用
100 万~20 MB
1,000 万~200 MB
1 亿~2 GB
10 亿~20 GB

当爬取量达到亿级时,仅去重指纹就占用了大量内存,Redis 可能不堪重负。Bloom Filter 用位数组(Bitmap)替代 Set 存储,能将内存占用降低一个数量级。

4.2 Bloom Filter 原理

Bloom Filter 使用一个长度为 m 的位数组和 k 个独立的哈希函数。操作分为两步:

插入元素:对元素分别计算 k 个哈希值,将位数组中对应位置的值设为 1。

查询元素:对元素分别计算 k 个哈希值,检查位数组中对应位置是否全为 1。如果全为 1,则元素可能存在(有误判);如果任意一位为 0,则元素一定不存在

图表渲染中…

关键特性

  • 假阳性(False Positive):不存在的元素可能被误判为存在,意味着可能漏爬少量 URL
  • 无假阴性(No False Negative):存在的元素一定不会被误判为不存在,意味着不会重复爬取
  • 不支持删除:标准 Bloom Filter 无法删除已插入的元素

4.3 误判率控制

误判率 p 与三个参数相关:位数组大小 m、元素数量 n、哈希函数个数 k。经验公式:

  • 最优哈希函数个数:k = (m/n) * ln(2)
  • 在给定 m/n 和最优 k 下,误判率约为 p ≈ 0.6185^(m/n)

常见配置参考:

BLOOMFILTER_BIT位数组大小内存占用最优 k1亿元素误判率适用量级
282^28 位32 MB5~3%千万级
302^30 位128 MB6~0.8%1 亿级
322^32 位512 MB6~0.08%10 亿级
332^33 位1 GB7~0.05%10 亿级

4.4 对接 Scrapy-Redis

安装已封装好的库:

bash
pip3 install scrapy-redis-bloomfilter

替换去重过滤器配置:

python
# Python 3.10+
# 使用 Bloom Filter 版本的去重过滤器
DUPEFILTER_CLASS = "scrapy_redis_bloomfilter.dupefilter.RFPDupeFilter"

# 位数组大小参数,默认 30(128MB,适合 1 亿级去重)
BLOOMFILTER_BIT = 30

# 哈希函数个数,默认 6
BLOOMFILTER_HASH_NUMBER = 6

核心实现逻辑(自定义实现参考):

python
# Python 3.10+
class BloomFilter:
    """基于 Redis Bitmap 的布隆过滤器"""
    def __init__(self, server, key, bit=30, hash_number=6):
        self.m = 1 << bit  # 位数组大小 = 2^bit
        self.server = server
        self.key = key
        # 用不同 seed 生成 k 个哈希函数
        self.hash_funcs = [HashMap(self.m, seed) for seed in range(hash_number)]

    def exists(self, value):
        """判断元素是否可能存在(True = 可能存在,False = 一定不存在)"""
        if not value:
            return False
        # 对所有哈希位置做 getbit,全部为 1 才返回 True
        return all(
            self.server.getbit(self.key, h.hash(value))
            for h in self.hash_funcs
        )

    def insert(self, value):
        """将元素插入过滤器"""
        for h in self.hash_funcs:
            self.server.setbit(self.key, h.hash(value), 1)
Bloom Filter 使用注意事项
  • Bloom Filter 不支持删除,如需重新爬取,手动删除 Redis 中对应的 key 即可
  • BLOOMFILTER_BIT 参数需要根据预估的爬取总量设置,设置过小会导致误判率过高
  • 不同爬虫项目应使用不同的 Redis key 前缀,避免 Bloom Filter 互相干扰

五、分布式部署方案

分布式爬虫写好之后,还需要解决部署问题。如果只有 2-3 台服务器,手动上传代码还可以接受;但如果有几十台甚至上百台,就需要自动化部署方案。

5.1 部署工具全景

工具定位适用规模核心能力
Scrapyd爬虫运行服务1-10 台HTTP API 管理爬虫任务
Scrapyd-Client命令行部署工具3-10 台一键打包部署到 Scrapyd
Docker容器化运行环境5-50 台环境隔离、一次构建到处运行
Gerapy可视化管理平台10-50+ 台Web 界面管理、批量部署、任务监控

5.2 方案一:Scrapyd + Scrapyd-Client

Scrapyd 是一个运行 Scrapy 爬虫的 HTTP 服务,提供 RESTful API 来管理爬虫任务。

安装与启动:

bash
pip3 install scrapyd
scrapyd  # 默认监听 0.0.0.0:6800

核心 API 端点:

端点方法功能
/daemonstatus.jsonGET查看服务状态
/addversion.jsonPOST部署项目(需上传 Egg 文件)
/schedule.jsonPOST启动爬虫任务
/cancel.jsonPOST取消爬虫任务
/listprojects.jsonGET列出所有项目
/listjobs.jsonGET查看任务状态

Scrapyd-Client 将手动打包 Egg 和调用 API 两步合并为一条命令。

配置 scrapy.cfg

ini
[deploy:vm1]
url = http://120.27.34.24:6800/
project = weibo

[deploy:vm2]
url = http://139.217.26.30:6800/
project = weibo

部署命令:

bash
# 部署到指定主机
scrapyd-deploy vm1

# 部署到所有已配置主机
scrapyd-deploy -a
安全建议

不要将 Scrapyd 的 6800 端口直接暴露在公网。建议使用 Nginx 反向代理并添加 HTTP Basic Auth 认证,将 Scrapyd 绑定到 127.0.0.1。

5.3 方案二:Docker 容器化部署

Docker 解决了环境一致性问题——每台主机的 Python 版本、依赖库版本可能不同,而 Docker 镜像包含了完整且一致的运行环境。

Dockerfile 示例:

dockerfile
FROM python:3.10
ADD . /code
WORKDIR /code
COPY ./scrapyd.conf /etc/scrapyd/
EXPOSE 6800
RUN pip install -r requirements.txt
CMD scrapyd

关键配置修改(scrapyd.conf):

ini
[scrapyd]
bind_address = 0.0.0.0   # 允许外部访问(容器内必须这样设置)
max_proc_per_cpu = 10    # 单核最多运行的爬虫任务数

构建与分发:

bash
# 构建镜像
docker build -t myproject/scrapyd:latest .

# 推送到 Docker Hub
docker tag myproject/scrapyd:latest registry.example.com/myproject/scrapyd:latest
docker push registry.example.com/myproject/scrapyd:latest

# 各主机拉取并运行
docker run -d --restart=always -p 6800:6800 registry.example.com/myproject/scrapyd

5.4 方案三:批量部署(Fabric / Ansible 概述)

当主机数量达到 20 台以上时,需要在每台主机上安装 Docker 并启动 Scrapyd 容器。手动操作费时且容易出错,可以用 Ansible 实现批量部署。

Ansible 的核心优势:

  • 无需 Agent:通过 SSH 连接目标主机,不需要在目标主机上安装额外软件
  • 声明式配置:Playbook(YAML 格式)描述目标状态,Ansible 自动执行到目标状态
  • 幂等性:多次执行 Playbook 结果一致,不会重复操作

Ansible Playbook 示例:

yaml
# Python 3.10+ 环境
# scrapyd_deploy.yml
---
- name: 批量部署 Scrapyd Docker 环境
  hosts: scrapyd_servers
  become: yes
  tasks:
    - name: 安装 Docker
      apt:
        name: docker.io
        state: present
        update_cache: yes

    - name: 拉取 Scrapyd 镜像
      docker_image:
        name: registry.example.com/myproject/scrapyd
        source: pull

    - name: 运行 Scrapyd 容器
      docker_container:
        name: scrapyd
        image: registry.example.com/myproject/scrapyd
        ports:
          - "6800:6800"
        restart_policy: always
        state: started

    - name: 验证 Scrapyd 服务
      uri:
        url: "http://{{ inventory_hostname }}:6800/daemonstatus.json"
        return_content: yes

Inventory 主机清单:

ini
[scrapyd_servers]
120.27.34.24  ansible_user=root
139.217.26.30 ansible_user=root
120.27.34.26  ansible_user=root

执行:

bash
ansible-playbook -i hosts scrapyd_deploy.yml

5.5 方案四:Gerapy 可视化管理平台

Gerapy 是基于 Django + Vue.js 的分布式爬虫管理框架,将 Scrapyd 的命令行操作全部转化为 Web 界面操作。

Gerapy 架构

图表渲染中…

快速上手:

bash
pip3 install gerapy
gerapy init          # 初始化项目目录
gerapy migrate       # 初始化 SQLite 数据库
gerapy runserver     # 启动 Web 服务(默认 :8000)

Gerapy 提供以下核心功能:

  • 主机管理:添加多台 Scrapyd 主机,实时监控各主机状态
  • 项目管理:将 Scrapy 项目放入 gerapy/projects/ 目录,在线编辑代码
  • 一键打包部署:选择目标主机,点击按钮即可批量部署
  • 任务调度:Web 界面启动/停止爬虫任务,实时查看日志
  • 定时任务:配置定时规则,自动调度爬虫运行

六、从开发到部署的完整流水线

图表渲染中…

七、监控与告警

分布式爬虫上线后,不能放任不管。需要建立监控体系来确保爬虫健康运行。

7.1 监控指标

监控维度具体指标获取方式告警阈值建议
Redis 队列健康队列长度、去重集合大小redis-cli LLEN/ZCARD队列为空且持续超过 5 分钟
爬虫存活状态各 Worker 心跳Scrapyd /daemonstatus.json某 Worker 超过 2 分钟未响应
数据产出量每分钟入库 Item 数量MongoDB 计数产出量下降超过 50%
错误率下载失败、解析异常Scrapy Stats 统计错误率超过 10%
Redis 内存Redis 内存使用率redis-cli INFO memory超过 80%
目标网站状态响应状态码分布Scrapy Stats403/429 比例突然上升

7.2 健康检查脚本

python
# Python 3.10+
import redis
import requests
from datetime import datetime, timedelta

def check_workers_health(scrapyd_urls: list[str], redis_url: str) -> dict:
    """
    检查所有 Worker 和 Redis 的健康状态
    :return: 健康报告字典
    """
    report = {"timestamp": datetime.now().isoformat(), "workers": {}, "redis": {}}

    # 检查各 Worker
    for url in scrapyd_urls:
        try:
            resp = requests.get(f"{url}/daemonstatus.json", timeout=5)
            data = resp.json()
            report["workers"][url] = {
                "status": "ok" if data.get("status") == "ok" else "error",
                "running": data.get("running", 0),
                "pending": data.get("pending", 0),
                "finished": data.get("finished", 0),
            }
        except Exception as e:
            report["workers"][url] = {"status": "unreachable", "error": str(e)}

    # 检查 Redis
    r = redis.from_url(redis_url)
    info = r.info("memory")
    report["redis"] = {
        "used_memory_human": info.get("used_memory_human"),
        "connected_clients": info.get("connected_clients"),
        "uptime_in_days": info.get("uptime_in_days"),
    }

    return report

7.3 告警通知

建议通过以下方式发送告警:

  • 企业微信/钉钉 Webhook:异常时推送消息到工作群
  • 邮件通知:每日汇总报告
  • Prometheus + Grafana:搭建可视化监控面板(适合大规模集群)

八、常见陷阱

陷阱表现原因解决方案
Redis 连接泄漏Worker 运行一段时间后无法连接 Redis连接池耗尽,未正确释放配置 CONCURRENT_REQUESTS 限制并发,使用 Redis Sentinel 做高可用
去重丢失重启后爬虫重新爬取已爬过的页面SCHEDULER_PERSIST = False(默认值)设置 SCHEDULER_PERSIST = True
Worker 负载不均部分 Worker 空闲,部分 Worker 繁忙网络延迟差异、目标网站对不同 IP 的响应速度不同属于正常现象,增加 Worker 数量即可。也可通过监控动态调整
Redis 单点故障Redis 宕机导致所有 Worker 停止Redis 未配置高可用使用 Redis Sentinel 或 Redis Cluster
Redis 内存溢出长时间运行后 Redis 内存用满去重集合无限增长使用 Bloom Filter 替代 Set;估算总数据量,提前规划内存
数据存储分散各 Worker 数据存在本地 MongoDB忘记修改 MongoDB 连接为远程地址所有 Worker 统一指向远程 MongoDB
版本不一致各 Worker 运行不同版本的爬虫代码逐台手动部署导致遗漏使用 Docker 镜像确保版本一致,或使用 Gerapy 批量部署
目标网站被封所有 Worker 同时返回 403多台 Worker 共用少量 IP 导致封禁每台 Worker 使用独立代理 IP,或配置代理池轮换
Python 版本不兼容Worker 启动失败或运行异常各主机 Python 版本不一致,pickle 序列化不兼容统一使用 Docker 镜像,或在 requirements.txt 中锁定 Python 版本
时钟不同步调度异常、日志时间戳混乱多机系统时间不一致配置 NTP 时间同步服务

九、最佳实践

  1. 渐进式演进:先单机跑通,再接入 Scrapy-Redis 做分布式,最后引入 Docker 和 Gerapy 做运维管理。不要一步到位。
  2. Redis 高可用:生产环境务必配置 Redis Sentinel 或 Cluster,避免单点故障。Redis 是整个分布式爬虫的"心脏"。
  3. 合理设置并发:分布式爬虫的多机并发效果叠加,务必控制每台 Worker 的 CONCURRENT_REQUESTSDOWNLOAD_DELAY,避免对目标网站造成过大压力。
  4. 使用 Docker 保证环境一致:哪怕只有 3 台 Worker,也建议使用 Docker。它能消除"我这台机器能跑,你那台不行"的问题。
  5. 监控先行:上线前就搭建好监控和告警。至少监控 Redis 队列长度和 Worker 存活状态,异常时第一时间收到通知。
  6. 数据量预估:开始爬取前估算总数据量,根据量级选择去重方案(Redis Set vs Bloom Filter),提前规划 Redis 内存。
  7. 统一存储目标:所有 Worker 的数据写入同一个远程数据库(MongoDB/MySQL),方便统一查询和分析。
  8. 日志集中管理:使用 ELK(Elasticsearch + Logstash + Kibana)或 Loki 将各 Worker 的日志集中收集,便于排查问题。

术语表

术语英文释义
分布式爬虫Distributed Crawler多台主机协同执行爬取任务的爬虫架构
共享爬取队列Shared Crawl Queue多节点共同访问的 Request 队列,通常由 Redis 维护
去重指纹FingerprintRequest 的 SHA1 散列值,用于判断请求是否重复
Scrapy-RedisScrapy-Redis基于 Redis 实现的 Scrapy 分布式组件库
布隆过滤器Bloom Filter基于位数组的概率型数据结构,用于节省内存的去重
假阳性False Positive不存在的元素被误判为存在(Bloom Filter 的固有特性)
误判率False Positive RateBloom Filter 假阳性出现的概率,由 m/n 比值和 k 值决定
调度器Scheduler负责从队列中存取 Request 的组件
去重过滤器DupeFilter负责判断 Request 是否重复的组件
优先级队列Priority Queue按 Request 优先级排序的队列,基于 Redis Sorted Set
ScrapydScrapydScrapy 爬虫的部署和运行 HTTP 服务
Scrapyd-ClientScrapyd-ClientScrapy 项目的一键打包部署命令行工具
EggEggPython 项目的打包格式,Scrapyd 的部署单元
DockerDocker容器化平台,用于环境隔离和一致性部署
DockerfileDockerfileDocker 镜像的构建脚本
GerapyGerapy基于 Django + Vue.js 的分布式爬虫可视化管理平台
AnsibleAnsible基于 SSH 的自动化运维工具,用于批量部署
SentinelRedis SentinelRedis 官方高可用方案,提供监控、通知和自动故障转移
SCHEDULER_PERSISTScheduler Persist爬取结束后是否保持队列不清除的配置项
NTPNetwork Time Protocol网络时间协议,用于同步多台主机的时间

延伸阅读

站内链接

  • Scrapy 基础 —— Scrapy 框架入门与进阶
  • 数据存储 —— MongoDB、MySQL 等存储方案
  • 异步爬虫 —— 异步爬虫方案对比

外部链接

版本差异(爬虫技术栈 → 当前版本)

本文编写时当前稳定版
requests2.28/2.312.32.x
Scrapy1.x/2.02.11.x(API 稳定)
httpx0.240.28.x
Playwright1.3x1.6x(Python 版)
lxml/BeautifulSoup旧版保持稳定
Python3.8-3.123.14(推荐)

本文讲解的爬虫原理(HTTP、解析、反爬、存储)与核心 API 在最新版本中成立;注意 Python 3.9 及以下已 EOL,新项目使用 3.13/3.14。