简介:一套基于Docker的分布式爬虫服务项目资源,以Go语言实现核心爬虫逻辑,并配合容器化部署方案,适合正在学习分布式系统、爬虫开发或容器编排的开发者,也可作为计算机相关专业课程设计、毕业设计或项目初期立项的参考蓝本。资源共11个文件,以Go源码为主,辅以Protocol Buffers协议定义、Dockerfile与构建脚本、Markdown/Text说明文档、架构示意图及项目授权信息,压缩包仅311KB,虽然体积不大,但目录涵盖服务端、客户端、单机爬虫、容器镜像构建脚本与协议文件,结构相对完整。目前已有54人学习下载,代码经测试运行成功,可直接用于二次开发或学习调试。通过这份资料,读者既能快速理解分布式爬虫的服务拆分与跨语言调用方式,也能借用现成的代码骨架部署一套可运行的容器化爬虫服务,再根据实际业务调整目标站点、解析规则与调度策略。
1. 基于 docker 的分布式爬虫服务,问题从来不在 docker 本身
把单机爬虫改成基于 docker 的分布式爬虫服务,最常翻车的往往不是爬虫框架,而是交付方式:脚本在自己电脑上跑得欢,一塞进容器就连不上 Redis;worker 一扩容,重复抓取量直接翻倍;宿主机重启一次,任务队列丢得干干净净。标题里那套“详细文档+资料齐全”的方案,核心就是先把调度、去重、抓取拆成三个容器角色,再用 Compose 编排、用 Redis 做中心队列。这篇笔记按这个思路从架构拆到参数,给新手一条能照做的落地路径,也给已经上手的人一份排错清单。适合写过单机爬虫、准备用容器交付抓取服务的团队。
2. 分布式爬虫服务:先把调度、去重、抓取拆成角色,再谈 Docker
很多资料包一上来就贴 Dockerfile 和 docker-compose.yml,忽略了一个前提:分布式爬虫服务先得是“服务”,然后才是“分布式”。单机爬虫里调度、去重、下载、解析全在一个进程,拆成容器之前,你必须先想清楚哪部分可以单独扩、哪部分必须共享状态。顺序反了,后面写出来的编排文件要么不敢重启,要么一扩就重复抓。
2.1 调度器是不是必须要?去重器放在哪?先分清三个角色
分布式爬虫的常规定义里,有三个角色经常混在一起讲:调度器、去重器、抓取器。我的理解比较简单——调度器负责从任务队列拿 URL、生成新的请求,并控制抓取频率和优先级;去重器负责判断一个 URL 是否已经处理过,避免重复抓取;抓取器负责真正下载页面、解析内容、把结果写回存储。
这三个角色里,只有抓取器适合无限扩容,因为它的工作天然可以并行。调度器最不适合多副本,两个调度器同时对同一个任务队列做分发,很容易出现任务重复或优先级错乱,除非你引入分布式锁。去重器则必须依赖一个中心状态,否则每个 worker 各记各的,等于没去重。
所以我一般会把去重器直接放在 Redis 里,用SET保存 URL 指纹,抓取前查一次、结果入库后再确认一次。调度器是一个独立进程,它只跟 Redis 对话;抓取器也是一组独立进程,同样只跟 Redis 对话。三个角色里,Redis 是唯一的“黑匣子状态”,其他容器都是无状态服务。这个设计决定了后续 Docker 编排会非常轻松:无状态服务可以随便重启、随便扩展,不用担心数据落在哪个容器里。
框架层面,常见的实现是 Scrapy + Scrapy-Redis。Scrapy 自带下载中间件和 Item Pipeline,Scrapy-Redis 把原来的内存队列、内存去重集合替换成 Redis 里的 List 和 Set,代码改动量很小。你拿到的资料包里如果只塞了一堆分布式爬虫脚本,却没有交代这三个角色分别在哪里,那大概率是一份“能跑但不是服务”的半成品。
2.2 单镜像多角色还是多镜像分工?三条选择标准
下一个选择是镜像粒度。是做同一个镜像、用启动命令区分角色,还是调度器、worker、依赖组件各打各的镜像?
同一个镜像配不同command,这是我个人最喜欢的起步方式。因为调度器和 worker 用的是同一套爬虫代码,只是入口参数不同,镜像只构建一次,tag 也只维护一个。发布新功能时,构建一次镜像,更新所有节点,逻辑一致性最好。缺点是调度器改了频控逻辑,worker 也得跟着重启,发布耦合比较紧。
拆分镜像则是调度器一个镜像、worker 另一个镜像,甚至下载器和解析器再分开。这种方式隔离更干净,worker 镜像里可以只装抓取依赖,调度器镜像不必带着 Scrapy 的下载中间件。但代价是构建次数翻倍、镜像命名和版本对应关系要花心思维护,CI 配置也会复杂不少。
我的选型标准就三条:团队几个人、发布多频繁、故障隔离要求多高。一到两人维护、一周发一次版,用单镜像完全够;超过五个人、调度和抓取分开迭代,再考虑拆分。不要因为“微服务听着正规”就强行拆镜像。分布式爬虫的难点在状态共享,不在镜像数量。
镜像内部还有一个容易忽略的参数:Python 基础镜像版本必须锁死。不要用python:latest,用python:3.11-slim这种具体 tag。爬虫依赖里 Requests、Scrapy、lxml 对 Python 小版本兼容性非常敏感,锁版本是成本最低的“后悔药”。另外.dockerignore里把.git、venv、__pycache__排除掉,否则构建上下文一传几百 MB,每次docker compose build都像在等一个世纪。
2.3 最小可用拓扑:Scheduler + Redis + Worker 的容器映射
网上很多文档会把 Kafka、Zookeeper、Celery 全塞进来,我认为最小可用拓扑就三个:Redis 作为中心状态,scheduler 作为生产者,worker 作为消费者。先把这个跑通,再按需引入其他组件。
为什么可以用 Redis 代替 Kafka?因为分布式爬虫对消息一致性的要求没有金融系统那么苛刻。URL 重复了,去重指纹兜底;任务丢了,断点重爬兜底。Redis 的 List 可以做 FIFO 队列,Set 做去重指纹库,ZSet 做优先级队列,覆盖爬虫绝大多数场景。等到单 Redis 真的扛不住写入压力时,再考虑上 Kafka,而不是一开始就背上一个重中间件。
容器映射关系具体是这样的:scheduler 容器只往 Redis 里写任务,不抓数据;worker 容器只消费 Redis 里的任务,下载解析后把结果写回存储;Redis 容器独立挂载持久化卷。scheduler 是单点,但它不存任何状态,restart: unless-stopped足够扛住意外退出。
另一个建议:不要第一版就上 Swarm 或 Kubernetes。把 Compose 玩透,再用--scale worker=N模拟多节点,观察 Redis 队列水位和 worker 资源变化,这套验证做完,你会很清楚瓶颈在哪;直接上 K8s 只会让“调度器多副本冲突”、“网络策略不通”这些基础问题掩盖真正的抓取性能问题。
3. 用 Docker Compose 编排分布式爬虫服务:最小配置与常用命令
标题既然是“基于 docker”,那么资料包里最核心的交付物就应该是 docker-compose.yml。Compose 的好处是一份 YAML 声明所有服务、网络、卷、重启策略,docker compose up -d一键起全套。比散落的 docker run 脚本靠谱太多,也比 Swarm 门槛低。这一章给出一份可以直接改来用的最小编排文件,并逐个说明参数含义。
3.1 写编排文件之前,先把镜像、端口、持久化这三件事定下来
不要一上来就写 YAML,先回答三个问题:镜像用什么、端口要不要暴露、数据怎么存。
镜像方面,Redis 用redis:7-alpine,体积小、内存占用低;爬虫服务用 Python 基础镜像自己构建。如果宿主机架构是 ARM,记得在 Dockerfile 里确认依赖 wheel 有没有对应架构,否则pip install会现场编译,慢到怀疑人生。
端口方面,Redis 在 Compose 网络内部只需要通过服务名redis访问,不需要映射到宿主机。除非你习惯用 RedisDesktopManager 看队列,那也建议映射到本机回环地址,127.0.0.1:16379:6379,不要直接暴露0.0.0.0,不然服务器上任何进程都能连你的 Redis,数据安全没有保障。
持久化方面,这是分布式爬虫最容易忽略的一环。Redis 容器默认可读可写,但容器一删,队列和指纹数据全没。所以数据目录必须挂载成 volume,并且 Redis 开启 AOF。这两件事要在 yml 里写清楚,而不是等容器重建之后再补救。
3.2 从零写 docker-compose.yml:调度器、Worker 共用一个镜像
下面这份配置是我常用的最小编排,调度器和 worker 共用同一个镜像,通过环境变量ROLE区分角色。这里用到了 YAML 锚点来避免环境变量重复书写,也方便以后统一调整。
version: "3.8" x-spider-env: &spider-env REDIS_HOST: redis REDIS_PORT: 6379 TZ: Asia/Shanghai services: redis: image: redis:7-alpine command: ["redis-server", "--appendonly", "yes", "--appendfsync", "everysec"] volumes: - redis_data:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 5s timeout: 3s retries: 10 scheduler: build: context: ./app dockerfile: Dockerfile image: spider-service:local environment: <<: *spider-env ROLE: scheduler depends_on: redis: condition: service_healthy restart: unless-stopped command: ["python", "main.py"] worker: image: spider-service:local environment: <<: *spider-env ROLE: worker SPIDER_NAME: product_detail DOWNLOAD_DELAY: "0.5" depends_on: redis: condition: service_healthy restart: unless-stopped command: ["python", "main.py"] volumes: redis_data:先看 Redis 这一段。command里开启了 AOF 持久化,--appendfsync everysec表示每秒刷盘一次,崩溃时最多丢一秒数据。这是分布式爬虫队列不丢任务的关键。healthcheck用redis-cli ping探测服务是否真正就绪,interval 是探测间隔 5 秒,timeout 是单次探测超时 3 秒,retries 是连续失败 10 次才判定不健康。
重点看depends_on。Compose 里默认的depends_on只控制启动顺序,不保证服务已就绪。如果 Redis 还没完成初始化,scheduler 就启动,爬虫入口大概率直接连不上 Redis。所以这里加了condition: service_healthy,意思是必须等 Redis 的健康检查通过,才启动调度器和 worker。这个写法在 Docker Compose v2 里才被完整支持,老版本的docker-compose可能会报condition不识别,建议升级到 compose 插件。
调度器和 worker 的关系上,scheduler 先构建镜像并打上spider-service:local标签,worker 直接复用这个标签,避免同一个上下文被重复构建两次。如果以后改了镜像名,记得两处同步改,否则 worker 会尝试从远程仓库拉取一个不存在的镜像。
3.3 启动、排错与扩容的常用命令说明
编排文件写好后,最常用的命令是下面这一组。我按使用频率排个序:
# 构建并后台启动所有服务 docker compose up -d --build # 查看服务状态,重点看 STATUS 是否 Up docker compose ps # 跟踪调度器和 worker 日志,-f 是 follow docker compose logs -f scheduler worker # 进到 worker 容器里测试 Redis 连通性 docker compose exec worker redis-cli -h redis ping # 把 worker 扩到 3 个副本 docker compose up -d --scale worker=3 # 停止服务但保留 volume,队列数据还在 docker compose down # 停止服务并删除 volume,队列数据会一起没,慎用 docker compose down -vup -d --build是第一次部署最稳的做法,先构建镜像再启动,避免用到旧镜像。logs -f scheduler worker可以同时跟多个服务的日志,比单独看一个容器方便得多。
扩容用--scale worker=3,这条命令会保持 Redis 和 scheduler 的数量不变,只把 worker 增加到 3 个。要注意,--scale是对当前状态的一次性调整,如果你扩容到 3 后又执行了一次docker compose up -d,它会保持 3 个副本而不是退回默认配置。想要恢复默认就再执行一次--scale worker=1。
compose down和down -v的区别必须刻在脑子里。down只停止并删除容器,命名卷redis_data仍然保留;down -v会连卷一起删。如果误操作了down -v,Redis 里的队列和指纹数据会全部丢失,没有后悔药。所以我建议把down -v的用法写进团队文档,标注为危险操作。
4. Docker 网络和 Redis 数据一致性:分布式爬虫最容易翻车的地方
分布式爬虫容器化之后,代码逻辑往往没变,变的只是网络边界和数据持久层。本章这两个点,几乎每个接入 Docker 的团队都会踩一遍。搞明白它们,比多看十份资料包都管用。
4.1 容器里的 localhost 不是宿主机,Redis 地址写错了半夜抓瞎
单机爬虫里写redis://127.0.0.1:6379/0完全没问题,进程就在同一台机器上。但放进容器后,127.0.0.1指向的是容器自己的回环网卡,不是宿主机,更不是你可能在宿主机上运行的 Redis。
爬虫进程一旦跑起来就报Error 111 connecting to redis:6379,或者连接被拒。我第一次遇到时查了很久的 Dockerfile,最后发现就是配置里写死了localhost。解决办法不是改容器内的网络模式,而是把 Redis 地址做成环境变量。
environment: REDIS_HOST: redis REDIS_PORT: 6379这里的redis就是 Compose 网络里的服务名,scheduler 和 worker 容器启动后,会自动解析到这个 Redis 容器的 IP。判断一个分布式爬虫方案是否成熟,看它的配置里有没有把 IP 抽成环境变量就够了。写死localhost的配置,换个环境就废,根本谈不上可移植。
检查环境变量是否生效,直接进容器看:
docker compose exec worker env | grep REDIS如果REDIS_HOST=redis已正确注入,但连接还是失败,再用docker compose exec worker ping redis看网络通不通。通的话问题在 Redis 认证,不通的话问题在自定义网络或防火墙。
4.2 用服务名和 Compose 默认网络,把三个容器连成一条链路
Compose 每次启动项目,都会自动创建一个默认网络,名字通常是<项目名>_default。所有服务不写network字段的话,默认都加入这个网络,彼此可以通过服务名直接通信。这是分布式爬虫服务能跑起来的前提。
万一你手动指定了网络,或者两个 Compose 项目想互相访问,常见做法是让多个 Compose 项目共享同一个外部网络。比如你要在本地起一个 Redis 调试容器,又想让爬虫服务连它,可以在爬虫的 compose 文件里定义一个外部网络:
networks: shared: external: name: spider_shared然后在每个服务下加networks: ["shared"]。外部网络必须提前创建:docker network create spider_shared。这套机制适合多项目联调,但分布式爬虫单项目内部不需要这么做,默认网络已经够用。
还有一个验证网络的好方法:进入 worker 容器,用getent hosts redis查看服务名的解析结果。如果返回一个 IP,说明网络打通;如果报找不到主机,检查服务名拼写,或者是不是把service_name写成了容器名。Compose 支持容器名别名,但默认主网段解析用的是服务名。
4.3 Redis 队列去重数据的持久化:AOF、挂载和崩溃恢复
Redis 默认的持久化策略是 RDB 快照,默认配置下可能几分钟才保存一次。对爬虫来说,几分钟的数据丢失意味着队列里几千条 URL 全没了,去重指纹也没了,重新上线后重复抓取一大片。所以必须开启 AOF,并挂载数据卷。
我在 3.2 的配置里已经写了--appendonly yes和--appendfsync everysec。前者开启 AOF,后者控制刷盘频率。结合宿主机上的 volume 挂载,Redis 数据目录/data会映射到命名卷redis_data,容器删除后数据仍保留。
验证持久化是否生效,可以用以下命令:
# 查看 Redis 持久化信息,aof_enabled 应为 1 docker compose exec redis redis-cli info persistence # 手动触发一次 AOF 重写,压缩日志体积 docker compose exec redis redis-cli bgrewriteaof # 备份 Redis 数据目录到宿主机 docker cp $(docker compose ps -q redis):/data ./redis_backup如果 Redis 日志提示 AOF 文件损坏,容器会一直启动失败。这时候不要急着删卷重建,先把数据目录备份出来,再用redis-check-aof --fix appendonly.aof修复。修复有风险,但总比直接清空队列强。生产环境更稳的做法是定期把数据目录打包备份到异地,或者用 Redis 主从复制加一个从节点,主节点挂了从节点能顶上去。
5. 常见问题与避坑:基于 docker 的分布式爬虫血泪经验
这一章是踩坑记录。每个问题我都按现象、原因、解决三部分写,方便对号入座。你拿到的资料包再好,也不如这五条经验来得实在。
5.1 worker 秒退,日志一直报 “Error 111 connecting redis:6379”
现象:docker compose up -d之后,worker 容器反复重启,docker compose logs worker里全是redis.exceptions.ConnectionError: Error 111 connecting redis:6379。
原因:绝大多数是 Redis 地址写成了127.0.0.1,容器里的回环地址不是宿主机。其次是depends_on没有加condition: service_healthy,Redis 还没就绪 worker 就抢先启动,第一次连接失败后进入重启循环。
解决:把环境变量里的 Redis 地址改成服务名redis,同时给 Redis 加健康检查。修改后重新构建并启动:
docker compose up -d --build --force-recreate如果还是不行,手动进 worker 容器执行redis-cli -h redis ping。能返回 PONG 说明配置没问题,可能是应用层读取配置有缓存;不能返回 PONG 就检查网络和 Redis 启动状态。
5.2 容器重启后新任务是全量重抓,队列去哪了
现象:docker compose restart或服务器重启后,爬虫任务从第一条开始重新执行,业务方收到的数据大量重复。
原因:Redis 没有开启 AOF,volume 也没有挂载,重启后队列和去重集合全部清空。另一个隐蔽原因是使用docker compose down -v清理过环境,卷数据被主动删除。
解决:在 compose 文件的 Redis 服务里加上--appendonly yes,并挂载redis_data:/data。改成这两项后,restart和down都不会丢数据。注意千万别把down -v写进自动化脚本,一旦执行,AOF 文件都没了,想恢复只能靠外部备份。
5.3 时间慢八小时,北京时间入库变 UTC,日期分表错乱
现象:爬虫抓到的数据入库时间比服务器本地时间慢 8 小时,按日期分表的场景里数据被扔进前一天的表,甚至会出现负数小时。
原因:大部分基础镜像默认时区是 UTC,而部署环境是北京时间。容器里date命令看到的时间比宿主机慢 8 小时,爬虫代码里的datetime.now()也跟着错。
解决:在公共环境变量里加TZ: Asia/Shanghai,scheduler 和 worker 都生效。如果你用的是 Debian 系镜像,光设 TZ 有时候不够,还需要安装tzdata并链接时区文件,否则 syslog 里时间还是 UTC。更彻底的做法是爬虫代码里所有时间统一用 UTC 计算和存储,展示层再转北京时间,这样容器跑在哪个时区都不受影响。
5.4 宿主机重启后服务一直 restarting,健康检查不通过
现象:服务器断电重启后,docker compose ps显示 Redis 或 worker 一直在 restarting,日志里看不到明显异常,healthcheck 始终不通过。
原因:常见原因有三个。Docker daemon 启动顺序和容器启动顺序冲突,重启后容器先于网络就绪;Redis 的 AOF 文件在非正常关机时损坏,恢复失败;宿主机内存不足,容器被调度器反复 OOM。
解决:先docker compose up -d强制拉起一次,看具体报错。如果 Redis 日志提示Bad file format,进入数据目录备份 AOF,再用redis-check-aof --fix修复。如果宿主机内存不够,检查docker stats,把 worker 的内存限制调高或减少副本数。服务追求自动恢复可以设置restart: unless-stopped,但重启不等于恢复,日志才是排错第一入口。
5.5 worker 进程突然消失,容器被 OOM Kill 了
现象:worker 日志没有任何异常,进程直接没了,docker compose ps显示容器退出码 137,docker inspect里OOMKilled: true。
原因:容器内存超限被内核杀死。爬虫解析大页面时,响应体、DOM 树、Item 全在内存里,内存峰值比平时翻两三倍很正常。如果你在 compose 里设置了deploy.resources.limits.memory,这个限制偏小就会触发 OOM。
解决:先用docker stats观察 worker 在抓取高峰期占多少内存,然后把限制设成峰值乘以 1.5。另一个技巧是给容器限制日志大小,Docker 默认日志驱动无限增长,日志文件也可能撑爆磁盘:
logging: driver: json-file options: max-size: "10m" max-file: "3"这段配置放到 worker 服务下,单份日志 10MB,保留 3 份,避免日志占满磁盘后间接拖垮容器。
6. 进阶:从“能跑”到“能扛”,用 Compose 验证分布式爬虫容量
分布式爬虫真正值钱的地方在容量验证,而不是“能跑通”。这一章提供一套不依赖重型监控组件的验证方法,用 Compose 自带的命令就能完成。
6.1 用 --scale 模拟多 Worker,看队列水位和资源变化
先把任务队列塞满,比如用 scheduler 批量生成一万条 URL,然后只开一个 worker,记录队列积压量。命令是:
watch -n 5 "docker compose exec redis redis-cli llen pending_url"llen pending_url返回队列长度,watch每 5 秒刷新一次。单 worker 时,队列水位下降速度就是单节点吞吐。接着扩容到 3 个 worker:
docker compose up -d --scale worker=3观察队列消费速度是否接近三倍。如果几乎没变,大概率卡在目标站反爬、DNS 解析或单机带宽上,这时候加容器没有意义。
6.2 验证容量与稳定性的几个指标和查法
| 指标 | 查法 | 预警值 | 说明 |
|---|---|---|---|
| 队列积压 | redis-cli llen pending_url | 持续增长 | 消费速度跟不上生产速度 |
| 内存占用 | docker stats | 超过限制 80% | 考虑扩容 worker 或优化解析逻辑 |
| 去重命中率 | 日志里Filtered off计数 | 超过 30% | 调度器把已抓 URL 重新入队 |
| 失败率 | 日志里HTTP 5xx计数 | 持续上升 | 目标站风控或代理质量下降 |
这个阶段不一定要上 Prometheus,先把以上四个指标用 cron 脚本记录一周,你会发现瓶颈一般不在容器本身,而在调度策略和存储写入。容器只是把问题暴露得更清楚了。
6.3 把调试参数留在 docker-compose.override.yml 里的复盘习惯
Compose 默认会读取docker-compose.yml和docker-compose.override.yml两份文件,后者覆盖前者。我习惯把本地调试参数单独放 override 文件:加端口映射、开 DEBUG 日志、限制 worker 副本数。这样生产配置始终干净,本地调试的临时代码改动不会误提交。
最后说一个我自己的教训:不要等到线上出问题才翻日志。分布式爬虫的每一次事故,几乎都能在之前的日志里找到征兆——某个 worker 内存涨了 20%、某个时间段失败率上升、队列一直不下降。养成每天看一眼 Docker 容器状态和 Redis 队列水位的习惯,比任何高深理论都有效。希望帮到你。
本文还有配套的精品资源,点击获取