简介:这是一份基于Docker的分布式爬虫服务完整资料包,面向Python与Go技术栈的爬虫开发者,以及计算机相关专业在校学生、教师和企业工程师。资源直接针对多节点爬虫部署、容器化调度与高效抓取场景,既适合毕业设计、课程设计、项目立项演示,也可作为从零搭建分布式爬虫的学习样本。包内围绕服务端与客户端设计展开,覆盖Docker镜像构建、protobuf接口协议、容器化部署、单机与多机爬虫调用等核心环节,并配有架构示意图与运行说明,有助于理解服务拆分、任务分发和数据采集的完整链路。资源共11个文件,以Go语言源码为主,辅以Dockerfile、shell构建脚本、proto协议描述、说明文档及架构示意图,压缩包仅311KB,结构精炼、便于按需查阅。已有54人学习下载。该资源经项目测试运行成功,具备较高完成度,附有授权码与项目文档,既能直接支撑课题答辩,也能在此基础上改造扩展,适应不同业务需求。
1. 拿 docker 分布式爬虫资料包之前:先想清楚它在解决什么问题
做爬虫做到一定规模,单机 Python 进程总会先撞上两个天花板:一是带宽和 CPU 跑不满,二是 URL 去重状态全存在本地内存,一重启就丢。很多人第一反应是写个按 URL 哈希分片的调度脚本,把任务拆给几台机器,结果发现还要处理节点心跳、任务重投、去重同步,代码越写越长,最后变成一套自己都维护不了的私有分布式系统。这个标题给的方案其实很朴素:用 Docker 把爬虫服务容器化,用 Redis 做任务队列和去重中心,多开几个 Worker 容器就是分布式。它适合的场景是手里已经有能跑的 Scrapy 爬虫,想用最小成本换成分发模式,又不想引入 Kubernetes 这么重的编排系统。如果你卡在这个阶段,这份资料的核心价值就是那套可复现的配置,而不是爬虫业务逻辑本身。
2. 基于 docker 的分布式爬虫架构:为什么是 Redis 而不是 Kubernetes
2.1 分布式爬虫的协调者,Redis 为什么够用
先明确一个前提:分布式爬虫的难点从来不在“多进程跑同一个爬虫代码”,而在三个协调点——任务队列怎么共享、URL 去重怎么做、失败任务怎么重投。Kubernetes 能解决 Pod 调度和自动扩缩容,但它解决不了去重和队列,你照样要部署 Redis 或 RabbitMQ 来做状态中心。Kubernetes 带来的节点管理、Ingress、存储卷这些能力,对一批跑固定爬虫任务的 Worker 来说大部分用不上。
Redis 够用的理由很直接:单实例 Redis 的 LPUSH/BRPOP 操作能撑住每秒几千次请求分发,URL 去重用 SADD 或 ZSET 也只在内存里做判断,这个吞吐量对绝大多数垂直爬虫项目已经溢出。用 Redis 做协调者最大的好处是运维简单,一个容器、一个端口、一个密码,不依赖任何外部组件,Docker Compose 一条命令就能把整个集群拉起来。相比之下,Kubernetes 的部署和调参成本,在有三个以上节点之前都是负收益。
2.2 三种常见方案的取舍
除了 Redis 队列方案,常见的还有 Celery + Redis 和自研 RPC 调度。Celery 的优势是任务状态管理完善,有重试、超时、结果回执这些现成能力,但劣势是队列语义是“先进先出”,不适合给爬虫做 URL 级去重,你仍然要自己维护一个去重集合。自研 RPC 调度听起来灵活,实际上要把每个 Worker 的心跳、任务分片、故障恢复全部自己写一遍,光一个“任务跑了一半节点挂了”的场景就能折腾两周。
这里要区分“调度框架”和“爬虫框架”两个层面。Celery 管的是“任务函数”,Scrapy 管的是“请求对象”,两者中间需要一个桥接层。直接选 Scrapy 生态里的 Scrapy-Redis 组件更省事,它不改变爬虫代码写法,只替换 Scrapy 默认的去重器和调度器,让所有 Worker 共享同一个 Redis 实例。标题里的资料包如果组织得合理,核心内容应该就是这套配置的完整拆解。
2.3 最小可跑架构的四个角色
一个能实际运行的架构只需要四个角色。Redis 容器做状态中心,负责 URL 队列、去重集合和调度数据。一个 Worker 镜像,里面打包爬虫代码和 Scrapy,每个容器启动后从 Redis 取 URL 抓取页面。定时触发或手动触发的一次性任务入口,负责把种子 URL 推入 Redis 队列。最后是一个结果出口,抓到的数据直接写 MySQL 或对象存储,不走 Redis,避免大对象积压。
这四个角色对应 docker-compose.yml 里的四个 service,其中 Worker 用replicas参数控制并发数。业务增长时不需要改代码,只需要调整 Compose 里的副本数重新拉起。这套结构的核心约束是:爬虫本身不能有本地状态。所有需要跨 Worker 共享的数据——待抓取 URL、已抓取指纹、请求队列——必须全部放到 Redis。很多人的分布式爬虫翻车,不是配错了 Docker,而是爬虫代码里有本地文件缓存或内存集合,导致每个容器各抢各的任务,去重形同虚设。
3. 写爬虫镜像和 Compose 编排:从 Dockerfile 到一键拉起整个集群
3.1 基础镜像选择:slim 系优先,别碰 alpine
用 docker 部署爬虫,第一个坑就是选基础镜像。python:3.9-slim和python:3.9-alpine之间我一般直接选 slim。Alpine 的优点是体积小,但 Scrapy 依赖的 lxml、cryptography 这些包在 musl libc 下经常要现场编译,慢不说,还可能因为缺少编译工具链直接报错。slim 基于 Debian,apt 装依赖顺畅,编译包时踩坑少得多。一个爬虫镜像里塞了 Scrapy、Requests、MySQL 客户端,slim 版构建时间能比 alpine 少一半以上。
Dockerfile 里有一个必须处理的细节:时区。Scrapy 默认记录抓取时间用 UTC,日志里差 8 小时排查问题时非常误导。建议在构建阶段直接把时区写死:
FROM python:3.9-slim ENV TZ=Asia/Shanghai \ PYTHONUNBUFFERED=1 \ PIP_NO_CACHE_DIR=1 RUN ln -fs /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ && echo "Asia/Shanghai" > /etc/timezone WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple COPY . . RUN useradd -r -u 1001 spider USER spider CMD ["scrapy", "crawl", "quotes"]逻辑说明:PYTHONUNBUFFERED=1强制 Python 日志实时输出到 stdout,否则 docker logs 看不到爬虫打印的 INFO 日志,排错时像在摸黑。PIP_NO_CACHE_DIR=1避免 pip 缓存把镜像撑大。先拷贝 requirements.txt 再装依赖,是为了利用 Docker 的层缓存,代码改动时不会重新下载依赖包。最后用useradd创建普通用户并切换,避免容器内以 root 身份跑爬虫,这也是生产环境的基本要求。
3.2 docker-compose.yml:最小编排与副本扩缩
镜像构建好之后,编排用 docker compose 就够。启动顺序是个关键问题,Worker 如果先于 Redis 启动,爬虫进程会在连接 Redis 时直接报错退出。这里不能只写depends_on,因为它只保证服务启动顺序,不保证 Redis 已经可以接受连接。正确做法是给 Redis 加 healthcheck,让 Worker 等 Redis 健康后再启动:
services: redis: image: redis:6.2-alpine command: ["redis-server", "--appendonly", "yes"] ports: - "6379:6379" healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 5s timeout: 3s retries: 10 worker: build: . command: ["scrapy", "crawl", "quotes"] environment: - REDIS_URL=redis://redis:6379/0 depends_on: redis: condition: service_healthy deploy: replicas: 3参数说明:Redis 开启--appendonly yes是为了让队列数据持久化到磁盘,容器重启不丢任务;健康检查用redis-cli ping,返回 PONG 才视为健康。Worker 侧的deploy.replicas是扩展点,从 3 个改成 10 个就是扩容。Compose 里environment传REDIS_URL而不是在代码里硬编码 localhost,是因为容器内访问 Redis 必须用 Compose 中的服务名redis,而不是127.0.0.1——这是容器网络和宿主机网络隔离导致的,很多人第一次跑通时最容易在这卡住。
3.3 一键拉起完整集群:项目化目录结构
如果直接复制上面的 Compose 文件到项目根目录,跑起来没问题,但维护会越来越难受。我一般会按资料包里常见的标准结构组织目录,让代码、配置、部署文件互相不干扰:
spider_project/ ├── docker-compose.yml ├── Dockerfile ├── .env ├── requirements.txt └── spider/ ├── settings.py ├── spiders/ │ └── quotes.py └── items.py.env文件放可变参数,比如 Worker 副本数和 Redis 密码,Compose 里用${WORKER_REPLICAS:-3}读取。启动命令就一行:
docker compose up -d --scale worker=5这条命令的参数含义是:-d后台运行,--scale worker=5覆盖 Compose 文件里的replicas值,临时把 Worker 扩到 5 个。结束任务时docker compose down会连带删除容器,但 Redis 里排队中的 URL 因为 appendonly 持久化还在,下次启动会接着跑,这正是分布式爬虫比单机脚本强的地方——重启不怕丢任务。
4. scrapy-redis 配置与四个必调参数:把单机爬虫改成分发模式
4.1 先理解三件套:去重器、调度器、队列
把单机爬虫改成分发模式,核心工作不是改爬虫代码,而是替换 Scrapy 内部的三个组件。去重器默认存在内存里,请求指纹进一个 Python set,进程一结束全没了。调度器默认从内存队列取请求,多进程抢不到同一个队列。队列本身默认不跨进程,每个爬虫实例各排各的。
Scrapy-Redis 做的事情就是给这三个组件各找一个 Redis 后端。去重器写入 Redis 的 Set,调度器从 Redis 的 List 弹任务,请求对象被序列化后存进 Redis。改造之后,Spider 的start_urls不再直接进入调度器,而是先被 push 到 Redis 队列,再由 Worker 容器各取所需。这就是为什么用这个方案时,爬虫里start_urls经常是空列表——种子 URL 是外部推入 Redis 的。
4.2 settings.py 完整配置与每一项的含义
以下是一份能直接用于生产的最小配置,写在项目的settings.py里:
SCHEDULER = "scrapy_redis.scheduler.Scheduler" DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter" SCHEDULER_PERSIST = True SCHEDULER_FLUSH_ON_START = False REDIS_URL = os.getenv("REDIS_URL", "redis://127.0.0.1:6379/0") REDIS_ENCODING = "utf-8" SCHEDULER_QUEUE_KEY = "%(spider)s:requests" SCHEDULER_DUPEFILTER_KEY = "%(spider)s:dupefilter" CONCURRENT_REQUESTS = 16 DOWNLOAD_DELAY = 0.5配置说明:SCHEDULER_PERSIST设为 True 后,爬虫结束时请求队列和去重集合会保留在 Redis 里,下次启动继续。SCHEDULER_FLUSH_ON_START如果设为 True,则每次启动先清空队列和去重集,适合调试时强制重跑;生产环境必须保持 False,否则一次误触发会把全部进度清空。队列和去重集合的 key 用%(spider)s占位,意思是每个爬虫自动按名字隔离,不同爬虫之间互不干扰。
4.3 四个必调参数:并发数、延迟、超时、去重持久化
配置能跑之后,真正决定分布式爬虫吞吐量和稳定性的参数只有四个。第一个是CONCURRENT_REQUESTS,它控制单个 Worker 同时发起的请求数。单机 16 够用,但如果开了 5 个 Worker,总并发就是 80,目标站点的压力也跟着放大,容易被封。第二个是DOWNLOAD_DELAY,请求间隔,单机设 0.5 秒没问题,分布式下要对所有 Worker 的总量做估算。第三个是DOWNLOAD_TIMEOUT,单机默认 180 秒太长,容器网络环境里建议调到 30 秒,避免某个慢请求长期占住并发槽位。第四个是 Redis 连接池大小,在REDIS_PARAMS里控制:
REDIS_PARAMS = { "socket_timeout": 30, "socket_connect_timeout": 30, "retry_on_timeout": True, "max_connections": 32, }参数说明:retry_on_timeout决定网络抖动时是重试还是直接报错,分布式环境里网络抖动是常态,设为 True 能显著降低偶发失败率。max_connections是 Worker 到 Redis 的最大连接数,每个 Worker 默认的 Scrapy 并发请求数是 16,连接池给到 32 已经留了一倍余量,再大反而浪费 Redis 的文件描述符。
4.4 数据出口不要走 Redis:拆一个独立落库消费者
一个容易踩的坑是把抓取结果也写进 Redis List,然后另开脚本批量消费。少量数据跑着没问题,但爬虫一天抓几十万条时,Redis 内存会持续增长,Persistence和AOF重写也会把 IO 拉高,最终影响队列性能。常见做法是让 Scrapy 的 Pipeline 直接把 Item 写入 MySQL 或 ClickHouse,Redis 只做任务协调不存业务数据。
如果目标存储压力大,可以在 Compose 里加一个独立消费者服务,从 Redis List 批量取 Item 再落库。这个服务的代码逻辑很简单:
while True: item = redis.blpop("items", timeout=30) if item: save_to_mysql(json.loads(item[1]))逻辑说明:blpop是阻塞式弹出,队列空时挂起等待而不是空转占 CPU,timeout 设为 30 秒是为了在队列长时间为空时能回头检查 Redis 连接是否还活着。这样做的价值是把抓取和落库解耦,Worker 只负责下载页面和提取字段,落库速度慢不会反过来拖住抓取进度。
5. 分布式爬虫避坑指南:五个让新手翻车的真实故障
5.1 镜像下载慢到怀疑人生:换源而不是等
现象:执行docker compose build时,pip install卡在下载 lxml 的进度条上,几分钟不动一次。
原因:默认 pip 源在境外,国内网络环境下大包下载非常不稳定,这不是 Docker 的问题,是软件源的问题。
解决:在 Dockerfile 里显式指定国内镜像源,构建参数传给 pip:
RUN pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple同理,基础镜像建议做一层加速。在/etc/docker/daemon.json里配置 registry mirror:
{ "registry-mirrors": ["https://docker.mirrors.ustc.edu.cn"] }注意改完要重启 Docker 服务。这两个操作能让整个构建时间从半小时压到五分钟以内,是提升效率最直观的一步。
5.2 Windows 上 Docker Desktop 起不来:检查 WSL2 而不是重装
现象:启动 Docker Desktop 直接弹窗报错,提示failed to connect to the docker api at npipe:////./pipe/dockerDesktopLinuxEngine,或者提示virtualization support not detected。
原因:Windows 跑 Docker 依赖 WSL2,而 WSL2 需要 CPU 虚拟化和 Hyper-V 被开启。报这个错时十有八九是 BIOS 里的虚拟化没开,或者 WSL2 内核没装,Docker Desktop 本身并没有坏。
解决:先到 BIOS 开启 CPU 虚拟化,然后以管理员身份执行bcd /set hypervisorlaunchtype auto,重启后再启动 Docker Desktop。如果还是报 npipe 连接失败,多半是 Docker Desktop 没真正启动成功,看右下角图标是否变成绿色鲸鱼,变不了就去 Windows 功能里勾选“适用于 Linux 的 Windows 子系统”和“虚拟机平台”,装完再试一次。这个坑的隐蔽点在于:很多人以为是 Docker 坏了去重装,实际是底层虚拟化没就绪。
5.3 容器间网络不通:别再用 127.0.0.1
现象:Worker 容器启动后日志报错,提示ConnectionError: Error 111 connecting to 127.0.0.1:6379. Connection refused。
原因:容器内的127.0.0.1指向容器自己,不是宿主机的 Redis。Scrapy 代码里配置的REDIS_URL如果写成了 localhost,在容器环境里必然连不上。
解决:在 Compose 里把环境变量设为服务名:
environment: - REDIS_URL=redis://redis:6379/0同样的问题也会出现在两个 Worker 容器之间互相访问的场景。用 Compose 管理时,容器间通信一律用服务名作为主机名,redis会被 Docker 内部 DNS 解析到 Redis 容器的 IP。原因本质是 Docker 默认的 bridge 网络提供了容器间 DNS 解析,但不映射宿主机 localhost,这是容器网络模型的基础行为,写配置时把这一点刻在脑子里能少翻很多次车。
5.4 任务队列积压到 Redis 内存爆炸:运行时加监控
现象:跑了大半天后,Redis 内存涨到好几 GB,Worker 抓取速度变慢,最后 OOM 被系统杀掉。
原因:种子 URL 一次性推入太多,或者站点反爬导致抓取失败重试,而 Worker 消费速度跟不上。本质上是指标不可见,不知道队列积压了。
解决:加两步动作。第一步是启动时用LLEN看队列深浅:
docker exec -it redis redis-cli LLEN quotes:requests第二步是 Compose 里给 Redis 加内存上限:
deploy: resources: limits: memory: 2G配合一个简单的定时脚本,每五分钟查一次队列长度,超过阈值就发告警。分布式爬虫不像单机脚本能一眼看出进度,队列深度就是唯一可信的健康指标,没有指标就是凭空猜,跑挂了才知道。
5.5 Redis 数据持久化和容器重启的顺序陷阱
现象:docker compose down之后重新up,发现去重集合还在,但请求队列清了,爬虫不再继续抓剩下的页面。
原因:这个现象有个很细微的时序问题——Redis 的 AOF 持久化是异步落盘的,容器被down强制停止时,最后几秒写入的命令可能还没落到磁盘。如果刚好把种子 URL 推入队列的操作发生在这几秒内,重启后队列就丢了,而去重集合因为写入时间早,已经被持久化。
解决:不要用docker compose down直接停,先执行 Redis 的BGSAVE命令把内存快照落盘,等SAVE完成再关容器:
docker exec -it redis redis-cli BGSAVE另一个相关建议是把SCHEDULER_PERSIST设为 True 时,部署脚本里增加一个停止前的安全等待逻辑,停 Redis 前先通过健康检查确认没有正在执行的写操作。这个坑看起来小,但一旦触发,爬虫会漏抓一批 URL 而且毫无察觉,比报错还难排查。
6. 验证分布式是否真在跑:用 Redis 里的 key 回答三个问题
分布式爬虫启动后,最尴尬的场景是看着多个 Worker 的日志都在刷输出,但不知道它们是各跑各的还是在共享同一个队列。验证方法不需要看代码,直接看 Redis 里的 key。先连进 Redis 容器:
docker exec -it redis redis-cli第一个问题:URL 队列在不在。执行KEYS *requests*,能看到quotes:requests这个 List,用LLEN quotes:requests看队列深度。第二个问题:去重集合在不在。执行SMEMBERS quotes:dupefilter,能看到所有 Scrapy 已处理的请求指纹,指纹是以 URL 为基础算出的 SHA-1 哈希。第三个问题也最关键:多个 Worker 共享同一个去重集合吗。在同一时间段抓一批页面,然后执行SCARD quotes:dupefilter看集合大小。如果是单机跑,集合增长速率和单个 Worker 的抓取速率正相关;如果分布式生效,集合增长速率应该约等于所有 Worker 的总抓取速率之和。简单说,只要所有 Worker 的请求指纹都写进同一个 key,分布式就成立。
这个验证方法还有一个高级玩法:抓取量翻三倍时对比去重效率和队列消费速度的关系,精确估算每个 Worker 的处理能力。假设队列以每分钟 1000 条的速度被消费,两个 Worker 都正常工作时,LLEN应该只增不减。当你把 Worker 从 2 个扩到 5 个时,如果队列深度快速下降,说明系统的瓶颈确实在网络请求或页面解析这层,扩 Worker 是有效手段;如果队列深度没变化,说明瓶颈在目标网站响应速度或 Redis 本身,再扩容也是浪费资源。
最后说个养成习惯:我会在爬虫项目的部署脚本里放一段十行左右的健康检查,把SCARD和LLEN的结果直接输出成监控指标,而不是每次都手动敲命令。亲身经历是,有一次 Redis 集群被运维重启之后,Worker 全连上新实例但队列是空的,没有这段检查的话根本发现不了任务已经悄悄断供。分布式系统里,肉眼可见的报错从来不是最可怕的,静默的空转才是。这个方案本身没有多深的技术门槛,做到能看见、可量化、出问题能定位,就值得投入了。希望帮到你。
本文还有配套的精品资源,点击获取