采集任务之间的依赖:别用 cron 错峰赌运气

任务一多,「先采什么、再采什么」的顺序问题就来了:先采搜索、再采详情;先采完数据、再出报表。第一版我用 cron 错峰硬排——搜索 6 点跑、详情 7 点跑、报表 8 点跑,赌「6 点的任务肯定 1 小时能跑完」。直到有一天搜索任务因为重试拖到 7 点半,详情任务 7 点照跑不误,拿到一份全空的 feature_id 列表。教训:任务顺序应该由「依赖」决定,不是由「时间」决定。

依赖的本质:数据依赖,不是时间依赖

任务 B 依赖任务 A,本质是「B 需要 A 的产物」:

  • 触发条件:A 完成(且成功)后 B 才能启动
  • 产物可用:A 的产出已经落库/落盘,B 能读得到
  • 失败传播:A 失败了,B 怎么办——这是最容易被忽视的一条

把「6 点、7 点、8 点」换成「A→B→C」的依赖图,一切就显式化了。

实现层级(从简到繁)

层级一:脚本链。 A && B && C——顺序有了,但失败定位、跳过、部分重跑全靠人判断。适合两三个任务的阶段。

层级二:状态表 + 前置列表(推荐的中间形态)。

CREATE TABLE jobs (
    job_id      text PRIMARY KEY,
    deps        text[],              -- 前置任务列表
    status      text NOT NULL,       -- pending/running/done/failed
    started_at  timestamptz,
    finished_at timestamptz
);

调度逻辑就一个循环:

def next_ready(jobs):
    """所有前置 done、自己 pending 的任务"""
    return [
        j for j in jobs
        if j.status == "pending"
        and all(get_status(jobs, d) == "done" for d in j.deps)
    ]

def run_all(jobs):
    while True:
        ready = next_ready(jobs)
        if not ready:
            break
        for job in ready:
            try:
                do_work(job)
                set_status(job, "done")
            except Exception:
                set_status(job, "failed")   # 依赖它的任务不会启动

这张表本身就回答了「谁等谁、谁失败、从哪续」——排查时看一眼状态列就够。

层级三:成熟调度器(Airflow/Prefect/Dagster)。 任务多、重试策略复杂、要可视化依赖图的时候再上。多数采集系统停在层级二就够。

失败传播的三个策略(按依赖性质选)

A 失败后,B 的处理不是只有「停」一种:

  1. 硬依赖(必须重跑):详情任务依赖搜索的 feature_id——A 失败,B 必须等 A 恢复后重跑
  2. 可降级:报表可以先用昨天的数据——A 失败,B 用旧产物 + 标注「数据截至昨日」
  3. 可跳过:日报的某可选环节——A 失败,B 跳过并告警

别一刀切——一刀切「全停」会让噪音变大,一刀切「不管」会让脏数据悄悄流通。

产物版本化

依赖的准确对象是「A 的某次运行产物」,不是「A 最近的状态」。落库时带 run_id:

-- 搜索任务产物
SELECT * FROM search_results WHERE run_id = ?;

这样重跑 A 不会影响「已经消费了旧产物的 B」——重跑语义(A 重跑后 B 要不要跟进)是明确的设计决定,而不是事后惊讶。

踩坑记录

坑 1:用 cron 错峰代替依赖。 任务慢一次就错乱,错乱的还是数据链路。依赖显式化是第一原则。

坑 2:依赖记录在人的脑子里。 新同事不知道「为什么详情要等搜索」。依赖写进状态表/文档。

坑 3:失败传播一刀切。 全停=噪音,不管=脏数据。按业务性质选策略。

坑 4:重跑语义没定义。 「重跑 A 之后 B 要不要也重跑」没人定,各跑各的。写进任务文档。

坑 5:产物没版本。 B 处理的是 A 的旧产物还浑然不知。带 run_id。

坑 6:循环依赖。 A 等 B、B 等 A,挂在那里永远不跑。依赖图加环检测(构建时校验)。

工程清单

  1. 数据依赖显式化:触发条件 + 产物可用 + 失败传播
  2. 状态表 + 前置列表,调度循环 20 行内解决
  3. 失败传播分类:硬依赖 / 可降级 / 可跳过
  4. 产物带 run_id,重跑语义写进文档
  5. 依赖图构建时做环检测
  6. 状态表本身是可观测性(谁等谁一眼可见)

任务依赖的本质,是把「顺序」从「编排在时钟里」搬到「建模在数据里」。依赖图的每一根箭头都在回答三个问题:B 何时能跑(A 完成了吗)、跑出来对不对(产物版本对吗)、A 挂了 B 怎么办(传播策略是什么)。答全这三问,任务再也不会「因为运气而错乱」。

任务产出与请求的关联可以靠接口响应里的 request_id(做产物版本与追踪的锚点),字段说明见 SerpBase 官方文档。你们的任务顺序是依赖还是错峰?评论区聊聊事故现场。

posted @ 2026-10-08 08:47  蜘蛛人  阅读(12)  评论(0)    收藏  举报