sourcecode.school接单
← 文章列表
数据工程与开发3 min read

一套运行两年的采集系统,调度与去重上的实际问题

爬虫写完只是开始。任务调度、失败重试、增量去重、代理池健康度,才是长期维护的主要成本。记录两年里改过三版的几个设计。

这套系统最初只是一个脚本,跑单平台的商品详情。两年后它覆盖六个平台、日均四十万条,中间调度层重写过三次。回头看,真正吃时间的从来不是解析逻辑。

日均采集
40 万条
平台数
6
调度层重写
3 次

第一版:cron 加脚本#

最初的形态是每个平台一个脚本,crontab 定点跑。问题在第三个平台接进来时就暴露了:任务之间没有任何协调,两个脚本同时跑会把代理池打满,失败了也没有统一的重试入口,只能靠人看日志。

真正致命的是没有任务状态。脚本被 kill 掉之后,没人知道它跑到哪了,只能整个重跑一遍。

第二版:任务表加工作进程#

把任务落到数据库里,用状态字段驱动。这一版解决了大部分问题,但引入了一个新坑:状态更新和实际执行不是原子的。

进程在「标记为执行中」和「真正开始请求」之间被杀掉,任务就永远卡在执行中,不会被任何人捡起来。解决办法是给状态加一个心跳时间戳,超过阈值没更新的任务视为死亡,重新入队:

回收僵死任务
UPDATE crawl_task
SET status = 'pending',
    retry = retry + 1,
    worker_id = NULL
WHERE status = 'running'
  AND heartbeat_at < NOW() - INTERVAL '10 minutes'
  AND retry < 3;

retry < 3 这个条件是后来补的。之前没有上限,一个必然失败的任务会被无限回收重试,日志里全是它。

增量去重:三种做法的取舍#

去重是这类系统里最容易做错的地方。用过三种方案:

方案优点实际问题
数据库唯一索引最简单,强一致数据量上去后写入变慢,冲突走异常路径开销大
Redis Set内存占用随数据量线性涨,重启要重建
布隆过滤器内存恒定有误判率,会漏掉少量新数据

最后的选择是分层:布隆过滤器做第一道拦截,挡掉绝大部分重复;通过的再查一次数据库唯一索引确认。布隆的误判方向是「可能存在」,所以漏判会被第二层兜住,而绝大部分真重复根本走不到第二层。

代理池:健康度比数量重要#

早期的判断标准是「池子里有多少个可用 IP」,这个指标具有误导性。真正该看的是每个 IP 在目标平台上的成功率——同一批代理在 A 平台好用,在 B 平台可能全被拉黑。

现在的做法是按「代理 × 平台」维度记录滑动窗口内的成功率,低于阈值的对该平台临时下线,其他平台不受影响。这个改动让整体成功率从八成出头提到九成五以上,而代理数量一个没加。

小结#

如果只看代码量,解析逻辑占大头;如果看两年里的改动次数,调度、重试、去重、代理健康度这四块加起来远超解析。写第一版的时候把它们当作「以后再说」的部分,后面每一样都要付利息。

至少把任务状态和失败上限这两件事在第一版就做对——它们改起来最痛,因为要动已经跑起来的数据。


Minner
Minner

长期做数据采集与逆向分析,同时负责采集系统后端与数据管道。方向集中在自媒体与电商平台的数据获取、接口协议还原,以及在此之上的数据工程与分析。