4. DSL2 核心概念详解

这一章是整个 DSL2 的骨架。我刚开始接触 DSL2 时,总觉得 Process 和 Workflow 差不多,都是写一段代码跑一跑。但真正用它搭过几个流程后才发现,这三者——Process、Workflow、Channel——各有各的脾气。

说白了,Process 是干活的人,Workflow 是排班的经理,Channel 是传递消息的快递员。你想想看,没有 Process,活没人干;没有 Workflow,活排得乱七八糟;没有 Channel,数据传不过去。所以理解这三样,DSL2 就掌握了一半。

4.1 Process 定义与语法

Process 是 Nextflow 里最小的工作单元。一个 Process 通常对应一个命令行工具或者一段脚本。它的基本结构我总结为“三件套”:

  • 输入声明(input)——告诉流程你要用什么数据
  • 输出声明(output)——告诉流程你产出了什么
  • 脚本块(script)——真正执行的命令

举个最简单的例子,我写一个对 FASTQ 文件进行质量控制的 Process:

process fastqc {
    input:
    path reads

    output:
    path "${reads.baseName}_fastqc.html"
    path "${reads.baseName}_fastqc.zip"

    script:
    """
    fastqc ${reads}
    """
}

嗯,这里要注意:path 是 DSL2 里声明文件类型输入的最佳实践。我以前喜欢用 val 传文件名,结果跑出来的文件路径全乱套了。后来养成习惯,只要涉及文件,就用 path 声明,再用 .baseName 取文件名核心部分。

Process 的“坑”与“巧”

我曾经犯过一个低级错误:在 script 块里直接写了 fastqc $reads,没有用双引号把变量括起来。如果 reads 路径里有空格或者特殊字符,Nextflow 会直接把路径拆成两个参数,命令就崩了。所以安全写法是 fastqc "${reads}"。

另外,script 块里可以写任意 shell 命令。你甚至可以把它当成一个简单的脚本文件来写,支持 if/for 等控制流,但要注意转义。我个人习惯把复杂的多行命令用 heredoc 写,可读性高很多。

小提示:DSL2 里 Process 默认用 bash 运行,但你可以在 process 头部加 executor 'local' 或者 container 'docker://xxx' 来指定执行环境。我经常在开发阶段用 local 快速调试,上线前换成集群或者容器。

4.2 Workflow 定义与语法

Workflow 是 DSL2 的核心编排单位。你想想看,几十个 Process 堆在一起,谁先谁后?怎么串起来?这就是 Workflow 的活。

DSL2 的 Workflow 有两种定义方式:

  • 隐式 Workflow——直接在脚本末尾按顺序调用 Process
  • 显式 Workflow——用 workflow { ... } 块明确定义

显式定义更能看清数据流向。我一般都会用显式,哪怕只有两三个步骤。

workflow {
    // 先跑 fastqc
    fastqc_ch = fastqc(reads_ch)
    // 再跑 multiqc 汇总
    multiqc_ch = multiqc(fastqc_ch.collect())
}

这里有个细节:fastqc_ch 是 Process 的输出 Channel。Workflow 里每个 Process 调用都会返回一个 Channel,你可以直接把它塞给下一个 Process。但如果你想把多个输出合并成一个输入,就得用 .collect() 或者 .mix() 操作符。

Workflow 的参数与导入

Workflow 还能接受外部参数。比如我从配置文件中读入样本列表,把它传给 workflow:

params.reads = "data/*.fastq.gz"

workflow {
    reads_ch = Channel.fromPath(params.reads)
    fastqc(reads_ch)
}

我个人习惯把参数都放在 nextflow.config 里,这样脚本和参数分开,团队协作时不会互相踩到。如果你在 workflow 内部直接写死参数,下次换数据还得改脚本,太死板了。

注意:Workflow 块内不要写复杂的业务逻辑,只负责编排。我曾见过有人把数据清洗、格式转换全塞在 workflow 里,结果调试时根本分不清是哪个 Process 出的问题。一个好的习惯是:每个 Process 只干一件事,Workflow 只告诉它们顺序。

4.3 Channel 的三种类型

Channel 是 Nextflow 的数据管道。所有数据都必须通过 Channel 流动。DSL2 里 Channel 分为三种:

类型 说明 典型创建方式 常见用法
Value Channel 只存储一个值,可被重复消费 Channel.value("hello") 传递常量、配置文件、全局参数
File Channel 存储文件路径,支持通配符 Channel.fromPath("data/*.fastq") 处理大量输入文件
Queue Channel 存储队列数据,消费一次就没了 Channel.fromList(["a","b"]) 按顺序处理数据,不重复

这里我重点说一下 Value Channel。很多人以为它和普通变量差不多,其实有本质区别。Value Channel 可以被多个 Process 消费,而且每个 Process 都会拿到相同的一份副本。举个例子:

params.genome = "hg38"

workflow {
    genome_ch = Channel.value(params.genome)
    step1(genome_ch)
    step2(genome_ch) // step2 也能拿到 hg38
}

而 Queue Channel 就不一样了。比如你从 Channel.fromList(["a","b","c"]) 创建了一个队列,第一个 Process 消费了 "a","a" 就没了。如果第二个 Process 再去消费,只能拿到 "b"。所以 Queue Channel 常用于一对一的按需分配。

三种 Channel 的实战选择

我曾经在项目中犯过一个错误:用 Queue Channel 去传递一个全局参考基因组,结果跑完第一个 Process 后,参考基因组被“吃掉”了,后续 Process 都拿不到,报错说找不到文件。后来改成 Value Channel,问题解决。

那么什么时候用 File Channel?很简单:当你处理一批文件时,比如从 data/*.fastq.gz 自动发现文件,就用 Channel.fromPath()。它会返回一个 File Channel,每个元素是一个文件对象,而且支持 .name、.baseName 等属性。

一句话记清楚:

  • 全局参数、引用文件 → Value Channel
  • 大量输入文件(通配符) → File Channel
  • 按顺序消费、用完即丢 → Queue Channel

接下来我画一张图,帮你把这三者的关系理清楚。

DSL2 三大核心组件关系图 Process 工作单元 input → script → output 例:fastqc, bwa, samtools Workflow 编排引擎 连接 Process 数据流控制 参数传入 Channel 数据管道 Value Channel File Channel Queue Channel Workflow 编排 Process,Channel 传递数据,Process 执行具体任务。

从图上你能看到,Channel 既给 Process 输入,也给 Workflow 提供数据流;Workflow 负责把 Process 串起来。三者缺一不可,但又相互独立。你只需要记住它们的职责,就能在写代码时迅速判断哪里出了问题。

嗯,最后再分享一个实用技巧:调试时我经常用 .view() 方法查看 Channel 里到底有什么。比如 reads_ch.view() 会打印出所有传入的文件路径,对排查错误特别有用。别怕写多了,调试阶段多用 .view(),跑通再删掉。