工作流基础:任务定义、工作流结构与输入输出声明

真正开始写WDL之前,得先把骨架搭明白。说白了,WDL脚本就两大块:任务(Task)和工作流(Workflow)。我刚开始接触的时候,总觉得这俩东西差不多,写起来经常混在一起。直到有一次,我把数据校验和压缩的逻辑塞进同一个任务里,结果耦合得死死的,改一个就要牵连另一个……后来我学乖了:Task是执行单元,Workflow是编排单元,分工特别清晰。

嗯,我们先看看它们长什么样。

WDL 工作流与任务核心结构 WDL 工作流 数据输入 (input) Task: 数据校验 Task: 格式转换 Task: 压缩输出 数据输出 (output) 工作流串联多个任务,任务之间通过输入输出传递数据
个人习惯:我写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 脚本就算入门了。