这两年做大模型训练数据集的同学越来越多,大家一开始都会把精力放在清洗、标注、去重这些环节上,但其实藏在水面底下的数据采集工程,往往才是决定一个项目能不能按时交付的关键。我最近刚把手头一个千万级网页语料采集系统做完一轮重构,核心改动就两个字:调度。更准确地讲,是把动态IP资源池和高并发采集调度揉在一起,形成了一条能持续稳定产出数据的流水线。今天就把这套系统从设计到落地的完整思路拆开讲清楚,重点说动态IP怎么管、高并发怎么调度、系统稳定性又是怎么被一点点保住的。
如果你正在做LLM训练数据采集、垂直领域数据构建,或者只是维护一个日请求量比较大的分布式爬虫系统,这篇文章里的思路应该可以直接抄。咱们不聊虚的,直接看问题和解法。
1. 大模型数据采集的核心挑战:规模、时效与稳定性
1.1 语料采集的业务特性:不只是“爬得快”这么简单
大模型训练需要的数据量级,往往超出常规爬虫项目的想象。通用语料动辄几亿个网页,垂直领域数据也得百万级起步。我这边维护的采集系统,目标是给预训练项目持续供给网页正文、知识库文档和结构化表格数据,日均新增数据量在TB级别。这种量级下,单机脚本早就撑不住了,必须上分布式采集架构。
但比“爬得多”更麻烦的是时效性。很多数据源是实时更新的,比如新闻资讯、商品价格、行业报告,如果采集任务调度不及时,拿到手的很可能是一堆过期页面,清洗完之后价值大打折扣。这就要求调度器不仅要快,还得能按照数据源的更新频率自动安排采集节奏,该凌晨跑的不会拖到中午。
另外还有合规和数据质量的问题。大模型训练最怕脏数据,一个页面编码识别错误、HTML解析残缺、正文抽取混入导航栏内容,都会直接影响下游模型效果。采集系统需要在入口处就把数据质量卡住,而不是把所有脏活累活都丢给清洗模块。这些业务特性叠加在一起,就让“数据采集”这件事从纯技术活变成了一个系统工程。
1.2 稳定性差的根因:三个“不可控”叠加
做采集系统最痛苦的不是功能写不出来,而是系统跑着跑着就崩了,而且每次崩的原因都不同。复盘下来,稳定性问题几乎都源于三个不可控因素。
第一个是目标网站的不可控。对方服务器随时可能调整响应速度、变更页面结构、增加风控策略,你以为昨天还好好的接口,今天就被限流甚至封禁了。第二个是网络环境的不可控,机房出口IP被对方识别为数据中心流量,或者某个IP段的访问频率触发了对方的告警阈值。第三个是采集任务本身的不可控,URL列表的返回结果分布极不均匀,有的网站只有几十个页面,有的网站几百万个URL,任务时长差异巨大,很容易出现某些Worker忙死、另一些闲死的情况。
这三个不可控因素叠加,就会形成一种典型故障:某几个IP因为请求过快被对方封禁,导致该IP上的所有任务变成超时重试,重试又产生大量无效请求,进一步加重封禁,最终系统进入一种“越跑越差”的恶性循环。我在项目初期就吃过这个大亏,当时采集成功率一度掉到60%以下,排查了三天才发现是IP调度策略太粗糙,没有根据实时反馈动态调整。
1.3 衡量稳定性的关键指标:采集成功率与任务完成率
说了这么多问题,那系统稳定性到底怎么量化?落地的时候我主要盯两个指标。
一个是采集成功率,也就是成功获取到有效内容的请求数占总请求数的比例。正常情况下这个值应该在95%以上,低于90%基本就能判断系统出问题了。另一个是任务完成率,即当天计划采集的URL中,实际在规定时间内完成并入库的比例。这个指标反映的是调度系统的吞吐和时效,任务堆积越严重,完成率越低。
这两个指标不是孤立的,采集成功率下降必然导致重试增多,重试增多又占用调度资源,最终拖累任务完成率。所以我在重构时所有优化动作都围绕这两个指标做验证,任何一个改动上线前都要先评估它对这两个数字的影响。系统的稳定性,本质上就是靠这些量化指标在支撑判断,而不是靠感觉。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 动态IP资源池设计:稳定性的基础底座
2.1 为什么动态IP能提升采集稳定性
很多人对动态IP的第一反应是“为了隐藏自己”,但站在系统稳定性的角度,它的核心价值其实是为调度系统提供了冗余和容错空间。
想象一下,如果你只有10个IP,每个IP的并发上限是50,那整个系统的最大并发就是500,任何一个IP被封都会直接损失10%的容量。而如果你有一个容量500的IP资源池,就算同时挂了10个IP,容量损失也只有2%,系统照常运转。这就是资源池的冗余价值。
更重要的是,动态IP让调度器有了“换路”的能力。当某个IP连续出现超时、被限制访问的迹象时,调度器可以立刻把该IP上的流量切换到其他健康IP上,而不是等任务失败后再重试。这种主动规避机制,比出了故障再去处理的响应速度要快得多,也是提高采集成功率最直接的手段。
2.2 资源池分层:热池、温池与冷池
IP资源池不是简简单单一个列表,真要支撑高并发,必须分层管理。我用的方案是三层结构:热池、温池和冷池。
热池存放的是当前状态最好、响应速度最快、剩余配额充足的IP,日常的绝大部分流量都从这里分配。温池是备用池,存放那些状态正常但近期使用频率略高的IP,当热池压力过大时启用。冷池则存放新接入的IP或者刚从封禁中恢复的IP,需要经过一段时间的低负载验证,确认稳定后才能升入热池。
这个分层机制解决了一个很实际的问题:有些IP服务商提供的IP质量参差不齐,如果统一放在一个池子里轮询,质量差的IP会被频繁分配到任务,导致整体成功率被拉低。分层之后,通过不同池子之间的升降级机制,系统会自动形成一种“优胜劣汰”的筛选效果,长期运行下来,热池里的IP质量会越来越高。
2.3 IP健康度评估:不只是看通不通
判断一个IP是否可用,绝不能只看“能不能连通”,而是要综合多个维度的信号做评分。我这边一个IP的健康度评估包含四类指标:连接成功率、平均响应时长、最近一小时失败次数、当前并发数。
每个指标有对应的权重,算出一个0到100的综合评分。评分低于60的IP会被自动移出热池,进入降级观察期;连续两次评分低于40,就直接拉黑并通知管理员换新IP。这里要注意的是,评分计算不能太频繁,30秒一次就够了,太频繁会给资源池管理模块增加不必要的负担,而且短时间内波动过大,反而会让调度器一直处于抖动状态。
另外,IP的健康状态是会随时间衰减的,所以调度器在分配任务时需要给每个IP打上“最近分配时间”标签。如果一个IP已经空闲超过5分钟,在重新分配前最好做一次轻量的连通性检查,避免拿到了一个已经失效的IP导致请求直接超时。
2.4 配额管理:限制每个IP的并发与频率
动态IP能提升稳定性,但不代表可以无限制地使用。恰恰相反,真正的稳定性来自合理的配额限制,而不是无限压榨。
我参照的标准是“单IP并发上限=目标网站容忍度÷单请求耗时”。举个例子,如果某个目标网站对单IP的最大容忍频率是每秒10次请求,单次请求平均耗时200ms,那单IP并发最多就是2左右。这个数值不是拍脑袋定的,而是通过阶梯式压测得到的。
落地时我在每个IP上维护了两个计数器:当前活跃连接数和每秒请求数。调度器分配任务前必须确认这两个指标都在阈值以内,超了就换下一个IP。这种配额管理看起来保守,但恰恰是高并发系统稳定的护城河,宁可让任务排队,也不要暴力压垮一个IP然后引发连锁故障。
3. 高并发调度系统设计:把流量管起来
3.1 调度器的核心职责:任务与IP的双重匹配
数据采集系统的调度器,本质上在做两件事:把海量URL任务分配给合适的Worker节点,同时给每个Worker分配合适的IP节点。这两件事单独做都不难,难点在于它们之间是互相影响的——任务的属性决定了它适合放在哪个IP上,IP的健康度又反过来影响任务分配策略。
我的调度器采用了“任务-IP双关联”的匹配逻辑。每个采集任务会携带一组属性标签,比如目标域名、预计请求量、请求频率要求、允许使用的IP地域等。调度器先从全局任务队列里把一组任务捞出来,再根据这些任务的目标域名去IP资源池里筛选对应的可用IP,最后将任务和IP绑定后一次性下发到Worker节点执行。
这种绑定关系不是永久的。任务执行过程中如果出现IP健康度下降,Worker会通过心跳上报给调度器,调度器会为后续任务重新匹配IP,已有的连接则继续跑完,避免中途切换造成数据半截。这种“先绑定后执行、异常再解绑”的思路,既保证了任务执行的连续性,又保留了动态调度的灵活性。
3.2 流量削峰与队列缓冲:防止系统被瞬时流量冲垮
大模型数据采集有一个典型场景:某个数据源突然发布了海量新内容,或者一个验证通过的采集通道突然被大量任务命中,瞬时流量可能是平时的几十倍。如果调度系统直接把所有任务全部打出去,目标网站会崩、IP会封、Worker会挂,整个系统瞬间陷入瘫痪。
解决这个问题必须靠队列缓冲和流量控制。我采用的方案是三级队列:入口队列负责接收所有新任务,会做一次去重和优先级标记;调度队列按照配置好的QPS(每秒请求数)从入口队列拉取任务,相当于一个流量闸门;等待队列则存放那些因为目标网站繁忙或者IP配额不足而暂时无法执行的任务。
实际开发中,QPS的调节不能是固定值,而是要根据实时反馈动态调整。我这边会监控目标网站的平均响应时间和错误率,响应变慢就自动降低调度速率,响应恢复后再慢慢提上来,形成一种类似TCP拥塞控制的机制。这套动态限流策略,让系统在面对波动流量时能够自然“伸缩”,而不是硬抗。
3.3 熔断与降级:当故障发生时怎么保住主流程
任何系统都会出故障,关键是一出故障就得把影响范围限制在最小。我在调度器里实现了三级熔断机制:域名级熔断、IP级熔断和Worker级熔断。
域名级熔断是最常用的,当某个目标域名的错误率连续5分钟超过30%,调度器会暂停向该域名分配新任务,已经入队未执行的任务转到等待队列,直到该域名恢复。IP级熔断则是动态IP资源池的自我保护,前文提到的健康度评分低于阈值就会触发。Worker级熔断关注的是节点本身的状态,如果某个Worker节点的CPU或内存持续高位,调度器会降低派发给它的任务量,让它有喘息的机会。
同时还要做好降级预案。比如动态IP资源池不足时,系统可以降级为只采集高优先级任务、暂停低质量数据源采集;Worker节点批量故障时,优先保证核心采集目录不受影响。这些降级策略都提前写在配置中心里,故障发生时不需要重新发版,运营人员改配置就能生效。
3.4 调度策略选型:权重计算与优先级调度
任务调度方面,我放弃了单纯的FIFO(先进先出)策略,因为它无法区分任务的紧急程度和资源消耗。现在用的是“权重评分+优先级队列”的组合。
每个任务在入队时会被计算一个权重分,考虑的因素包括:数据源的重要程度(重点语料源权重高)、任务的时效要求(新闻类当天有效,权重需要随时间衰减)、任务预计耗时(短任务优先,提高系统吞吐量)。权重分高且优先级高的任务会先被调度器捞取执行,低优先级的任务不会被饿死,只是放在低优先级队列里等待资源空闲时再执行。
这里踩过一个坑:最开始权重计算模型太复杂,涉及十多个参数,结果调度器本身成了性能瓶颈。后来把参数精简到5个以内,每个参数都用简单的线性关系,调度决策时间从几十毫秒降到了个位数毫秒。对于调度系统来说,决策速度比决策精度更重要,模型太复杂反而会拖慢整个系统的响应。
4. 实操过程与核心环节实现
4.1 系统整体架构与模块划分
整套系统跑在Kubernetes集群上,核心模块有四个:任务管理中心、调度引擎、IP资源池管理和采集Worker集群。任务管理中心负责接收数据源产生的URL列表,做去重和优先级标记后写入分布式消息队列;调度引擎从消息队列拉取任务,结合IP资源池的状态做匹配决策,然后把绑定好的任务通过gRPC下发到Worker节点。
IP资源池管理是一个独立服务,对接多个IP供应商的接口,定时拉取最新的IP列表,做连通性测试、评分、入库、上下线管理。Worker节点是真正发起HTTP请求的地方,每个Worker会根据下发的任务和IP信息创建连接,执行爬取、HTML解析、正文提取和原始数据存储。
模块之间全部通过接口通信,任务调度和IP分配逻辑都集中在调度引擎里,Worker保持无状态设计。这样设计的好处是出问题时可以单独扩容某个模块,比如IP资源池不够了就扩IP管理服务,任务积压了就扩调度引擎实例,不需要整个系统一起动。
4.2 调度器核心代码解读:动态IP分配与任务下发
调度器里最关键的代码块就是任务和IP的匹配逻辑。直接看核心实现:
python复制async def assign_task_with_ip(task, ip_pool):
# 1. 根据任务目标域名筛选候选IP
candidates = ip_pool.get_candidates(task.target_domain)
if not candidates:
await task_queue.retry_wait(task, reason="no_ip_available")
return None
# 2. 按健康度评分从高到低排序
candidates.sort(key=lambda x: x.health_score, reverse=True)
# 3. 遍历候选IP,找到满足并发和频率配额的那个
for ip_node in candidates:
if ip_node.concurrent_count >= ip_node.max_concurrent:
continue
if ip_node.qps_counter.get_qps() >= ip_node.max_qps:
continue
# 4. 绑定任务与IP,扣减配额
ip_node.concurrent_count += 1
ip_node.attach_task(task.task_id)
return ip_node
# 5. 所有IP配额不足,进入等待队列
await task_queue.enqueue_waiting(task)
return None
这段代码的逻辑看起来简单,但有几个细节很重要。第2步按健康度排序的时候,我会加一个随机因子,避免每次分配都集中到最前面的几个IP上,造成“头部IP过热、尾部IP闲置”的问题。第3步的配额检查是原子性的,用Redis的INCR命令实现,防止分布式环境下多个调度器实例同时修改同一个IP的并发计数。
任务下发之后,调度器不会立刻忘记这个任务。Worker每10秒上报一次心跳,携带当前任务状态、IP响应情况、数据抓取进度。如果心跳超时,调度器会把这个任务重新标记为待分配,同时把对应IP的并发数减一,确保整个系统的账目始终是平的。
4.3 IP资源池健康检查与自动切换
IP资源池管理的核心是健康检查机制。我设计了两种检查:定时全量检查和按需定向检查。定时全量检查每5分钟跑一次,对池子里所有IP做一次TCP连接测试和HTTP探针请求;按需定向检查则是在采集任务连续失败达到某个阈值时,对当前绑定的IP触发一次快速检查。
健康检查的代码实现:
python复制async def health_check(ip_node):
probe_start = time.time()
try:
async with aiohttp.ClientSession() as session:
async with session.get(
ip_node.probe_url,
timeout=aiohttp.ClientTimeout(total=5),
proxy=f"http://{ip_node.address}",
) as resp:
latency = time.time() - probe_start
if resp.status == 200:
ip_node.update_metrics(success=True, latency=latency)
else:
ip_node.update_metrics(success=False, status_code=resp.status)
except Exception as e:
ip_node.update_metrics(success=False, error=str(e))
ip_node.health_score = ip_node.calculate_score()
当IP健康度低于阈值时,资源池管理服务会触发自动下线流程:先将该IP从热池和温池中移除,等待当前正在执行的任务自然结束或超时,然后通知调度器不再为该IP分配新任务。下线后的IP进入冷却列表,10分钟后再做一次检查,如果恢复就重新拉回温池观察。
这个自动切换机制是系统稳定性的关键保险。有一次压测时,某个IP供应商的节点出现大面积故障,靠这个机制系统在2分钟内自动把故障IP全部下线,并将流量切换到其他节点,任务完成率只下降了3个百分点,没有造成整体阻塞。
4.4 压测数据:并发提升后稳定性不降反升
重构完成后,我做了一组对比压测。同一条采集任务流,分别用旧的固定IP策略和新的动态IP高并发调度策略跑,压测时长1小时,目标是对50个新闻网站的首页和详情页进行持续采集。
压测结果如下:
| 指标 | 固定IP方案 | 动态IP调度方案 |
|---|---|---|
| 平均QPS | 320 | 1250 |
| 采集成功率 | 82.6% | 97.4% |
| IP封禁数量 | 7 | 1 |
| 任务平均耗时 | 4.8秒 | 1.9秒 |
| 系统CPU平均负载 | 68% | 52% |
这个结果很有说服力。动态IP调度方案把系统的整体并发能力提升了近4倍,采集成功率反而提高了近15个百分点,IP封禁数量从7个降到1个,系统自身的CPU负载也下降了。原因是调度器能在IP即将被封之前就感知到异常并切换流量,避免了大量无效重试导致的资源浪费。
5. 常见问题与排查技巧实录
5.1 高频问题速查表
| 问题现象 | 可能原因 | 排查思路 | 解决方法 |
|---|---|---|---|
| 采集成功率持续走低 | IP池健康度评估不准确 | 检查健康检查频率和评分算法 | 增加按需定向检查,降低封禁前响应时间 |
| Worker节点频繁报超时 | 单IP并发配额设置过高 | 查看IP的QPS和延时曲线 | 下调单IP并发数,增加动态限流 |
| 任务堆积但系统负载不高 | 调度器任务匹配效率低 | 检查任务入队到分配的执行耗时 | 精简权重计算模型,增加调度实例 |
| 固定域名频繁触发风控 | 单域名总请求频率超额 | 查看该域名的全局QPS统计 | 扩大IP池中该域名的可用IP数量,分散频率 |
| 系统内存缓慢上涨 | 连接池未释放 | 查看Worker的GC日志和连接状态 | 增加连接空闲回收机制 |
5.2 三个让人印象深刻的故障复盘
第一个故障是IP池“雪崩”。刚开始上线时,所有IP都是均匀轮询分配的,结果某个热门数据源的任务量暴增,大量任务都在抢同一批IP,导致这些IP的QPS瞬间飙升,触发了一系列风控封禁。封禁后剩下的IP要承担更多流量,又接着被封,五分钟之内整个IP池几乎全军覆没。这次故障让我彻底明白了IP分层和配额管理的必要性。
第二个故障是调度器内存溢出。排查发现是有个Worker节点故障后没有及时上报状态,调度器一直认为任务还在执行中,相关上下文也无法回收,积压了几十万个任务对象后直接OOM。后来给调度器加了两道保护:一是对任务执行超时做硬性判定,二是对调度器内存使用量设置警戒线,达到80%就暂停接收新任务。
第三个故障比较隐蔽,是目标网站返回了302重定向但Worker没有正确跟随,导致大量请求返回空内容。采集成功率看着是100%,但数据入库量却少得可怜。这个问题的排查花了两天,最后是在数据质量监控里发现正文长度分布异常才定位到。现在我们的Worker对重定向状态码做了严格的上下文处理,同时增加了“入库数据条数”与“任务完成数”的比例监控。
5.3 独家避坑技巧:这些坑文档里不会写
关于动态IP资源调度,有几个细节是官方文档里不会告诉你的。第一个是IP池数量并不是越多越好,关键是每个IP的质量和稳定性。一个能稳定跑一小时的优质IP,比十个经常断线的IP对系统的帮助要大得多。所以比起不断接入新IP,我更建议定期清理低质量的IP节点。
第二个是调度器的时间轮精度问题。分布式环境下各节点的时间同步很重要,如果调度器和Worker之间存在毫秒级的时间偏差,会导致任务超时判定错误,进而出现重复执行和数据不一致。我用的是NTP时间同步加每个节点本地时间戳的偏移校正,保证任务时间的判定基准一致。
第三个是重试策略要避免“惊群效应”。当一个IP被封导致大量任务失败时,如果这些任务同时进入重试队列,瞬间产生的重试流量会把其他IP也打成高负载。我的做法是给重试任务加随机延迟,延迟区间在10秒到60秒之间,同时限制同一时刻进入重试队列的任务总数不能超过正常任务量的30%。
6. 最后分享一点个人体会
动态IP和高并发调度这两个东西,单独拿出来都有很多现成的方案可以抄,但真正把它们组合成一套稳定的采集系统,需要的是对“稳定性”这件事本身的深入理解。我自己的体会是,稳定性不是靠某一两个漂亮的技术点换来的,而是靠一套完整的容量规划、配额管理、故障响应和灰度验证机制共同撑起来的。每次系统出现抖动,都是一次重新审视这套机制的机会,别急着打补丁,先想想是哪个环节的防护被你忽略了。这套动态IP高并发调度方案上线运行大半年,采集成功率稳定在97%以上,也把我的心理预期从“今天能不能跑完”拉到了“这周能不能稳定跑完”。希望这篇文章能帮你在构建自己的语料采集系统时少走一些弯路。
