3、WDL基础:工作流与任务定义、输入输出声明、运行时配置
好,我们正式开始写WDL了。
如果你是第一次接触WDL,可能会觉得它跟常见的脚本语言不太一样。它更像是在描述「谁先做、谁后做、谁依赖谁」的依赖关系图。说白了,你把每个步骤都封装成一个任务,然后用工作流把它们串起来。
这一章,我们就来拆解WDL的三大核心零件:工作流定义、任务定义、输入输出声明、运行时配置。掌握了这些,你就能写出第一个能跑的WDL脚本了。
3.1 工作流与任务定义
先来看一个最简单的WDL骨架。不用害怕,代码其实很规整——嗯,就像搭积木一样。
version 1.0
workflow my_workflow {
input {
File my_file
}
call my_task {
input:
input_file = my_file
}
output {
File result = my_task.output_file
}
}
task my_task {
input {
File input_file
}
command {
cat ${input_file} > output.txt
}
output {
File output_file = "output.txt"
}
runtime {
docker: "ubuntu:latest"
memory: "1 GB"
cpu: 1
}
}
注意两个东西:workflow 和 task。
- workflow:就是整个管线的总指挥。它只负责编排,不干活。
- task:具体的干活单元。你所有的 shell 命令、软件调用、参数配置,都写在 task 里。
我个人习惯把 workflow 理解成「生产调度室」,task 理解成「车间里的机器」。调度室只管说「机器A做完传给机器B」,机器才管具体怎么操作。
call 语句调用 task。如果需要传递输入,通过 input: 块传入。这就是 WDL 的依赖表达方式。
3.2 输入输出声明
WDL 是强类型的。每一个输入、输出变量,都必须先声明类型。
为什么?因为 Cromwell 需要知道传进来的是文件还是字符串,是做路径挂载还是直接传值。我刚开始写的时候常常漏掉类型声明,结果 Cromwell 报错说找不到输入——其实就是没告诉它这是个 File。
3.2.1 常见数据类型
| 类型 | 描述 | 示例 |
|---|---|---|
String |
字符串 | "chr1" |
Int |
整数 | 42 |
Float |
浮点数 | 3.14 |
Boolean |
布尔值 | true |
File |
文件路径(最常用) | "gs://bucket/sample.fastq" |
Array[File] |
文件数组 | ["a.fastq", "b.fastq"] |
Map[String, String] |
键值对 | {"key": "val"} |
Pair[File, File] |
成对文件 | (read1, read2) |
3.2.2 输入声明的写法
在 task 顶部的 input {} 块中声明。写的时候想清楚:这个任务需要哪些参数?哪些是必须的,哪些有默认值?
task fastqc {
input {
File read1
File? read2 # 可选的(问号表示可选)
String sample_name # 必须提供
Int threads = 4 # 带默认值
}
command {
fastqc -t ${threads} ${read1} ${read2}
}
}
? 标记可选输入,用 = 设置默认值。Cromwell 会在运行时自动补全。我曾经碰到过一个案例,忘记给线程数设默认值,结果每次跑都卡在同一个错误上,排查半天才发现是 Cromwell 在等用户输入一个没传的参数。
3.2.3 输出声明的写法
输出声明在 task 底部的 output {} 块中。关键点:输出的文件路径,要与 command 块中实际生成的文件名一致。
task bwa_mem {
input {
File ref
File read1
File read2
}
command {
bwa mem ${ref} ${read1} ${read2} > aligned.sam
}
output {
File aligned_sam = "aligned.sam"
}
}
注意这里的 aligned.sam。WDL 会认为这个文件就是任务的最终产物。如果 command 里生成的文件名跟 output 声明的不一致,Cromwell 会报错说输出文件找不到。
/output/result.bam。但 Cromwell 有自己的工作目录,你把路径写死了,它就找不到了。正确的做法是只写相对路径或文件名,Cromwell 会自动处理工作目录挂载。
3.3 运行时配置
运行时配置,就是告诉 Cromwell 这个任务需要用多少内存、多少 CPU、跑在什么 Docker 镜像里。这部分写在 task 的 runtime {} 块中。
3.3.1 常用运行时属性
| 属性 | 说明 | 示例 |
|---|---|---|
docker |
指定 Docker 镜像 | "ubuntu:latest" |
memory |
内存大小 | "4 GB" |
cpu |
CPU 核数 | 2 |
disks |
附加磁盘 | "local-disk 20 HDD" |
maxRetries |
任务失败后的重试次数 | 3 |
continueOnReturnCode |
允许非 0 退出码继续执行 | true |
3.3.2 典型运行时配置示例
task star_alignment {
input {
File genomeDir
Array[File] readFiles
String prefix
}
command {
STAR --genomeDir ${genomeDir} \
--readFilesIn ${sep=' ' readFiles} \
--outFileNamePrefix ${prefix} \
--runThreadN ${cpu} \
--outSAMtype BAM SortedByCoordinate
}
output {
File aligned_bam = "${prefix}Aligned.sortedByCoord.out.bam"
}
runtime {
docker: "quay.io/biocontainers/star:2.7.10a--h9ee0642_0"
memory: "32 GB"
cpu: 8
disks: "local-disk 100 SSD"
maxRetries: 2
}
}
cpu 变量,可以直接在 command 里用 ${cpu} 引用。这样你只需要在 runtime 里改数字,command 里的参数会自动跟着变。我在项目里就靠这个,一次性调优了所有任务的资源配置。
3.3.3 为什么推荐用 Docker 镜像?
想想看,如果没有 Docker,你要在每台机器上装 STAR、BWA、FastQC……版本还得一致。有了 Docker 镜像,Cromwell 会在任务开始前自动拉取镜像,跑完自动清理,干净利落。
我个人习惯把所有工具打包成一个镜像,比如 bioinfo-pipeline:v1.0,然后在所有 task 里共用这个镜像。这样做的好处是:你只需要维护一个 Dockerfile,不用为每个任务单独写镜像。
docker run 启动容器,任务之间互不干扰。
3.4 本章知识结构 SVG
下面这张图总结了我们刚刚讲的核心概念。它展示了 WDL 脚本的骨架:工作流如何包含任务,任务如何由输入、命令、输出和运行时四个部分组成,以及它们之间的数据流向。
3.5 避坑总结
写 WDL 的时候,有三个地方最容易出问题,我列出来提醒一下自己,也提醒你:
- 路径问题:永远不要在 output 里写绝对路径。用相对路径,Cromwell 会自动处理。
- 类型不匹配:声明了
File却传了String,Cromwell 不会帮你隐式转换。严格匹配类型。 - 命令换行:WDL 里用反斜杠
\续行,不要用^或别的符号。我曾经在一个 STAP 脚本里用了分号分隔命令,结果 Cromwell 把整行当成一条命令执行了。
continueOnReturnCode: true,即使 command 跑炸了(非 0 退出),Cromwell 也不会停。这对于有些临时错误(比如网络抖动)很有用,但不要滥用,否则会掩盖真正的 bug。