ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Docker和Redis的Scrapy分布式爬虫架构实践

基于Docker和Redis的Scrapy分布式爬虫架构实践 简介这是一份基于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 而不是 Kubernetes2.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 基于 Debianapt 装依赖顺畅编译包时踩坑少得多。一个爬虫镜像里塞了 Scrapy、Requests、MySQL 客户端slim 版构建时间能比 alpine 少一半以上。Dockerfile 里有一个必须处理的细节时区。Scrapy 默认记录抓取时间用 UTC日志里差 8 小时排查问题时非常误导。建议在构建阶段直接把时区写死FROM python:3.9-slim ENV TZAsia/Shanghai \ PYTHONUNBUFFERED1 \ PIP_NO_CACHE_DIR1 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]逻辑说明PYTHONUNBUFFERED1强制 Python 日志实时输出到 stdout否则 docker logs 看不到爬虫打印的 INFO 日志排错时像在摸黑。PIP_NO_CACHE_DIR1避免 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_URLredis://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 worker5这条命令的参数含义是-d后台运行--scale worker5覆盖 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 或 ClickHouseRedis 只做任务协调不存业务数据。如果目标存储压力大可以在 Compose 里加一个独立消费者服务从 Redis List 批量取 Item 再落库。这个服务的代码逻辑很简单while True: item redis.blpop(items, timeout30) if item: save_to_mysql(json.loads(item[1]))逻辑说明blpop是阻塞式弹出队列空时挂起等待而不是空转占 CPUtimeout 设为 30 秒是为了在队列长时间为空时能回头检查 Redis 连接是否还活着。这样做的价值是把抓取和落库解耦Worker 只负责下载页面和提取字段落库速度慢不会反过来拖住抓取进度。5. 分布式爬虫避坑指南五个让新手翻车的真实故障5.1 镜像下载慢到怀疑人生换源而不是等现象执行docker compose build时pip install卡在下载 lxml 的进度条上几分钟不动一次。原因默认 pip 源在境外国内网络环境下大包下载非常不稳定这不是 Docker 的问题是软件源的问题。解决在 Dockerfile 里显式指定国内镜像源构建参数传给 pipRUN 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_URLredis://redis:6379/0同样的问题也会出现在两个 Worker 容器之间互相访问的场景。用 Compose 管理时容器间通信一律用服务名作为主机名redis会被 Docker 内部 DNS 解析到 Redis 容器的 IP。原因本质是 Docker 默认的 bridge 网络提供了容器间 DNS 解析但不映射宿主机 localhost这是容器网络模型的基础行为写配置时把这一点刻在脑子里能少翻很多次车。5.4 任务队列积压到 Redis 内存爆炸运行时加监控现象跑了大半天后Redis 内存涨到好几 GBWorker 抓取速度变慢最后 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 全连上新实例但队列是空的没有这段检查的话根本发现不了任务已经悄悄断供。分布式系统里肉眼可见的报错从来不是最可怕的静默的空转才是。这个方案本身没有多深的技术门槛做到能看见、可量化、出问题能定位就值得投入了。希望帮到你。本文还有配套的精品资源点击获取
返回列表