资讯动态

分布式爬虫调度器架构设计:基于Redis的任务队列与去重实战

发布时间:2026/9/13 7:30:57 来源:尧图企业网站定制
1. 为什么你要重新设计调度器单机爬虫的瓶颈与分布式诉求先聊个实际场景。我手里有一个比较典型的采集项目每天要从十几个目标站点抓取大约百万级的商品数据站点结构不统一有的要求高频轮询有的需要登录态维持还有的页面里混着不少重复链接。最早的时候我用单机 Scrapy 跑一个节点绑定一个 Redis任务队列直接挂在内存里。跑了一周问题开始集中爆发单机下载延迟升高去重占用的内存越来越夸张任务积压时队列无法横向扩容最难受的是——只要进程一重启队列里的未消费请求全部丢失断点续爬几乎是空谈。所以这个项目的核心矛盾不是“Scrapy 好不好用”而是“单机调度器的模型撑不起分布式场景”。于是我做了一个独立模块分布式爬虫调度器。它不是一个框架不是 scrapy-redis 的简单替换而是一个基于 Redis 任务管道、独立于 Scrapy 核心之外的调度架构。它负责统一管理所有爬虫节点的请求分配、去重、优先级排序、消费确认和断点续爬。这篇博文我会把这个调度器的架构设计完整拆开讲从模块划分、数据结构设计、调度策略、指纹去重、队列命名规范到实操中踩过的坑和排查思路。适合已经在用 Scrapy、准备上分布式、或者正在被任务队列搞到头大的爬虫工程师参考。如果你只是刚接触 Scrapy也能通过这篇内容理解调度器在整个爬虫系统里到底扮演什么角色。2. 架构设计的第一件事把调度器从爬虫进程里摘出来2.1 单机调度器的核心缺陷Scrapy 自带的调度器在单机场景下够用它维护一个优先级队列把 Request 对象保存在内存中同时用RFPDupeFilterRequest Fingerprint Duplicate Filter做去重。问题在分布式场景下非常致命调度队列存在每个爬虫进程内部多个节点之间无法共享队列去重集合只对本进程生效A 节点爬过的 URLB 节点不知道于是重复抓取率飙升进程重启队列和去重指纹全部清空。你可以把单机调度器理解成一个只有一位服务员的小餐馆菜单记在服务员脑子里。客人一多服务员记不住谁点了什么更别提两个分店之间共享同一份点单记录了。2.2 分布式调度器的架构定位我的核心设计思路是调度器从 Scrapy 引擎中完全剥离变成一个独立的逻辑模块通过 Redis 作为消息管道和状态存储。每个爬虫节点只负责“下载网页 解析数据”不再自己持有任务队列和去重集合。所有请求的入队、出队、去重、优先级排序统一由调度器模块完成。整个系统的模块划分如下模块职责存储/通信方式Scheduler 调度核心接收 Scrapy 引擎传入的 Request决定入队/丢弃/修改优先级Redis 有序集合、列表DupeFilter 去重器判断请求指纹是否已存在必要时做增量去重Redis Set / Redis HyperLogLog请求指纹生成将 URL、方法、请求体、关键头信息哈希为唯一指纹SHA-1 哈希任务队列按站点分队列存储待抓取请求支持优先级Redis Lists / Sorted Sets消费确认记录请求分发状态防止任务丢失Redis Hash 定时补偿爬虫节点从队列中拉取请求执行下载与解析把新请求重新入队Scrapy scrapy-redis 客户端这样的架构带来的最直接收益是扩容变成加机器的问题而不是改代码的问题。你新增几个爬虫节点只需要把它们指向同一个 Redis 实例调度器会自动把任务分发过去无需任何人工干预。3. 调度器工作流与核心数据结构设计这一部分重点讲调度器内部是怎么流转的以及每个模块背后的数据结构选型逻辑。我依照实际代码的调用顺序来拆解。3.1 请求入队流程指纹计算与优先级决策当一个 Request 从 Scrapy 引擎传入调度器时第一步不是入队而是计算指纹。我使用的是 Scrapy 官方兼容的指纹算法取请求的 URL规范化后、请求方法、请求体对 POST 请求body 参与指纹计算、关键请求头如Accept、Accept-Language等拼接后做 SHA-1 哈希。# 简化版指纹计算逻辑生产环境可直接使用 scrapy.utils.request.request_fingerprint import hashlib from scrapy.utils.request import request_fingerprint from scrapy.http import Request req Request(urlhttps://example.com/data?page1fromnav, methodGET) fp request_fingerprint(req) print(fp) # 输出形如1a7f24a1c6e0b8d1e6f2c9b0d8e7a6f5c4b3a2f1指纹生成后调度器拿着这个指纹去 Redis 的 Set 集合里查询如果指纹已存在说明该请求已经抓取过或已在队列中调度器直接返回丢弃这次入队请求。如果指纹不存在则先把指纹写入 Set占用一个待提交位再把请求序列化为字符串推入对应站点的任务队列。这里有一个在生产环境必须注意的细节入队和指纹写入必须保证原子性。我在项目初期遇到过一个问题——指纹写入成功但请求入队失败比如 Redis 连接超时导致这个 URL 再也不会被爬取。后来我把这个逻辑封装成一个基于 Redis Lua 脚本的原子操作彻底解决了这个“假去重”问题。-- 入队 指纹写入原子操作简化版 local fingerprint KEYS[1] local request_data ARGV[1] local queue_key ARGV[2] local dupe_key KEYS[2] if redis.call(sismember, dupe_key, fingerprint) 1 then return 0 -- 已存在丢弃 end redis.call(sadd, dupe_key, fingerprint) redis.call(rpush, queue_key, request_data) return 1 -- 入队成功3.2 任务队列设计按站点分队列还是全局单队列这是调度器架构里最值得权衡的地方。很多人一上来就搞全局单队列所有站点的请求混在一起用一个有序集合统一排序。但实际跑起来你会发现不同站点的抓取频率控制完全不同——A 站允许每秒 20 个请求B 站每秒只能 2 个C 站则要避开高峰期。如果全部消息混在一个队列里频率控制和调度策略互相污染页面获取失败后重试又会对队列产生扰动。我的做法是按站点 业务线双维度拆分成多个队列队列命名规则为{project}:{site}:{biz_type}:requests。例如product_scraper:site_a:detail:requestsproduct_scraper:site_b:list:requests每个队列独立控制优先级调度器出队时通过配置中心我用的是 Redis Hash 本地缓存读取每个队列的权重和频率限制再按权重比例轮询出队。这样某个站点出现问题比如验证码拦截时只影响它自己的队列其他站点的爬取完全不受牵连。3.3 优先级队列的实现Sorted Set 还是 Multiple Lists很多人会问为什么不直接用 Redis 的 Sorted Set有序集合来实现优先级队列把所有请求都丢进去分数score作为优先级出队时按分数取理论上可行但实践下来有几个坑Sorted Set 的成员是唯一的如果同一个 URL 因为不同参数要被重复抓取比如两个不同用户 ID 的详情页URL 不同但十分相似去重规则需要额外处理。Sorted Set 的 zrangebyscore 在大量成员下时间复杂度是 O(log(N)M)M 为返回条数。当队列积压百万级请求时出队操作的耗时和 CPU 占用会显著上升。多站点隔离性不好——不同站点的请求放在同一个 ZSet 里想单独控制某个站点的清理和重置非常麻烦。所以在我的架构里优先级用多级列表实现每个站点队列内部再拆成一个默认队列和 N 个高优队列用-high、-default、-low后缀标识。高优队列里的请求被消费完后才轮询默认队列。这个模型下正常页面走默认队列登录失败后重试、需要立即回调的页面走高优队列优先级清晰且可观测。# 调度器出队核心逻辑伪代码 def next_request(self, site: str): high_key f{site}:requests:high default_key f{site}:requests:default low_key f{site}:requests:low for key in [high_key, default_key, low_key]: # 从队列左侧弹出lpop 保证不阻塞 data self.redis.lpop(key) if data: return deserialize_request(data) return None4. 消费端逻辑与断点续爬分布式爬虫最容易被忽略的一环4.1 消费确认机制分布式环境下任务从队列里弹出和任务真正执行完成之间存在一个时间差。如果节点 A 从队列里弹出了一个请求然后节点 A 崩溃了这个请求就永久丢失了。我的设计是用 Redis Hash 记录“进行中”的任务请求出队时把请求的唯一标识指纹写入{project}:inflight这个 Hashvalue 为请求详情和过期时间。请求执行完成后从inflightHash 中删除该指纹。后台一个定时任务我用 Redis 的 scan 判断过期时间实现每隔一段时间扫描inflightHash把过期未完成的任务重新放回队列尾部。这个过程相当于给每个任务加了一个事务保护罩节点崩溃不会造成任务丢失只会延迟重试。实现起来不复杂但能显著提高整个系统的稳定性。4.2 断点续爬调度器重启后任务不丢断点续爬是调度器架构设计的核心价值之一。因为所有待抓取请求和去重指纹都放在 Redis 里爬虫节点和调度器进程重启后只需做一件事读取 Redis 中仍在队列中的请求重新建立本地状态就能继续从上次中断的位置跑下去。具体实现上每个爬虫节点启动时会从 Redis 获取当前节点的任务队列长度、inflight任务列表和被分配的可执行任务然后重新注册到调度器。调度器通过 Redis 的 Pub/Sub 广播节点上线/下线事件所有节点收到事件后重新均衡任务。这套机制保证即使在运行中动态扩容或缩容任务池也不会混乱。4.3 去重策略的升级从 Set 到 Bloom FilterURL 去重最朴素的做法是用 Redis Set每个请求指纹占一个成员。但当一个项目的请求量级过亿时Set 的内存占用会非常可观。我的项目爬取了大概三亿个 URL 后去重集合占用超过 2GB 内存成本不低。这里我引入了一个升级方案对“高频重复”的请求用 Set 精确去重对“长尾 URL”用 Redis bloomfilter布隆过滤器做概率去重。布隆过滤器有极小的误判率我配置为 0.001%但它对内存的占用比 Set 少一个数量级。在实际业务中长尾 URL 重复出现的概率很低少量误判带来的影响可以忽略不计。当然布隆过滤器有一个问题删除不方便。所以我的方案是“双写”策略——请求指纹先写入布隆过滤器只有布隆过滤器判断“肯定不存在”时才允许入队如果布隆过滤器判断“可能存在”则再查 Set 确认。这样既控制了内存增长又不丢失精确性。5. 调度器与动态页面、iframe 场景的协同用 Playwright 处理复杂抓取5.1 为什么爬虫调度器要关心动态页面很多读者会问调度器是管任务分发和队列的跟 Playwright 有什么关系实际业务中动态页面增加了一个非常重要的调度约束——渲染耗时不固定且并发能力远低于普通 HTTP 请求。如果调度器不了解下游是普通 HTTP 下载还是浏览器渲染就容易出现两类问题大量 Playwright 渲染任务被同时弹给一个节点节点的 CPU 和内存瞬间被打满。渲染超时的任务被当成失败重试反复压给同一个节点造成恶性循环。所以我在调度器里增加了“任务类型”字段并在出队时根据节点上报的能力是否支持浏览器渲染、渲染通道数做定向分发。5.2 动态 iframe 场景的一个实际案例真实项目里有一种非常恶心的页面目标内容藏在多层级 iframe 中且 iframe 的加载状态没有明确的 DOM 事件可以监听。这种情况下普通 HTTP 请求拿到的 HTML 里只有一个 iframe 占位符真正的内容在另一个 URL 里。我用 Playwright 处理这类页面时的标准做法是# 使用 Playwright 处理动态 iframe 页面 import asyncio from playwright.async_api import async_playwright async def fetch_dynamic_iframe(url: str) - str: async with async_playwright() as p: browser await p.chromium.launch(headlessTrue) page await browser.new_page() await page.goto(url, wait_untildomcontentloaded, timeout30000) # 等待 iframe 完全加载这是一个常见的坑wait_untilnetworkidle 在这里几乎是必需 await page.wait_for_selector(iframe, stateattached, timeout10000) # 获取 iframe 的 src 或 content frame_element page.frame_locator(iframe) content await frame_element.locator(body).inner_text(timeout10000) await browser.close() return content这段代码里值得留意的细节是wait_untildomcontentloaded而不是默认的load。很多动态页面主文档加载完成并不代表 iframe 内容加载完成domcontentloaded事件后 iframe 可能还在渲染中。我建议用wait_for_selector结合stateattached先确认 iframe 节点存在再去定位 iframe 内部内容比单纯等一个固定时间可靠得多。调度器对这类任务的调度策略是把动态页面请求标记为task_typebrowser控制单节点的浏览器并发数我这里限制为 4 个浏览器实例超过并发上限的任务自动在队列里等待直到有空闲渲染通道。这避免了每个节点同时拉起几十个 Chromium 造成的资源占用。6. 调度器的实现细节代码结构与核心类解析6.1 调度器主类设计我写了一个独立的调度器包与 Scrapy 核心引擎解耦。核心类RedisScheduler实现了 Scrapy 的Scheduler接口包括open、close、has_pending_requests、enqueue_request和next_request方法。下面是我的核心实现# redis_scheduler/core.py import json from scrapy.http import Request from scrapy.utils.request import request_fingerprint from redis import Redis, BlockingConnectionPool from .dupefilter import RedisDupeFilter from .queue import RedisPriorityQueue class RedisScheduler: 基于 Redis 的分布式调度器。 - 队列存储Redis List 多级优先级 - 去重指纹Redis Set Bloom Filter可选 - 任务状态追踪Redis Hashinflight 任务 def __init__(self, server: Redis, dupefilter: RedisDupeFilter, queue_key_template: str {project}:{site}:requests): self.server server self.dupefilter dupefilter self.queue_template queue_key_template self.inflight_key {project}:inflight def open(self, spider): self.project spider.name self.queue RedisPriorityQueue( serverself.server, queue_key_templateself.queue_template, projectself.project ) # 注册节点广播上线事件 self.server.publish(spider:events, json.dumps({ type: node_online, node: spider.settings.get(NODE_ID, unknown), project: self.project })) def close(self, reason): # 关闭时把 inflight 任务重新放回队列 self._requeue_inflight_tasks() self.server.publish(spider:events, json.dumps({ type: node_offline, node: self.server.get(node_id, unknown) })) def enqueue_request(self, request: Request, spider) - bool: fp request_fingerprint(request) if not self.dupefilter.request_seen(fp, request): # 入队 指纹写入Lua 原子操作 self.queue.push(request, fp) return True return False def next_request(self) - Request: request_data self.queue.pop() if request_data is None: return None # 记录 inflight防止崩溃丢任务 self._mark_inflight(request_data) return deserialize_request(request_data) def _mark_inflight(self, request_data: dict): key self.inflight_key.format(projectself.project) self.server.hset(key, request_data[fp], json.dumps(request_data)) self.server.expire(key, 3600 * 24) # 24小时保护期6.2 队列模块的优化延迟队列与优先级队列的实现我实现的RedisPriorityQueue内部其实不是一个独立的 Redis List而是一个按业务维度拆分的多列表结构。每个站点的requests队列再拆成三层优先级-high、-default、-low。正常解析出的新请求进默认队列特定条件下比如登录失效后重试手动指定priority10进高优队列。class RedisPriorityQueue: 多级优先队列。每个站点独立的小队列避免任务之间互相干扰。 出队时优先级高的队列先被消费保证关键数据优先抓取。 def __init__(self, server: Redis, queue_key_template: str, project: str): self.server server self.queue_key_template queue_key_template def push(self, request: Request, fp: str, site: str, priority: str default) - None: key self.queue_key_template.format(projectself.project, sitesite) f:{priority} # 将 Request 序列化为 dict并携带 fp 作为幂等键 data { url: request.url, method: request.method, body: request.body.decode(utf-8, errorsignore) if request.body else , meta: request.meta, fp: fp, } self.server.rpush(key, json.dumps(data)) def pop(self, site: str None) - dict | None: # 优先消费高优队列 high_key self.queue_key_template.format(projectself.project, sitesite) :high default_key self.queue_key_template.format(projectself.project, sitesite) :default low_key self.queue_key_template.format(projectself.project, sitesite) :low for key in [high_key, default_key, low_key]: data self.server.lpop(key) if data: return json.loads(data) return None6.3 去重模块的细节如何优雅处理 Scrapy 默认指纹的局限性Scrapy 自带的request_fingerprint默认只考虑 URL、请求方法和部分请求头。这在分布式场景下有个隐患如果两个节点对同一个 URL 发送了不同请求体比如加了不同的Referer指纹就会不同同一个页面可能被抓两次。所以我在指纹计算里把meta中的关键标识如site_id、biz_type也纳入指纹计算范围并做了缓存减少重复哈希带来的性能损耗。# dupefilter.py import time from scrapy.utils.request import request_fingerprint from redis import Redis class RedisDupeFilter: def __init__(self, server: Redis, dupe_key: str dupefilter): self.server server self.dupe_key dupe_key self._local_cache set() # 本地缓存近期指纹 def request_seen(self, fp: str, request) - bool: # 先查本地缓存 if fp in self._local_cache: return True # 查 Redis if self.server.sismember(self.dupe_key, fp): self._local_cache.add(fp) return True return False def add(self, fp: str): self.server.sadd(self.dupe_key, fp) self._local_cache.add(fp)7. 频率控制与调度策略这是分布式爬虫最见真功夫的地方7.1 按站点配置独立速率限制多站点采集时每个站点的容忍度不同。有的站点接口比较健壮每秒 50 个请求都没问题有的站点稍微频繁一点就触发滑块验证。调度器必须为每个站点独立设置速率限制。我实现了一个基于 Redis 令牌桶的限速器。# rate_limiter.py import time from redis import Redis class TokenBucketLimiter: 令牌桶限速器。每个站点一个桶capacity 为桶容量refill_rate 为每秒补充的令牌数。 def __init__(self, server: Redis, key: str, capacity: int, refill_rate: float): self.server server self.key key self.capacity capacity self.refill_rate refill_rate def _init_bucket(self): # 初始化桶记录桶容量和当前令牌数 if not self.server.exists(self.key): pipeline self.server.pipeline() pipeline.hset(self.key, tokens, self.capacity) pipeline.hset(self.key, last_refill, time.time()) pipeline.execute() def acquire(self, tokens: int 1) - bool: 尝试获取 tokens 个令牌返回是否获取成功。 使用 Lua 脚本保证原子性。 script local tokens tonumber(redis.call(hget, KEYS[1], tokens)) local last_refill tonumber(redis.call(hget, KEYS[1], last_refill)) local refill_rate tonumber(ARGV[1]) local capacity tonumber(ARGV[2]) local requested tonumber(ARGV[3]) local now tonumber(ARGV[4]) local elapsed now - last_refill tokens math.min(capacity, tokens elapsed * refill_rate) if tokens requested then redis.call(hset, KEYS[1], tokens, tokens - requested) redis.call(hset, KEYS[1], last_refill, now) return 1 else redis.call(hset, KEYS[1], tokens, tokens) redis.call(hset, KEYS[1], last_refill, now) return 0 end return bool(self.server.eval(script, 1, self.key, self.refill_rate, self.capacity, tokens, time.time()))这个限速器的工作方式每个站点对应一个令牌桶容量决定突发流量上限refill_rate决定长期稳定的抓取速率。调度器在调用next_request出队之前先检查该站点是否有足够令牌如果不够就跳过这个站点先处理其他站点的任务。这样整体爬取速率是自适应的不会因为某个站点限速而阻塞整个系统的运转。7.2 站点级权重轮询除了限速不同站点的请求优先级也不同。比如核心商品详情页的数据质量影响主流程权重设为 10而评论、推荐等非核心页面权重设为 2。调度器维护一个权重表在每个调度轮询周期里按照权重比例从不同站点的队列中出队。这个策略的好处是即使某个站点任务量巨大也不会饿死其他站点的任务。你可以把它理解成操作系统的多级反馈队列调度——高权重站点获得更多 CPU 时间片低权重站点的请求保证不被完全阻塞。8. 实战踩坑记录分布式调度器最常见的 6 个问题8.1 请求对象序列化丢失meta信息不完整Scrapy 的 Request 对象的meta字段里存储了下载中间件和处理逻辑需要共享的数据。入队时把 Request 序列化成 JSON 后meta里的非 JSON 类型比如函数、类实例、回调函数会直接报错或丢失。这是新手最容易踩的坑。我的解决方案是入队时只保留meta中可 JSON 序列化的字段其他对象在爬虫启动时通过注册表还原。具体做法是在调度器中维护一个meta_restore_funcs字典按meta[meta_type]恢复对应的对象。8.2 Redis 连接池耗尽导致调度器假死当爬虫节点数量增多我曾经最高加到 30 个节点每个节点都会与 Redis 建立大量连接。如果每个爬虫进程都开一个独立的 Redis 连接池默认的连接数上限很容易被打满。我在调度器初始化时统一使用BlockingConnectionPool并设置max_connections20关键操作使用 pipeline 批量提交大幅减少了 Redis 连接数。8.3 重复消费问题节点崩溃导致的重复抓取分布式环境下任务从弹出到完成之间节点崩溃会导致两个节点几乎同时拿到同一个任务。我的方案是引入 Redis 分布式锁请求出队后先尝试获取这个请求指纹的锁只有抢到锁的节点才能执行。如果节点崩溃锁通过expire自动释放任务再由补偿机制重新入队。8.4 序列化与反序列化的性能瓶颈最初我用 Python 的json.dumps序列化 Request 对象单机百万请求时性能勉强够用但当请求量达到千万级时JSON 序列化/反序列化成了 CPU 瓶颈。我后来改用msgpack进行二进制序列化性能提升了约 3 倍序列化后的大小也减少了一半以上。如果你也遇到类似瓶颈这是一个值得考虑的优化方向。8.5 优先级反转低优任务始终被饿死我在设计多级队列时默认队列和高优队列用的是同一个 Redis List。问题是当高优队列持续有新任务进入时低优队列永远不会被消费。后来我调整了策略高优队列出队 N 次后强制从默认队列出一个任务。这保证了系统不会因为优先级倾斜而完全忽略低优任务。8.6 任务堆积告警与监控调度器跑起来之后一定要做任务堆积告警。我通过 Redis 的 List 长度监听每 30 秒检查一次所有站点队列长度当队列长度超过阈值比如 10 万时通过 Webhook 推送到企业微信/钉钉群。另外还记录每个节点的消费速率、平均抓取延迟和成功率方便在任务积压时快速定位是调度器出问题还是单个节点能力下降。9. 调度器常见问题速查表现象可能原因排查与解决路径任务队列长度持续增长单一节点消费能力不足或出队策略阻塞检查该站点限速器是否生效增加节点调高权重同 URL 被抓多次指纹算法未覆盖请求体或请求头检查指纹生成逻辑将关键 header 或 body 纳入指纹计算节点重启后任务丢失inflight 保护机制未开启或过期时间过短确认 inflight Hash 写入延长过期时间开启补偿任务Redis 内存暴涨去重集合或队列积压过大引入布隆过滤器清理过期队列配置 Redis 内存淘汰策略调度器假死命令超时Redis 连接池耗尽或大 key 阻塞排查慢查询日志优化 pipeline增加 max_connectionsPlaywright 渲染任务占用内存过高多节点同时启动过多浏览器实例在调度器侧限制浏览器并发数量出队时做 capacity 预判任务延迟过高轮询间隔太长或高优任务过多缩短轮询间隔监控各站点队列长度针对性扩容10. 一个实际调度配置的参考模板最后分享一个我在项目里使用的调度器配置模板你可以根据自己的业务调整。它涵盖了队列命名、去重策略、限速参数、优先级规则和监控告警可以直接复制到你的settings.py或独立配置文件中。# 调度器配置参考 SCHEDULER redis_scheduler.core.RedisScheduler SCHEDULER_PERSIST True # 调度器持久化重启后恢复队列 DUPEFILTER_CLASS redis_scheduler.dupefilter.RedisDupeFilter DUPEFILTER_KEY product_scraper:dupefilter # Redis 连接参数按需调整 REDIS_HOST 127.0.0.1 REDIS_PORT 6379 REDIS_DB 2 SCHEDULER_QUEUE_KEY product_scraper:{site}:requests SCHEDULER_INFLIGHT_KEY product_scraper:inflight SCHEDULER_INFLIGHT_TTL 86400 # 站点级权重与限速配置 SITE_CONFIGS { site_a: { weight: 10, capacity: 20, refill_rate: 10, max_browser_instances: 4, # 动态页面专用参数 }, site_b: { weight: 2, capacity: 5, refill_rate: 1, max_browser_instances: 1, }, } # 监控与告警 SCHEDULER_MONITOR_INTERVAL 30 # 秒 SCHEDULER_QUEUE_ALERT_THRESHOLD 100000 SCHEDULER_ALERT_WEBHOOK https://hooks.example.com/alert这个模板里的核心是SITE_CONFIGS这个字典。每个站点的weight决定调度器在多个站点之间如何分配注意力capacity和refill_rate决定令牌桶的突发能力和持续速率max_browser_instances是给 Playwright 动态渲染任务用的并发限制。根据目标站点的实际承受能力去调整这几个参数整个爬虫系统的稳定性会上一个台阶。我从最初“调度器只是 Scrapy 内部一个不起眼的组件”的认知到后来把它当成整个分布式爬虫系统的核心中台这个过程走了不少弯路。最大的体会是调度器的设计一定要为业务场景服务不要为了分布式而分布式——如果你的任务量就几百万单机加个持久化队列完全够用当你真正遇到多节点协同、任务积压、断点续爬这些痛点时再引入分布式调度器也不迟。后续扩展的话这个调度器还可以往任务优先级动态调整、基于机器学习预测队列积压、多机房部署等方向继续优化但先把基础架构打稳比什么都重要。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价