3. WDL基本语法
3.1 理解workflow与task
刚开始接触WDL时,我最困惑的就是workflow和task到底有什么区别。说白了,这俩概念是整个WDL的基石。你想想看,一个计算任务总得有个"做什么"和"怎么组织"的问题。
我个人习惯这么理解:task 是"干活的",它定义具体的计算命令;workflow 是"管事的",它编排这些task的执行顺序和数据传递。打个比方——workflow是剧本,task是演员,剧本指挥演员什么时候上场、说什么台词。
我在项目中遇到过不少新手把代码全塞在一个task里,workflow反而成了摆设。其实这样既不好复用,也不好调试。正确的做法是:把每个独立步骤拆成task,让workflow去串联。
3.2 task定义:执行的最小单元
先来看看task长什么样。一个完整的task包含四个部分:input、command、output 和 runtime。我建议你记住这个骨架。
task hello_world {
input {
String name
}
command {
echo "Hello, ${name}!"
}
output {
String greeting = read_string(stdout())
}
runtime {
docker: "ubuntu:20.04"
memory: "1 GB"
cpu: 1
}
}
咱们拆开看:
- input:声明输入参数,类型和默认值都可以定义。嗯,这里要注意,参数名尽量语义化,别用a、b这种。
- command:放你真正的命令或者脚本。支持
${}插值,能把input里的变量传进去。 - output:从命令执行结果里提取数据。WDL支持多种捕获方式,
read_string、read_int、read_file都很常用。 - runtime:指定运行环境。Docker镜像、内存、CPU核心数都在这里配。我曾经因为忘了设runtime,任务被默认资源限制卡住,排查了半天才发现。
${var} 而不是 $var 的习惯。前者在复杂表达式里更安全,不容易解析出错。
3.3 workflow定义:编排的舞台
task是零件,workflow就是把零件组装起来的流水线。它的核心结构包括:输入声明、task调用、输出声明。来看一个例子:
workflow greet_pipeline {
input {
String user_name
}
call hello_world {
input: name = user_name
}
output {
String result = hello_world.greeting
}
}
关键点在于 call 语句。你用 call task_name 来引用一个task,并通过 input: 把workflow的变量传给它。task执行完,用 task_name.output_name 的方式拿到结果。
我在做项目时犯过一个低级错误——在workflow里 call 了一个task,但忘了在workflow的 output 里声明对外输出。结果下游流程拿不到数据,愣是排查了两小时。记住:workflow的输出不会自动暴露,必须显式写出来。
call 的task名称不能有重复。如果你需要多次调用同一个task,用别名区分:call my_task as first_step。
3.4 workflow与task的协作关系
理解了各自的语法,咱们再看它们怎么配合。我整理了一个对比表格,这样更直观:
| 对比维度 | task | workflow |
|---|---|---|
| 角色定位 | 执行具体计算 | 编排调度任务 |
| 输入输出 | 定义局部参数 | 定义全局数据流 |
| 语法核心 | command + output + runtime | call + 数据传递 |
| 执行方式 | 独立运行 | 按依赖顺序执行 |
| 复用性 | 高,可被多个workflow调用 | 低,通常是顶层入口 |
下面这张图帮你建立整体印象。workflow负责"调兵遣将",task负责"冲锋陷阵",数据通过input/output在它们之间流动。
3.5 实战:串联一个完整流程
我们写一个完整的例子。需求很简单:输入一个半径和高度,先算圆面积,再算圆柱体积,最后把结果格式化输出。
version 1.0
task calculate_area {
input {
Float radius
}
command {
python3 -c "
import math
radius = ${radius}
area = math.pi * radius * radius
print(area)
"
}
output {
Float area = read_float(stdout())
}
runtime {
docker: "python:3.9-slim"
memory: "512 MB"
cpu: 1
}
}
task calculate_volume {
input {
Float area
Float height
}
command {
python3 -c "
area = ${area}
height = ${height}
volume = area * height
print(volume)
"
}
output {
Float volume = read_float(stdout())
}
runtime {
docker: "python:3.9-slim"
memory: "512 MB"
cpu: 1
}
}
task format_result {
input {
Float volume
String label = "圆柱体积"
}
command {
echo "${label}: ${volume} 立方单位"
}
output {
String result = read_string(stdout())
}
runtime {
docker: "ubuntu:20.04"
memory: "256 MB"
cpu: 1
}
}
workflow calculate_pipeline {
input {
Float radius
Float height
}
call calculate_area {
input: radius = radius
}
call calculate_volume {
input: area = calculate_area.area,
height = height
}
call format_result {
input: volume = calculate_volume.volume
}
output {
Float final_volume = calculate_volume.volume
String final_result = format_result.result
}
}
这段代码有三个task和一个workflow。注意数据是怎么一步步传下去的:calculate_area 算出面积,传给 calculate_volume,再传给 format_result。workflow 用 task_name.output_name 的方式引用上游结果。