Nest.js 今日热榜:采集调度、去重与失败重试
热点站后端要像勤务系统:按时跑、失败可补、结果可降级。
今日热榜的后端用 Nest.js 搭建:定时采集微博、知乎、B 站、少数派等多平台热点,经清洗去重后写入 PostgreSQL,再由 Next.js SSR 列表页消费。项目上线第一周我们就遇到典型问题——某源站突然改版 HTML 结构,解析全空;另一平台在高峰期限流返回 429。采集系统最难的不是「能抓到数据」,而是源站挂了、格式变了、限流了,页面还能不能看。
调度模块化
按平台拆 Provider,统一 fetch → normalize → upsert 接口。新增平台只加模块,不动调度内核。每个 Provider 只关心自己的解析规则与字段映射,调度层负责并发控制、超时与重试策略。我们在 CrawlerModule 里用工厂模式注册各源,配置表驱动 cron 表达式,运维可在后台暂停单个源而不重启服务。
用 @nestjs/schedule 的 cron 配合 Bull 队列控制并发:同一平台串行、不同平台并行,避免同一时间把所有源打爆。对源站限速与指数退避写在公共中间层,某平台连续 429 时自动降频,而不是硬怼到封 IP。采集任务带 traceId,日志能串起「哪次调度、哪个源、失败在哪一步」。HTTP 客户端统一设置 User-Agent 与超时,敏感 cookie 走环境变量,不进代码仓库。
去重
同一热点可能在不同平台标题微调、链接带不同追踪参数。我们用规范化标题(去标点、全半角统一、繁简可选转换)加链接指纹(去掉 utm、from 等 query)做联合去重,保留首次出现时间与热度轨迹,而不是无脑覆盖最新一条。这样前端可以展示「已在榜 N 小时」或简易热度曲线,运维也能对比各源差异。PostgreSQL 上对 (fingerprint) 建唯一索引,upsert 用 ON CONFLICT DO UPDATE 只更新热度字段,避免重复行膨胀。
失败重试
单平台失败不影响其他源:任务失败进入重试队列,上限三次,间隔 1、5、15 分钟。持续失败则标记源健康度为 degraded,运维看板可见,并触发钉钉告警。解析规则变更时,我们在 staging 跑 shadow 采集对比新旧结果条数,差异超过阈值再切生产。全源失败时仍保留上一版快照供 SSR 降级——采集系统的体面,是挂了一半源,页面上仍有榜可看;全挂了,至少还有六小时内的旧榜,而不是空白首屏。
数据清洗与入库
normalize 阶段统一字段:标题、链接、热度值、平台标识、抓取时间。HTML 实体解码、去除零宽字符,链接补全相对路径。热度值各源量纲不同,入库前做 min-max 归一或按平台分桶,前端展示时注明来源,避免用户误以为「全网统一排名」。写入 PostgreSQL 用事务批量 upsert,单轮采集控制在三十秒内完成,避免长事务锁表。采集元数据表记录每轮耗时、成功条数、失败原因,供后续调优 cron 频率。
与前端契约
API 或 SSR 直查只读「当前快照 ID」,不耦合采集进程。版本切换原子化:新快照写完后一次性 flip active 指针,读者不会看到半新半旧。Nest 侧暴露 /health/crawler 给 K8s 探针,degraded 仍返回 200 但带 warning 头,运维平台可聚合。上线前用录制好的 HTML fixture 做契约测试,源站微调时 CI 先红再改 parser,而不是生产静默空榜。监控里单独看「空榜轮次」与「降级轮次」,前者是事故,后者是设计生效。这套勤务式采集,让热榜站像交通路况屏:偶尔延迟,但不应黑屏。
小结
采集系统要按「平台可插拔、失败可隔离、结果可降级」来设计。Nest.js 模块化很适合把调度内核与各源 Provider 分开演进。源站限速、健康度与重试上限决定了你能不能长期稳跑,而不是靠人工半夜补采。
实践中把上述清单变成可勾选的发布门禁,比事后补救更省时间。