这个标题看起来简单,真正上手才知道坑有多少。我自己的第一版爬虫是在单机上跑Scrapy,刚开始两周还觉得挺稳,后来业务方要求数据覆盖更多平台,单机跑十几个小时才抓一轮,中间任何一个节点抽风就全废。后来花了三周把项目改造成分布式架构,才彻底把“靠一台机器硬扛”变成“让机器们各干各的”。这篇文章就把我踩过的坑、重新梳理过的原理、以及最后整理出的可直接复用的方案完整写出来,给那些正准备把Scrapy项目从单机往分布式推一把的朋友。
1. 从单机到分布式:我为什么要把Scrapy改造成分布式
先说场景。我负责的某跨平台数据采集项目,起步阶段只需要抓几个垂直网站的公开信息,每天不到几万条数据,一台4核8G的云主机配好代理池,跑Scrapy单机其实完全够用。但后来需求变了:客户不再只要“某个分类”的数据,而是要全量覆盖、实时更新,外加历史数据回补。这时候单机方案有几道致命的坎:
- 单点故障:一台机器挂了,所有任务全部停摆,半夜还得爬起来重启。
- 扩展性差:加代理、加请求频率上限,能压榨的空间非常有限。
- 任务重复:多个爬虫实例同时跑同一个start_urls列表,如果不用分布式去重,同一个链接会被抓好几遍。
- 数据写入瓶颈:单机模式下Redis、数据库连接这种公共资源反而不会被抢,一旦多机并行,处理不好就会出现连接风暴、数据重复。
我当时的第一反应很简单:把同一份Scrapy代码复制到几台机器上,然后把每台机器的start_urls变成从Redis里读取。这个思路方向是对的,但实际落地的时候才发现,Scrapy自带的调度器、去重过滤器和Spider内部的请求队列,全都是进程内单机实现,你复制代码到十台机器,它们各自有各自的队列,互相之间根本不知情。想要真正共享同一个请求队列、共享同一个去重集合,必须把这三个核心组件改成“基于Redis的分布式实现”。
这就是整个改造工作的核心目标:
- 让所有爬虫节点共享同一个待抓取URL队列。
- 让所有爬虫节点共享同一个已抓取指纹集合。
- 让调度逻辑、请求分配逻辑尽量均匀、可靠。
如果你知道自己要解决的是这三件事,再去查资料写代码,定位就会非常清晰。我当时花了不少时间去看Scrapy源码和Scrapy-Redis这个第三方库的源码,发现它本质上干的事情就是这三件:把Scheduler里的queue和dupefilter替换成Redis版本,然后再换一个能通过Redis分配request的Spider父类。架构上并不神秘,难点全在配置、容错和对Scrapy生命周期细节的理解上。
1.1 先看清单机Scrapy的“隐形瓶颈”
很多人没意识到,Scrapy单机的核心瓶颈并不在CPU,而是在“等待”上。爬虫的本质是发起网络请求、等待响应、解析提取、再次发起请求的循环。网络IO在绝大多数时间里都是空闲等待状态,CPU占用率其实并不高。你去看那些跑得不好的爬虫,往往不是因为处理器不够,而是因为:
- 并发连接数被限制;
- 单机IP访问频率控制过于保守;
- 代理池质量不稳定,导致大量请求超时重试;
- 数据库写入速度跟不上抓取速度,积压大量item。
分布式改造并不是解决这些问题的银弹,它解决的是“一台机器无论如何也不可能同时拨出太多网络请求”这个物理上限。通过把请求队列放到了所有节点共享的边缘位置,把去重集合也移到了共享存储里,我们才能让多台机器同时从同一个队列拿任务、往同一个指纹集合丢任务,这才算真正把抓取能力横向扩展出去。
我的结论是:如果你的项目目前只有一两台机器、每天抓取量不到十万条,老老实实把单机调优做好可能更划算,分布式不是必须的。一旦日请求量突破百万级、机器数量达到三台以上,才值得引入分布式架构。否则维护成本反而会盖过收益。
1.2 哪些项目根本不需要分布式
这里顺便泼一盆冷水。如果你想抓的数据量也就是几百页,或者对实时性几乎没要求,或者目标网站的反爬主要是浏览器渲染,那分布式对你的意义极其有限。因为你的瓶颈可能根本不在请求排队,而在解析渲染、图色识别或者验证码处理的单机资源消耗上面。分布式解决的是“我要更多、更快地发请求”这个前提下,怎么组织机器并行处理的问题。如果连单机的性能都没有吃掉,先不要上分布式。
另外,如果目标网站的数据是登录后接口返回的,而且接口有严格的频率限制,那么哪怕你上了十台机器,也会因为共用同一批账号而被封停。分布式不会降低你的账号成本,反而会放大账号风险。这一点大家务必冷静评估。
2. Scrapy分布式改造的三大核心替换:队列、调度器与指纹集合
明确了要改造什么,我们来看技术选型。目前社区里最常用的方案是把Scrapy的几大核心组件替换为Redis实现,整体上形成“一队列、一指纹、多消费者”的模型。
2.1 Redis为什么是首选,而不是数据库或消息队列?
我见过有人直接把Scrapy调度器改成MySQL实现,表里放request队列,每次从表里取一条,然后另一个进程负责去重。说实话,在数据量小的时候也能跑,但是并发一高就全乱了。原因有两个:
- MySQL这种事务型数据库锁的开销很大,在高并发插入、删除的时候很容易拖慢整个调度链路。
- 请求这类数据天然适合放在基于内存的队列中,而不是持久化到SQL表里反复查询。
Redis的优势在于它本身就是内存数据库,操作延迟在微秒级,同时提供了我们需要的多种数据结构:List、Set、Hash、ZSet。其中List天然适合先进先出的请求队列,Set天然适合去重指纹存储,ZSet可以支持带优先级的调度。而且Redis本身是单线程模型,处理原子操作非常方便,多个Scrapy节点并发写同一个List,不会有并发覆盖或者脏读问题,这也是我最终选择Redis的核心原因。
2.2 核心替换点一:Scheduler
Scrapy的默认Scheduler是一个基于内存的PriorityQueue,专门维护Spider待处理请求的有序集合。单机模式下,它还负责在request入队之前查重,如果重复就不入队。在分布式场景下,我们需要替换为RedisScheduler,把“保存待处理请求”和“保存已处理指纹”这两件事全部搬进Redis。
具体实现上,Scrapy-Redis给出了一个非常清晰的方案:
SCHEDULER = "scrapy_redis.scheduler.Scheduler" DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"配置里加上这两行后,Scrapy的调度器和去重过滤器就会从Redis读取配置,所有request先经过Redis中的去重指纹判断,如果指纹不重复,则把request整理为字节串,push到Redis的List里。各节点通过同一个Spider从队列中pop出request,再交给下载器执行。这里的核心是:每个节点都在消费同一个队列,而不是各自维护各自的队列。
2.3 核心替换点二:DupeFilter去重
Scrapy单机的请求去重原理是:计算请求的指纹,存入进程内的一个set集合里。凡是已经在集合里的指纹,后续请求一律不再重复发送。这个指纹是一段SHA1哈希,由请求的method、url、请求体等字段计算得出。
分布式场景下,我们需要把set集合从每个进程的内存中搬到Redis的Set数据结构里,让所有节点共享同一个“是否已经访问过该URL”的判断集合。Scrapy-Redis实现的RFPDupeFilter,就是基于request_fingerprint函数得到指纹,然后执行Redis的SADD操作,如果返回0说明已经存在,于是丢弃该请求。
这里有三个容易踩的细节:
- 指纹计算只基于请求本身,和机器无关,也不包含代理IP、Cookie等运行态字段。也就是说同样的URL带上不同的Cookie,会被视为同一个请求。如果你需要“同一URL但不同Cookie要反复抓取”的场景,必须重写指纹计算函数。
- 指纹集合的键名在Scrapy-Redis中默认是
spider.name:dupefilter。如果你跑多个项目,一定要确保Key不会冲突,否则不同项目之间会互相去重,导致数据缺失。 - Redis的Set容量会随着URL数量线性增长。抓取量超过亿级时,需要定期清理过期指纹,或者在设计时使用带TTL的Redis替代方案。
2.4 核心替换点三:请求队列的分配策略
Scrapy-Redis默认使用的请求队列是FIFO队列(先进先出),对应Redis的List。List的lpush和brpop操作天然就能实现一个简单的分布式队列。但是实际使用中,我建议认真考虑一下ZSet作为队列底层数据结构的场景。
什么情况下要用ZSet?当你的爬虫任务有优先级时。比如普通全量采集和实时增量采集混合在同一个任务中,如果全都塞进同一个先进先出队列,实时任务会被大量全量任务阻塞。最高效的办法是给每个request指定一个优先级数字,让所有节点优先pop高优先级的请求。ZSet正好支持按score排序取出,Scrapy-Redis也提供了基于ZSet的PriorityQueue,只需要配置:
SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.PriorityQueue'这样每次从理论上说,高优先级请求会先被消费。但是注意,ZSet的pop操作不是阻塞式的,有可能出现队列为空时的高CPU空转。我在实际项目中会把PriorityQueue和阻塞队列结合,或者自己写一个优先级与阻塞并存的队列,保证节点即使没有请求待处理也不会疯狂轮询Redis。
3. 分布式爬虫Spider层设计:别再和单机一个思路
这一章的关键是,很多人改造分布式爬虫时,只改了配置,Spider代码却沿用单机的一套写法,跑到多机就会遇到各种奇怪问题。比如请求的绝对URL拼接、同一个start_urls被每个节点重复启动、item最终进入哪个数据库等问题。所以Spider层必须同步调整设计逻辑。
3.1 start_urls还要不要用?
如果你使用Scrapy-Redis,Spider父类通常要改成RedisSpider,它的作用并不是从一个静态的start_urls列表开始,而是从Redis中读取一个Key,这个Key里存着起始请求集合,所有节点共享同一份“起点”。配置方式如下:
from scrapy_redis.spiders import RedisSpider class MySpider(RedisSpider): name = 'my_spider' redis_key = 'my_spider:start_urls' def parse(self, response): # 业务逻辑 pass启动方式变成:先在Redis中lpush一个或多个起始URL,再启动任意数量的爬虫节点,节点会阻塞等待该Key的写入。这个模式的最大好处是:你可以先启动十台机器,再统一注入起始URL,确保所有节点同时开始;也可以随时往这个Key里增量注入新的起点,而不用重启任何一个节点。
实际运行中有一个小坑:如果你多台机器使用同一个Spider name,并且RedisKey没有区分业务模块,很可能A机器要抓的起点和B机器要抓的起点混在一起。我的经验是:要么为不同模块创建不同Spider name,要么在RedisKey上带上业务标签,比如my_spider:start_urls:module_a。
3.2 yield request之后的URL与回调参数处理
单机写爬虫的时候,我们经常会这样写:
yield scrapy.Request(url=next_url, callback=self.parse_detail)request被调度器放入内存队列时,所在的进程还在,没问题。分布式之后,请求会从A机器的Spider发出去,序列化到Redis,再由B机器的Scheduler拉出来交给B机器的Downloader,最后响应回调还是在B机器上触发。所以request里携带的meta信息必须可以被Python标准库序列化和反序列化,否则B机器根本拿不到相关的上下文。
常见问题就是往meta里放了不可序列化的对象,比如数据库连接、Spider实例、函数对象。解决办法是:meta里最好只放数字、字符串、字典这类基础类型;如果一定要传复杂数据,先转成字符串再传。踩过一次这个坑之后,我养成了习惯:yield Request之前,检查meta中的所有值是否都是JSON可序列化类型。
3.3 item派发与数据库写入的幂等设计
分布式爬虫到达item阶段的量通常非常大,如果你每一个item都直接开数据库连接去写,极大概率会出现连接数被打满、写入任务大量堆积的问题。比较好的实践是:把item推到Redis的另一个队列,由独立的数据消费进程去批量入库。
def parse_detail(self, response): item = MyItem() item['title'] = response.css('.title::text').get() item['url'] = response.url yield item默认Pipeline会把item直接交给数据库存储。为了减轻数据库压力,我通常会在Pipeline中把item写入Redis的某个List,再由一个常驻进程批量消费,攒够1000条或每隔5秒flush一次写入数据库。这样数据库写入频率大幅降低,批量插入也更快。
另外要注意:分布式环境下同一个item可能因为网络重试被采集两次,入库条数不能只看item数量。最保险的办法是给item增加一个唯一键,在数据库做唯一索引或者用Redis的Set记录已入库的item指纹,保证幂等。我在项目中就用item中的url_md5字段在MySQL里建了唯一索引,重复写入时直接忽略,省去了一堆麻烦。
4. 配置与部署:真实环境下的关键参数和集群调优
配置阶段是整个分布式改造里最容易“看着能跑、一多就挂”的地方。很多人改完配置在本地用两个爬虫进程测,发现没问题就上生产,结果生产环境Redis一忙,整个爬虫系统全线超时。这里我要详细讲几个部署有关的要点。
4.1 核心配置清单
一个典型的Scrapy-Redis分布式爬虫配置项,可以分为“Scrapy基础配置”和“Redis调度配置”两块。
# scrapy settings.py 节选 # 分布式调度 SCHEDER = "scrapy_redis.scheduler.Scheduler" DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter" SCHEDULER_PERSIST = True SCHEDULER_QUEUE_CLASS = "scrapy_redis.queue.FifoQueue" REDIS_HOST = "redis服务的地址" REDIS_PORT = 6379 REDIS_PASSWORD = "****" REDIS_ENCODING = "utf-8" # 可选:设置队列的Key前缀 REDIS_PARAMS = { "socket_timeout": 30, "socket_connect_timeout": 30, "retry_on_timeout": True, }这个SCHEDULER_PERSIST选项非常关键,表示爬虫被结束时,是否保留Redis队列和去重集合中的指纹。我建议生产环境设置为True,这样即使爬虫全部停止,重启后队列数据仍在,可以从断点继续抓,而不是从头抓一遍。
如果设置为False,默认会在Spider退出时清空队列和去重集合,它的意义只在于测试场景。我第一次改造就吃了这个亏,测试完没改回True,手一抖重启生产爬虫,队列丢了,所有页面从零开始抓,浪费了大量带宽和时间。
4.2 Redis连接配置需要单独考虑
Scrapy-Redis源码中默认使用redis.StrictRedis建立连接,如果你的Redis吞吐量很高,并发连接数会迅速增长。这时你至少要做三件事:
- 在Redis配置里把
maxclients调高到10000以上(单机默认通常是10000,够用,但如果Redis本身也跑在服务上要注意)。 - 在Scrapy的
REDIS_PARAMS中设置socket_timeout和connect_timeout,避免某个Redis操作阻塞住整个下载线程。 - 用单独的Redis实例或至少单独数据库编号(db)来跑爬虫队列,别和业务缓存混在一起,防止缓存键删除时误删爬虫队列。
还有一点容易被忽略:Distributed模式下,Scrapy各个节点的系统时间如果不同步,请求的时间戳和日志顺序会非常混乱。建议在每台机器上配置好NTP时间同步,尤其是当你用ZSet做优先级队列、依赖score排序时,时间不同步会导致排序错乱。
4.3 集群部署时进程与容器的配置建议
我在生产环境用的是Docker容器部署,每台物理机或虚拟机上跑若干爬虫容器。Docker部署有个好处是可以快速扩缩容,扩容时直接启动新容器即可,缩容时要留意优雅退出,避免正在处理的请求中断。
Docker部署的几个注意事项:
- 容器内进程数尽量保持为1个scrapy crawl进程,不要在一个容器里用supervisor拉起多个爬虫,否则健康检查会混乱。
- Docker容器的资源隔离不要设置得太低,Scrapy下载器和解析器需要稳定的内存,建议至少2G内存起步。
- 每个容器里设置相同的环境变量,比如Redis地址、Proxy服务地址,避免环境的“幽灵差异”。
如果你不想用Docker,也可以用systemd管理Scrapy进程,但每台机器的多进程管理会比较繁琐。我的倾向是:只要涉及三台以上的机器,直接上容器编排,哪怕只是docker-compose,也比手动管理靠谱得多。
4.4 爬虫节点的水平扩展策略
这里说一下扩展步长的问题。我自己的经验是,先启动两台节点跑通整个链路,看Redis的QPS、各节点的下载延迟、代理池的反馈情况,然后以2的幂数逐步扩容。一次增加到10台以上,很容易出现代理池被打满、目标网站整体限流、Redis网络带宽饱和等问题。这些都不是爬虫框架本身能通过配置解决的。
另外要注意,Scrapy的并发度需要通过CONCURRENT_REQUESTS参数控制。分布式集群里,每个节点各发各的请求,如果你一台节点配置100并发,十台就是1000并发。目标网站的承受力不一定适配这个值。我通常会在总目标并发可控的前提下,把单机并发调低,比如单机30~50,然后用节点数来调整总并发。这样更灵活,也更容易精准控制封禁风险。
4.5 代理IP池集成与限速
很多需要分布式爬虫的场景,目标网站都有自己的风控。项目里最稳的做法是自建代理池,把可用代理放进Redis的ZSet里,按响应耗时排序。Scaly下载中间件每次从代理池里取一个代理,如果请求失败就把分数降低;成功就提高分数。这样经过一段时间的自动洗牌,最终稳定下来的都是响应快且成功率高的代理。
写一个简单的代理中间件思路:
class RandomProxyMiddleware: def process_request(self, request, spider): request.meta['proxy'] = get_random_proxy_from_redis()真正上线的时候要注意,代理IP是有生命周期的,尤其是免费代理和短效隧道代理。你需要在代理池里定期清理失效代理,不能只靠评分降级。我在分布式场景下还会注意:同一个代理同一时刻不要分配给两个节点去抓同一个目标站,否则可能因为并发连接过多而被封。解决方法是给代理IP加一个“正在使用”的标记,在Redis里设置一个带过期时间的锁,过期后自动释放。
限速方面,我建议用DOWNLOAD_DELAY和AUTOTHROTTLE_ENABLED参数做一个基础防线,同时根据每一路代理的历史表现动态调整并发。分布式下,限速是全集群的问题,不能只考虑单节点。比如所有节点共用一个总目标频率控制器,可以通过Redis记录每个目标域名每秒的请求量,超过阈值就让中间件睡眠。这个逻辑听起来复杂,其实也就是从Redis取一个key,用INCR和EXPIRE统计每秒请求次数。
5. 数据去重、队列持久化与任务断点恢复
分布式爬虫运行几天后,你大概率会遇到队列堆积、重复抓取、断电恢复等问题。这里重点展开我实际用过且效果不错的处理思路。
5.1 去重指纹的扩展与自定义
默认的RFPDupeFilter只考虑请求本身的method和url。如果你的爬虫会用同一条URL,但不同查询参数代表不同页面,可能会被错误去重。比如一个网页通过post参数翻页,很多爬虫会把它拼到URL里变成query。多数情况下没问题,但遇到特殊请求体,必须重写指纹函数。
自定义指纹有两种途径:一是重写scrapy.utils.request.request_fingerprint函数,在项目里统一替换;二是自己实现一个DupeFilter类,继承RFPDupeFilter后重写request_seen方法。
我实际的做法是把request_fingerprint的源码复制一份,在计算前加入项目自定义字段,比如request.meta.get('use_cookie')之类的标识。注意,如果你修改了指纹函数的实现,所有节点的代码必须保持一致,否则不同节点计算出来的指纹值不一致,会产生大量重复请求。
5.2 队列堆积与慢消费者问题
分布式爬虫最常出现的运行状态是“请求队列只增不减”,也就是生产请求的速度大于消费速度。原因通常是解析阶段生成了一批新的URL,而这些URL生成的速度快过网络下载的速度,队列越来越长。
此时不要盲目加节点。先检查一下队列堆积的方向:是未抓请求堆积,还是待入库item堆积,还是待写入数据库的item堆积?分别对应三个日志指标。如果是未抓请求堆积,说明请求生成量远超下载能力,可以尝试降低生成速度(不要疯狂yield新的链接),或者增加节点。如果是入库堆积,说明数据库写入慢,这时加爬虫节点只会让入库更拥堵,应该增加深度优先的批处理入库进程。
还要注意,Redis的List如果堆积了上千万个request,会占用好几GB内存。建议给Redis队列设置一个最大长度,超过阈值后Spider的请求生产逻辑自动暂停,防止内存被打爆。
5.3 断点恢复的完整流程
分布式爬虫最怕的是Redis数据丢失和代码部署导致的中断。SCHEDULER_PERSIST=True只能保证不主动清理,但如果Redis实例持久化配置不当,或者没开AOF,宕机恢复后队列和去重集合可能都会丢。
我建议生产环境至少给Redis开启AOF和RDB双重持久化,并且定期备份。在恢复流程上,我们的做法是:
- 先把全部爬虫节点停止,避免继续写入旧的队列。
- 检查Redis中各个Spider对应的队列长度和指纹数量。
- 启动一个最小的测试节点,确认它能从现有队列中正常取任务。
- 逐步扩容所有节点。
另外,如果遇到队列完全丢失的情况,可以用事先备份好的start_urls结合去重指纹补录起点。不过更好的方式是,每轮抓取完成后把已完成的任务ID记录到独立的数据库表,下次重跑时直接用该表重建队列。也就是说,Redis队列是“运行态”,数据库里的任务表是“持久态”。做到这一点,断点恢复的压力就小多了。
6. 监控报警与运维实战:没有监控的分布式爬虫迟早出事
分布式爬虫只是一个工具,真正让它稳定运行的其实是监控和运维体系。我见过太多项目,上线的时候各种信誓旦旦,结果某天凌晨Redis内存爆了,所有节点报错,业务方第二天才发现数据没更新。所以文章中最后我想重点聊聊如何把分布式爬虫“管起来”。
6.1 必须采集的核心指标
我会在每个节点上采集如下几个维度的指标:
| 指标分类 | 具体项 | 采集方式 |
|---|---|---|
| 请求层 | 每分钟请求数、平均响应耗时、下载错误率 | 通过Scrapy StatsCollector统计,写入监控系统 |
| 队列层 | Redis中每个Spider队列长度、指纹数量、内存使用 | 定时脚本读取Redis INFO |
| 任务层 | 抓取成功的item数量、入库item数量 | 通过Pipeline日志和入库回调统计 |
| 节点层 | CPU、内存、网络IO、进程存活状态 | 用系统监控工具/容器监控工具 |
其中队列长度是最直接的“水位线”,我习惯给每个Spider队列设置两个阈值:当长度超过100万时执行告警,超过500万时自动暂停部分节点的请求产出,防止数据堆积导致Redis内存爆炸。
6.2 日志该怎么设计和采集
Scrapy的默认日志会输出到控制台,在分布式集群里根本没法查。我的做法是使用loguru或标准logging模块,把每个节点的日志按Spider名和日期输出到文件,同时通过日志采集组件集中收集,方便按时间倒序检索。
日志内容要包含必要的关键信息:当前进程ID、Spider名称、请求URL、耗时、状态码、代理IP、队列长度。特别是请求耗时和代理IP,这两个字段能直接判断当前的代理质量。日志字段统一之后,排查问题会快很多。
6.3 常用故障场景与排查思路
我把分布式爬虫常见的故障整理成一个简单的排查顺序表,分享给大家:
| 现象 | 可能原因 | 排查顺序 |
|---|---|---|
| 所有节点没有请求在跑 | Redis队列没数据 / 阻塞等待 | 查看Redis队列长度 |
| 单个节点卡死 | Redis连接超时 / 下载器线程阻塞 | 查看日志和连接数 |
| 数据大量重复 | 指纹计算不一致 / 去重Key冲突 | 检查指纹函数和Redis Key |
| 入库特别慢 | 数据库连接不足 / item队列堆积 | 查看数据库慢日志和item队列长度 |
| 内存暴涨 | 队列堆积 / 爬虫解析中数据异常 | 查看Redis内存和节点堆内存 |
其中“所有节点没有请求在跑”最容易迷惑人。如果你发现日志里一直显示Got request from Redis...,但队列长度不为0,那大概率是某些请求的URL格式错误,导致下载器一直异常。这时候需要单独把那些异常URL打印出来看,尤其注意URL中的中文编码和特殊字符。
还有一个隐蔽问题是“僵尸节点”。某台机器因为网络分区或者代理失效,仍然在跑,但实际已经很久没有成功请求了。监控里如果不看“节点最近一次成功请求的时间”,这个问题很难暴露。我会在监控面板上加一个模块:各个节点的过期时间,超过5分钟没有成功请求就高亮报警。
7. 最终方案与一点个人经验
整套方案跑下来之后,我的分布式爬虫架构变成这样:
- 起点:Redis
key:start_urls,所有节点共享。 - 调度:Scrapy-Redis调度器,基于Redis的FIFO或PriorityQueue。
- 去重:RFPDupeFilter加自定义指纹函数。
- 下载:下载中间件从Redis代理池取代理,自动维护代理质量评分。
- 数据处理:每个item先进入Redis item队列,再由独立消费者批量入库,入库幂等靠唯一索引。
- 监控:节点指标、队列水位、日志集中采集,设置阈值告警。
单机改分布式,最怕的是“表面分布式、逻辑还是单机”。只要队列、指纹、配置、运维四条线都真正分布式了,整个系统才算站稳。我在推进这个项目的过程中,最大的体会是:技术方案本身门槛不高,真正耗时间的全在边界细节上。比如一个request的meta数据没有序列化好、指纹函数不一致、Redis持久化配置没跟上,这些看起来都只是小问题,但它们凑在一起就能让一个晚上变成通宵排障。
最后分享一个实用小技巧:上线新版本的时候,不要一次性把所有节点都替换。先让一台机器用新代码跑,其他机器保持旧版本,观察新旧两个节点抓到的数据是否一致、请求频率是否有差异,确认没问题后再逐步全量发布。这个策略帮我避开了好几次导致数据混乱的发布事故,你们可以试试。分布式爬虫本质上是一条流水线,任何一环出问题都会放大到整条链路上,所以“稳”永远比“快”重要。