一套运行两年的采集系统,调度与去重上的实际问题
爬虫写完只是开始。任务调度、失败重试、增量去重、代理池健康度,才是长期维护的主要成本。记录两年里改过三版的几个设计。
这套系统最初只是一个脚本,跑单平台的商品详情。两年后它覆盖六个平台、日均四十万条,中间调度层重写过三次。回头看,真正吃时间的从来不是解析逻辑。
- 日均采集
- 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 平台可能全被拉黑。
现在的做法是按「代理 × 平台」维度记录滑动窗口内的成功率,低于阈值的对该平台临时下线,其他平台不受影响。这个改动让整体成功率从八成出头提到九成五以上,而代理数量一个没加。
小结#
如果只看代码量,解析逻辑占大头;如果看两年里的改动次数,调度、重试、去重、代理健康度这四块加起来远超解析。写第一版的时候把它们当作「以后再说」的部分,后面每一样都要付利息。
至少把任务状态和失败上限这两件事在第一版就做对——它们改起来最痛,因为要动已经跑起来的数据。