3. 任务依赖类型:硬依赖与软依赖、时间依赖、数据依赖、资源依赖、条件依赖
依赖关系,说白了就是任务之间的「谁等谁」问题。我做了这么多年调度系统,发现很多线上故障其实不是代码写错了,而是依赖关系没理清楚。你想想看,一个任务等另一个任务,到底是在等什么?是等它跑完?等它产出数据?还是等某个条件满足?
这一节,我把常见的依赖类型拆成五类来讲。每种类型我都会结合项目里的真实场景,帮你建立直觉。
3.1 硬依赖与软依赖
这是最基础的分类。硬依赖,就是「你不做完,我不开始」。软依赖,则是「你最好做完,但没做完我也能凑合跑」。
核心区别:硬依赖阻断执行,软依赖仅影响优先级或质量。
我在设计一个金融结算系统时,遇到过这样的场景:
- 硬依赖:交易对账任务必须等清算任务完成。清算没跑完,对账拿到的数据就是错的,不能启动。
- 软依赖:报表生成任务最好等缓存预热完成。但如果缓存没预热,报表也能跑,只是慢一点。
实现上,硬依赖通常用 DAG 调度器来保证——前驱节点状态为 success 时,后继节点才进入 ready 队列。软依赖则更灵活,可以用权重或超时机制来处理。
我的习惯:设计系统时,先把所有依赖都当成硬的来建模。等系统稳定了,再逐个评估哪些可以降级为软依赖。这样更安全。
3.2 时间依赖
时间依赖,就是任务对「什么时候执行」有要求。它不关心其他任务的状态,只关心时钟。
常见的三种模式:
| 模式 | 说明 | 典型场景 |
|---|---|---|
| 固定时间 | 每天/每周的某个时刻触发 | 凌晨 3 点跑日终批处理 |
| 延迟执行 | 任务启动后等待一段时间 | 数据写入后等 5 分钟再消费 |
| 窗口时间 | 只能在某个时间段内执行 | 业务高峰期不跑离线任务 |
我记得有一次,一个数据同步任务总是失败。排查了半天,发现它依赖的上游任务虽然跑完了,但数据库的索引重建还在进行。后来我加了一个「延迟 30 秒」的时间依赖,问题就解决了。
避坑指南:我曾经把时间依赖和硬依赖混在一起用,结果调度器死锁了。原因是:任务 A 等时间窗口,任务 B 等任务 A,而时间窗口永远不满足。记住,时间依赖应该独立于任务依赖图来评估。
3.3 数据依赖
数据依赖,是分布式系统里最容易被忽视的依赖类型。它不是说「等任务跑完」,而是「等数据就绪」。
举个例子:
- 任务 B 需要读取 HDFS 上的 /data/2024/part-*.parquet 文件
- 任务 A 负责生成这些文件
- 但任务 A 可能还在写,文件还没落盘
这时候,即使任务 A 的状态是 success,任务 B 也不能保证能读到完整数据。因为文件系统有写延迟,或者任务 A 的提交操作还没完成。
我一般用两种方式处理数据依赖:
- 标记文件(Marker File):任务 A 写完数据后,再写一个 _SUCCESS 标记文件。任务 B 只认标记文件,不认任务状态。
- 数据版本号:每个数据分区带时间戳或版本号,任务 B 指定版本号来消费。
# 伪代码示例:标记文件方式
def task_a():
write_data("/data/2024/part-1.parquet")
write_data("/data/2024/part-2.parquet")
# 最后写标记文件
touch("/data/2024/_SUCCESS")
def task_b():
# 只等标记文件,不等任务状态
wait_for_file("/data/2024/_SUCCESS")
read_data("/data/2024/part-*.parquet")
个人经验:在实时流处理场景里,数据依赖更复杂。我建议用 Kafka 的 offset 来管理——下游任务记录自己消费到的 offset,而不是依赖上游任务的完成状态。
3.4 资源依赖
资源依赖,就是任务对「运行环境」有要求。它不关心其他任务在做什么,只关心资源够不够。
常见的资源类型:
- 计算资源:CPU、内存、GPU
- 存储资源:磁盘空间、IOPS
- 网络资源:带宽、端口
- 许可资源:数据库连接数、API 调用配额
我遇到过最典型的案例是:一个 Spark 任务需要 64GB 内存,但集群里只有 32GB 的空闲资源。调度器把它分配到了节点上,结果 OOM 了。后来我加了资源依赖检查——任务启动前先确认资源是否满足,不满足就排队等待。
关键点:资源依赖不是「等任务完成」,而是「等资源释放」。它和任务依赖图是正交的,需要独立的资源管理器来处理。
3.5 条件依赖
条件依赖,是最灵活也最容易出问题的一种。它不是说「等某个任务完成」,而是「等某个条件成立」。
条件可以是:
- 上游任务的输出值(比如:销售额 > 100 万才触发报表)
- 外部系统的状态(比如:数据库主从同步延迟 < 5 秒)
- 业务规则(比如:今天是月末才跑月结任务)
我设计过一个风控系统,它的依赖逻辑是这样的:
# 条件依赖示例
if transaction_amount > 10000:
run_risk_check()
else:
run_normal_approval()
你看,这里没有「等谁完成」的概念,而是根据条件选择不同的执行路径。
我曾经踩过的坑:条件依赖里用了外部 API 的返回值,结果 API 超时了,整个调度链路卡死。后来我加了超时兜底——条件判断超过 3 秒就按默认分支执行。记住,条件依赖一定要有 fallback 逻辑。
3.6 依赖类型对比与选择
这五种依赖类型,在实际系统中往往是组合使用的。我整理了一张对比表,方便你快速参考:
| 依赖类型 | 依赖对象 | 阻断方式 | 典型实现 |
|---|---|---|---|
| 硬依赖 | 任务状态 | 强阻断 | DAG 调度器 |
| 软依赖 | 任务状态 | 弱阻断 | 权重/超时 |
| 时间依赖 | 时钟 | 条件阻断 | Cron/延迟队列 |
| 数据依赖 | 数据就绪 | 强阻断 | 标记文件/版本号 |
| 资源依赖 | 资源可用 | 排队阻断 | 资源管理器 |
| 条件依赖 | 条件表达式 | 逻辑阻断 | 规则引擎/分支判断 |
我个人建议,设计依赖关系时,先从硬依赖和数据依赖入手,把核心链路稳住。然后再根据业务需求,逐步引入时间依赖、资源依赖和条件依赖。别一开始就想把所有类型都用上,那样系统会变得很难调试。
3.7 依赖关系全景图
下面这张图,把五种依赖类型的关系和交互方式画了出来。你可以看到,任务 A 和任务 B 之间可能有多种依赖同时存在。
嗯,这张图里,任务 B 同时受硬依赖、时间依赖、数据依赖、资源依赖和条件依赖的约束。实际项目中,你很少会见到这么复杂的场景,但理解每种依赖的独立性和组合方式,是设计健壮调度系统的关键。