工作流基础:任务定义、工作流结构与输入输出声明
真正开始写WDL之前,得先把骨架搭明白。说白了,WDL脚本就两大块:任务(Task)和工作流(Workflow)。我刚开始接触的时候,总觉得这俩东西差不多,写起来经常混在一起。直到有一次,我把数据校验和压缩的逻辑塞进同一个任务里,结果耦合得死死的,改一个就要牵连另一个……后来我学乖了:Task是执行单元,Workflow是编排单元,分工特别清晰。
嗯,我们先看看它们长什么样。
个人习惯:我写WDL时,会先画一张类似的流程图。墨水滴到纸上再敲键盘,结构清晰多了。
2.1 任务(Task)定义
Task 是 WDL 的最小执行单元。它描述“做什么”以及“用什么资源做”。我个人习惯把 Task 想象成一个函数:有输入、有逻辑、有输出。
一个标准的 Task 定义长这样:
task validate_data {
input {
File input_file
String file_format
Int min_size
}
command {
python3 validate.py \
--input ${input_file} \
--format ${file_format} \
--min-size ${min_size}
}
output {
File validated_file = "${input_file}.validated"
Boolean is_valid = read_int(stdout())
}
runtime {
docker: "python:3.9-slim"
memory: "2 GB"
cpu: 1
}
}
拆开来看,每个部分都挺直白的:
| 区块 | 作用 | 我的坑爹经历 |
|---|---|---|
| input {} | 声明输入参数,指定类型和默认值 | 我曾经忘记在 input 里写类型,结果脚本跑一半报类型转换错误,排查了半小时才发现 |
| command {} | 实际要执行的命令或脚本 | 注意 ${} 引用变量,我早期漏过大括号,直接爆红 |
| output {} | 声明输出文件或值 | 输出路径没写对,下游任务拿不到文件,整个流程卡住 |
| runtime {} | 指定容器、内存、CPU 等资源 | 内存设太小,大文件校验直接 OOM 挂掉 |
重点:command 块里的 ${} 是变量替换语法。你在 input 里声明的变量,在 command 里用 ${变量名} 引用就行。WDL 会自动做字符串替换,省事得很。
2.2 工作流(Workflow)结构
工作流负责串联多个 Task。你想想看,一个完整的数据处理流程,不可能只有一个步骤。校验完数据,接下来可能要转换格式,然后压缩输出。工作流就是把它们按顺序串起来。
一个简单的工作流示例:
workflow data_pipeline {
input {
File raw_data
String output_format
Int compression_level
}
call validate_data {
input:
input_file = raw_data,
file_format = "csv",
min_size = 1024
}
call compress_data {
input:
input_file = validate_data.validated_file,
level = compression_level
}
output {
File final_output = compress_data.compressed_file
}
}
这里有几个关键点:
- call + Task 名:就像调用函数一样调用你定义好的 Task
- input: 参数 = 值:给被调用的 Task 传参,可以是工作流 input 里的变量,也可以是上一个 Task 的 output
- output {}:工作流最终的输出,和 Task 的 output 类似
注意:调用 Task 时,input 里的参数名必须和 Task 定义中的 input 名称完全一致。我见过有人把大小写写错了,结果 WDL 默默丢了个参数过去,下游命令直接崩。
2.3 输入输出声明
输入输出是 WDL 的灵魂。说白了,WDL 就是靠输入输出把任务串起来的数据管道。没有良好的声明,上游下游就断了。
WDL 支持多种数据类型,常用的有:
| 类型 | 含义 | 示例 |
|---|---|---|
| File | 文件路径 | File input_bam |
| String | 字符串 | String sample_name |
| Int | 整数 | Int min_quality |
| Float | 浮点数 | Float threshold |
| Boolean | 布尔值 | Boolean skip_validation |
| Array[类型] | 数组 | Array[File] input_files |
| Map[键类型, 值类型] | 键值对 | Map[String, File] sample_map |
声明输入输出时,我建议遵循几条简单的规则:
- 输入尽量限定时必填参数:不要留太多默认值,避免运行时忘记传参导致静默错误
- 输出路径尽量使用输入文件名衍生:比如输入
sample.bam,输出就是sample.validated.bam,清晰易懂 - 多用 Boolean 控制流程分支:比如加一个
skip_validation参数,调试时省去很多重跑时间
我的经验:在大型项目中,我会在 workflow 的 input 里加一个
String pipeline_version 参数,把版本号写到输出文件的元数据里。以后查 bug 时,一眼就知道当前结果是用哪个版本跑的。
来看看一个更完整的声明范例:
workflow compress_pipeline {
input {
Array[File] raw_files
String output_prefix
Boolean compress_flag = true
Int threads = 4
}
call validate_batch {
input:
files = raw_files,
min_size = 512
}
call compress_batch {
input:
input_files = validate_batch.validated_files,
output_prefix = output_prefix,
level = if compress_flag then 9 else 0
}
output {
Array[File] compressed_files = compress_batch.compress_files
String pipeline_version = "1.2.0"
}
}
注意到了吗?level = if compress_flag then 9 else 0 这种条件赋值也是 WDL 支持的特性。我觉得它特别适合做参数开关,很灵活。
总结一下:Task 是砖,Workflow 是墙,输入输出是水泥。砖要定义清楚形状(输入输出),墙要排好顺序,水泥要抹得均匀(参数传递)。这三样搞明白了,WDL 脚本就算入门了。