2. 工作流基础:DSL2语法与数据流
好,咱们直接切入正题。DSL2 是什么?我个人习惯把它看成是 Nextflow 工作流的“基础设施升级版”。如果你接触过 DSL1,你会发现 DSL2 最大的变化是:把流程拆得更细了,模块化做得更好,复用性也上去了。
说白了,DSL2 就是让你写流程的时候,不再是一大坨代码堆在一起。而是像搭积木一样,每个 process 是一块积木,channel 就是连接积木的轨道。
核心观点:DSL2 不是新语言,是新的组织方式。
2.1 DSL2 的核心语法结构
一个典型的 DSL2 工作流,大概长这样:
nextflow.enable.dsl=2
process FASTQC {
input:
path reads
output:
path "*.html"
script:
"""
fastqc ${reads}
"""
}
process TRIM_GALORE {
input:
path reads
output:
path "*.fq.gz"
script:
"""
trim_galore ${reads}
"""
}
workflow {
FASTQC(ch_reads)
TRIM_GALORE(FASTQC.out)
}
你看,这个结构很清晰:先定义 process,再在 workflow 块里串联起来。每个 process 只干一件事,输入输出都明确写出来。嗯,这里要注意——process 不能直接调用另一个 process,得通过 channel 传数据。
2.2 process 定义:输入、输出、脚本
每个 process 都是一个独立的任务。它有三大核心部分:
- input 声明:定义这个任务需要什么数据。常见的有
path(文件路径)、val(值类型)、env(环境变量)。 - output 声明:定义这个任务产出的数据。可以输出多个,甚至可以用
emit给输出命名。 - script 块:真正要执行的命令。可以是 shell 脚本、Python、R 等等。
举个例子,我之前在一个 RNA-seq 项目里,需要跑 STAR 比对。process 是这样定义的:
process STAR_ALIGN {
input:
path genome_index
tuple val(sample_id), path(read1), path(read2)
output:
tuple val(sample_id), path("${sample_id}.Aligned.sortedByCoord.out.bam")
script:
"""
STAR --genomeDir ${genome_index} \
--readFilesIn ${read1} ${read2} \
--outFileNamePrefix ${sample_id}.
"""
}
这里有个小细节:我用 tuple 把 sample_id 和 reads 打包在一起。为什么要这么干?因为后续的 process 可能需要同时用到 IDs 和文件,打包传递最安全。
我的习惯:每个 process 的输出尽量加 emit 命名,这样 workflow 里调用时更清晰。比如 FASTQC.out.html 而不是 FASTQC.out[0]。
2.3 channel:数据流的管道
channel 是 Nextflow 里最核心的概念。它就是一个数据管道,数据从源头流向各个 process。channel 的类型决定了数据怎么传输、怎么分发。
| 类型 | 创建方式 | 行为特点 |
|---|---|---|
value channel |
Channel.value(x) |
只存储一个值,可以被所有 process 共享 |
queue channel |
Channel.fromPath() |
单向队列,一个元素只能被消费一次 |
channel of channels |
Channel.fromList() |
嵌套 channel,用于复杂分组 |
举个例子,你有一个样本列表,想同时跑多个分析:
// 创建 queue channel
ch_samples = Channel.fromPath("data/*.fastq.gz")
// 创建 value channel(参考基因组只传一次)
ch_ref = Channel.value("/path/to/genome.fa")
process ALIGN {
input:
path reads
path reference
script:
"""
bwa mem ${reference} ${reads} > aligned.sam
"""
}
workflow {
ALIGN(ch_samples, ch_ref)
}
你想想看,如果我用 Channel.value() 传基因组文件,它会被所有任务共享,而不会消耗掉。但 queue channel 里的 reads 文件,一个任务拿走就没了,别人拿不到。
我曾经踩过坑:在 DSL2 中,同一个 queue channel 不能在 workflow 里被两个不同的 process 直接消费。否则其中一个 process 拿不到数据,直接报空指针。解决办法是用 .view() 或者 .buffer() 预先复制一份。
2.4 workflow 块:流程编排的核心
workflow 是 DSL2 的“总调度室”。它决定数据从哪里来、经过哪些 process、最后输出什么。
基本语法也很简单:
workflow {
// 定义输入
reads_ch = Channel.fromFilePairs("data/*_{R1,R2}.fastq.gz")
// 调用 process,注意这里是直接传 channel
FASTQC(reads_ch)
TRIM_GALORE(FASTQC.out)
// 也可以显式指定输出
FASTQC.out.view { "QC report: $it" }
TRIM_GALORE.out | VIEW
}
process VIEW {
input:
path trimmed
script:
"""
ls -lh ${trimmed}
"""
}
这里有个小技巧:你可以用 | 管道符把 channel 串起来。但说实话,我在项目里用得不多,因为直接传参数更直观。
2.5 模块化设计:复用与组合
DSL2 最大的优势是模块化。你可以把常用的 process 封装成模块,放在 modules/ 目录下。然后在主流程里引用。
比如,我整理了一套 QC 模块:
// modules/fastqc.nf
process FASTQC {
input:
path reads
output:
path "*.html", emit: report
script:
"""
fastqc ${reads}
"""
}
// main.nf
include { FASTQC } from './modules/fastqc'
workflow {
reads = Channel.fromPath("data/*.fastq.gz")
FASTQC(reads)
}
这个好处太明显了。你不同项目之间可以复用模块,团队协作时每个人的 process 定义都统一。
我个人建议:把每一步的输入、输出都写清楚,尤其是输出用 emit 命名。这样 workflow 调用时,就能直接用 FASTQC.out.report 拿到结果了。
2.6 数据流的常见模式
在实际项目中,数据流不会这么简单。我列出几种我遇到过的典型模式:
- 一对多分发:一个 channel 里的元素,被多个 process 独立消费。比如同一个样本同时跑 QC 和比对。
- 多对一合并:多个 channel 的数据合并成一个 tuple,传给一个 process。比如把样本 IDs 和文件路径打包。
- 分支与合并:先分叉成两条路径,各自处理,最后再汇合。比如测序数据先过滤,然后一条做比对,一条做定量,最后对比结果。
举个例子:
workflow {
reads = Channel.fromFilePairs("data/*_{R1,R2}.fastq.gz")
// 分支1:比对
STAR_ALIGN(genome_index, reads)
// 分支2:定量
SALMON_QUANT(salmon_index, reads)
// 合并结果
COMPARE_RESULTS(STAR_ALIGN.out, SALMON_QUANT.out)
}
你想想看,这样写是不是很顺?数据流清晰,每个 process 职责单一,维护起来也方便。
下面是 DSL2 核心构件关系图:
这张图展示的是 DSL2 里最典型的数据流模式:输入 channel 先分发给两个 process,各自处理完成后,再通过不同的 channel 汇合到下游 process,最后输出结果。workflow 块负责把这些组件串联起来。
2.7 避坑指南:我踩过的几个坑
讲几个我实际遇到过的问题:
- channel 被多次消费:在 DSL2 中,同一个 queue channel 不能被两个 process 同时消费。如果你需要多路分发,用
.into{}复制。 - process 命名冲突:模块化后,同一个 process 名可能出现在不同模块里。解决办法是用
include时指定别名,比如include { FASTQC as MY_FASTQC }。 - 输出不完整:process 的 output 声明必须和实际产生的文件匹配。不匹配的话,流程会卡住。
一个小技巧:调试时多用 .view() 打印 channel 里的内容。比如 ch_samples.view { "Sample: $it" }。这样一眼就能看出数据流对不对。
好了,DSL2 的基础内容就这些。你看,核心就是 process + channel + workflow 这三个东西。记住它们的关系,后面学高阶用法就顺了。