4. 任务依赖图建模:将实际任务抽象为节点、依赖关系抽象为边、权重与状态属性
好,咱们进入第四章。前面几章我们把可视化工具和基础概念过了一遍,现在该聊聊最核心的问题了——怎么把现实世界里乱七八糟的任务,变成一张计算机能理解的图。
说白了,任务依赖图建模就是做三件事:找节点、画边、贴属性。听起来简单吧?但我见过太多人在这上面翻车了。我刚开始做调度系统时,就犯过一个低级错误——把“编译代码”和“运行测试”拆成了两个节点,但忘了加依赖边。结果呢?测试跑在旧代码上,白白浪费了半小时。
4.1 节点抽象:把“事”变成“点”
先聊节点。现实中的任务千奇百怪:编译一个模块、发送一封邮件、训练一个模型、审批一份合同……但到了图里,它们都得变成一个个矩形或圆形节点。
我个人习惯,把节点抽象成三个层次:
- 原子任务:不可再拆的最小执行单元。比如“执行SQL查询”。
- 复合任务:由多个原子任务组成,但对外表现为一个节点。比如“数据清洗流程”。
- 子图:整张图里嵌套的另一张图。比如“微服务A的完整部署流程”。
嗯,这里要注意:不要过度拆分。我曾经把一个“发送通知”拆成了“查收件人列表→拼接模板→调用API→记录日志”四个节点。结果图变得密密麻麻,根本看不清。后来我学乖了——粒度控制在“一个人/一个脚本一次能完成”的程度就够了。
4.2 边抽象:依赖关系的三种类型
节点有了,接下来就是连线。依赖关系不是只有“A做完才能做B”这一种。我整理了一下,实际项目中常见的依赖分三类:
| 依赖类型 | 含义 | 例子 |
|---|---|---|
| 顺序依赖 | B 必须等 A 完成才能开始 | 先编译,再运行测试 |
| 资源依赖 | B 需要 A 产生的数据/文件 | 先下载模型权重,再加载推理 |
| 条件依赖 | B 是否执行取决于 A 的结果 | 如果测试失败,则触发回滚任务 |
你想想看,如果只把“顺序依赖”画成边,那条件依赖怎么办?我见过有人硬把条件依赖画成虚线,结果团队里一半人看不懂。我的做法是:在边上加标签。比如用 type="conditional" 属性,或者直接在边上写“if failed”。
4.3 权重与状态:给节点和边“贴标签”
光有结构和关系还不够。你得知道每个任务要跑多久、占多少资源、现在是什么状态。这些信息,就是权重和状态属性。
4.3.1 权重属性
权重可以理解成任务的“代价”。常见的权重有:
- 时间权重:预计执行时长(秒/分钟)
- 资源权重:CPU/内存/GPU 消耗量
- 优先级权重:任务的重要程度(1~10)
- 失败概率权重:历史失败率(0~1)
我个人习惯把权重放在节点下方,用小字标注。比如一个编译任务,我会写成:
节点: 编译前端模块
时间权重: 120s
资源权重: 4核CPU / 8GB内存
优先级: 8
这样调度器一看就知道:这任务重,得优先分配好资源。
4.3.2 状态属性
状态是动态的。任务跑起来之后,你得知道它现在是死是活。我常用的状态集是:
| 状态 | 含义 | 可视化颜色(我习惯用) |
|---|---|---|
| PENDING | 等待执行 | 灰色 |
| RUNNING | 正在执行 | 蓝色 |
| SUCCESS | 执行成功 | 绿色 |
| FAILED | 执行失败 | 红色 |
| SKIPPED | 条件不满足被跳过 | 黄色 |
为什么强调颜色?因为故障定位时,你扫一眼图,红色节点扎堆的地方就是重灾区。我有个习惯:把 FAILED 节点周围两层的邻居节点也高亮,因为故障往往会扩散。
4.4 完整建模示例:一个 CI/CD 流水线
光说不练假把式。咱们拿一个真实的 CI/CD 流水线来建模。假设流程是这样的:
- 代码提交触发
- 并行执行:代码检查 + 单元测试
- 如果两者都通过,执行集成测试
- 集成测试通过后,构建镜像
- 构建成功,部署到测试环境
- 部署完成后,运行冒烟测试
- 冒烟测试通过,标记为“可发布”
对应的图模型,我用 SVG 画出来:
你看,这张图里:
- 节点:每个步骤都是一个矩形,颜色区分角色(触发/检查/构建/部署/测试)
- 边:箭头表示顺序依赖,并行任务用分叉表示
- 权重:每个节点可以附加时间、资源等属性(图上没写,但建模时要有)
- 状态:运行时,每个节点会变成灰/蓝/绿/红/黄
4.5 代码实现:用 Python 定义图结构
最后,咱们看看怎么用代码把模型写出来。我个人喜欢用字典加列表,简单直接:
# 节点定义
tasks = {
"trigger": {"name": "代码提交触发", "type": "trigger", "weight": {"time": 5, "priority": 10}},
"lint": {"name": "代码检查", "type": "check", "weight": {"time": 30, "priority": 7}},
"utest": {"name": "单元测试", "type": "test", "weight": {"time": 60, "priority": 8}},
"itest": {"name": "集成测试", "type": "test", "weight": {"time": 120, "priority": 6}},
"build": {"name": "构建镜像", "type": "build", "weight": {"time": 90, "priority": 5}},
"deploy": {"name": "部署测试环境", "type": "deploy", "weight": {"time": 45, "priority": 4}},
"smoke": {"name": "冒烟测试", "type": "test", "weight": {"time": 20, "priority": 3}},
}
# 边定义(依赖关系)
edges = [
("trigger", "lint", {"type": "顺序依赖"}),
("trigger", "utest", {"type": "顺序依赖"}),
("lint", "itest", {"type": "条件依赖", "condition": "lint == SUCCESS"}),
("utest", "itest", {"type": "条件依赖", "condition": "utest == SUCCESS"}),
("itest", "build", {"type": "顺序依赖"}),
("build", "deploy", {"type": "顺序依赖"}),
("deploy", "smoke", {"type": "顺序依赖"}),
]
# 状态初始化
for task_id in tasks:
tasks[task_id]["status"] = "PENDING"
这段代码跑起来,你就有了一个完整的图模型。后面章节我们会讲怎么把它画出来、怎么更新状态、怎么定位故障。但今天先把地基打牢——节点、边、权重、状态,一个都不能少。
check_lint、test_unit、build_docker。这样看日志时,光看 ID 就知道它是什么任务,不用来回查字典。