第3章:配置文件基础——WDL与Cromwell.conf的核心结构,常见配置错误示例
3.1 WDL到底在描述什么?
WDL,全称是Workflow Description Language。说白了,它就是一种专门用来写生物信息学流程的“剧本”。
你得告诉Cromwell:第一步跑什么软件、第二步用什么参数、最后输出哪些文件。WDL就是干这个的。我最早接触WDL时,以为它和Shell脚本差不多,结果发现它比你想象中“较真”得多。
3.2 WDL的核心结构:三个你必须搞懂的部分
一个标准的WDL文件,通常由三大块组成。我习惯把它们比作“一剧三幕”。
- workflow:定义整个流程的步骤顺序和数据流向。
- task:定义每一个具体步骤要执行什么命令、需要什么资源。
- 结构关系:workflow调用task,task之间通过输入输出来串联。
来看一个最简单的例子:
workflow my_workflow {
call task_A
call task_B { input: in_file = task_A.out_file }
}
task task_A {
command {
echo "hello" > output.txt
}
output {
File out_file = "output.txt"
}
runtime {
docker: "ubuntu:latest"
}
}
task task_B {
File in_file
command {
wc -l ${in_file}
}
output {
Int line_count = read_int(stdout())
}
runtime {
docker: "ubuntu:latest"
}
}
嗯,这里要注意:每个task里的command块,Cromwell会把它当成一段独立的脚本来执行。所以你在里面写的路径、变量、重定向,都得是Linux语法。
我见过最坑的一个错误,是有人把Windows路径的写法直接搬到了WDL里,比如用反斜杠。结果任务跑起来就报“文件不存在”。你想想看,Cromwell毕竟跑在Linux容器里,路径这玩意儿可骗不了人。
3.3 避坑点:WDL变量作用域和类型声明
WDL是强类型的。什么意思?就是你声明变量时必须说清楚它是File还是String还是Int。
| 类型 | 示例 | 说明 |
|---|---|---|
| File | File input_bam | 文件路径,Cromwell会自动处理上传下载 |
| String | String sample_name | 普通字符串,注意不要和File混淆 |
| Int | Int threads = 4 | 整数,常用于runtime参数 |
| Float | Float mem_gb | 浮点数,别和Int搞混 |
| Array | Array[File] bam_list | 数组,适合批量处理 |
| Map | Map[String, String] config | 键值对,很少用到但了解下没坏处 |
我曾经在一个项目里,把File output_bam错误地声明成了String output_bam。结果任务跑完后,output_bam变量存的只是文件名“a.bam”,而不是完整路径。下游任务根本找不到这个文件。折腾了半小时才发现——类型声明搞错了。
小提示:WDL中的File类型支持通配符,比如*.bam。如果你想让Cromwell帮你收集所有bam文件,可以用Array[File] + 通配符。
3.4 Cromwell.conf:你的后端配置总开关
Cromwell本身只是一个“调度大脑”。真要执行任务,你得告诉它用哪个后端——是本地跑、SLURM集群、还是Google Cloud Batch?
所有这些信息,都放在一个叫cromwell.conf的文件里。我习惯叫它“全局配置单”。
3.5 一个标准的Cromwell.conf长什么样?
以混合云环境下最常用的Google Cloud Batch backend为例:
include required(classpath("application"))
backend {
default = "PBS"
providers {
PBS {
actor-factory = "cromwell.backend.impl.sfs.config.ConfigBackendLifecycleActorFactory"
config {
run-in-context = true
runtime-attributes = """
String docker
String cpu
String memory
"""
submit = "qsub -l nodes=1:ppn=${cpu},mem=${memory} -o ${outdir}/stdout -e ${outdir}/stderr ${script}"
kill = "qdel ${job_id}"
check-alive = "qstat ${job_id}"
job-id-regex = "(\\d+)"
}
}
}
}
这一段,是整个配置的核心。我最初配置时,一直搞不明白submit里的${script}是哪儿来的。后来才搞清楚:Cromwell会自动根据你的WDL command块生成一个shell脚本,然后把这个脚本路径传进去。
所以,submit字段其实就是你向调度器提交作业的命令模板。不同类型的集群,命令格式完全不一样。
| 调度器类型 | submit示例 | 注意事项 |
|---|---|---|
| SLURM | sbatch --cpus-per-task=${cpu} --mem=${memory} | 记得加--wrap参数 |
| PBS | qsub -l nodes=1:ppn=${cpu} | 输出文件要重定向 |
| LSF | bsub -n ${cpu} -M ${memory} | 要注意资源单位 |
| AWS Batch | Cromwell内置支持,无需手写submit | 配置key和secret |
3.6 常见配置错误示例汇总
这些bug,几乎每个Cromwell用户都踩过。我把它们列出来,你对照着检查自己的配置。
- runtime属性不匹配
WDL里声明了
runtime { cpu: 4 },但conf里把${cpu}写成了${cpus}。结果:任务提交时,变量值为空,调度器报错“invalid resource request”。
- 路径分隔符混用
在WDL里写
/data\sample.bam这种混搭路径。结果:Linux容器执行时,反斜杠被当成普通字符,文件找不到。
- docker镜像不指定tag
只写了
docker: "ubuntu",没有写版本tag。结果:每次执行拉取的镜像版本可能不同,导致结果不可重复。
- filesystem挂载错
本地调试时用
localbackend跑得好好的,换成PBS后忘了配共享存储。结果:任务提交到计算节点,但输入文件在登录节点上,根本读不到。
- job-id-regex写错
PBS的作业ID是
12345.master,但正则写成了(\\d+)。结果:Cromwell拿不到完整的作业ID,后续的
kill和check-alive都失效。
警告:千万别在cromwell.conf里直接写明文密码或token。我曾经见过有人把API密钥直接写在配置文件里,结果git push到公共仓库……那场面,简直灾难。建议用环境变量或密钥管理服务。
3.7 画一张图理清关系
为了帮你把WDL和Cromwell.conf的整体关系看清,我画了张流程示意图。
图里表达的意思很清楚:WDL定义了“做什么”,Cromwell.conf定义了“在哪里做”和“怎么做”。中间那个runtime-attributes是沟通这两者的桥梁。如果你在这桥上写错了变量名,任务就跑不下去。
3.8 一个我记得的真实教训
有一次帮客户配置混合云,PBS集群上跑基因组变异检测。WDL里声明了runtime { memory: "8 GB" },conf里也写了qsub -l mem=${memory}。按理说没问题吧?
结果任务提交后,PBS一直报“Invalid resource specification: mem=8 GB”。我查了半天才发现——PBS的mem参数单位默认是kb,后面带空格和“GB”会被解析成非法字符。正确的写法应该是mem=8192mb。从那以后,我养成了一个习惯:所有资源单位,尽量在conf里写死数值,不要从WDL里直接传字符串。
核心原则:配置文件中的变量替换只做字符串拼接,不做单位转换或类型校验。你的每一个${cpu}、${memory},最终变成什么字符串,完全取决于WDL里的runtime块和conf里的submit模板。两者必须对齐。