第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用户都踩过。我把它们列出来,你对照着检查自己的配置。

  1. runtime属性不匹配

    WDL里声明了runtime { cpu: 4 },但conf里把${cpu}写成了${cpus}。

    结果:任务提交时,变量值为空,调度器报错“invalid resource request”。

  2. 路径分隔符混用

    在WDL里写/data\sample.bam这种混搭路径。

    结果:Linux容器执行时,反斜杠被当成普通字符,文件找不到。

  3. docker镜像不指定tag

    只写了docker: "ubuntu",没有写版本tag。

    结果:每次执行拉取的镜像版本可能不同,导致结果不可重复。

  4. filesystem挂载错

    本地调试时用local backend跑得好好的,换成PBS后忘了配共享存储。

    结果:任务提交到计算节点,但输入文件在登录节点上,根本读不到。

  5. 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核心交互流程 WDL 工作流定义 workflow { ... } task { command, runtime } 变量类型 + 输入输出 Cromwell.conf 后端配置 backend.providers { ... } runtime-attributes 映射 submit, kill, job-id-regex 指定runtime属性 调度器集群 SLURM / PBS / LSF / AWS Batch 资源分配 + 排队 + 执行 提交命令 & 资源请求 执行容器 / VM Docker 镜像(runtime指定) 运行command + 产生输出 分发任务到节点 Cromwell 收集输出 + 触发下游任务 根据WDL workflow定义的依赖关系顺序执行 返回结果路径 注意:runtime属性名必须与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模板。两者必须对齐。