核心概念:流程 workflow、进程 process、通道 channel 的基本概念与关系
刚开始接触 Nextflow,你可能会被这三个词绕晕。我把它们拆开讲,但你先记住一句话:通道是管道,进程是水龙头,流程是整条流水线。
说白了,这三个东西就是管线的骨架。我早期带团队做多组学项目时,很多人把精力全花在写进程上,结果通道没配好,数据流全乱了。你想想看,数据在管道里跑偏了,下游的分析做得再好也没用。
进程(process)—— 管线里的执行单元
进程就是你要跑的计算步骤。比如比对、定量、质控,每个步骤都是独立的进程。我习惯把进程比作一个「黑盒子」—— 你给我输入,我处理完给你输出,里面具体怎么折腾我不管。
看一个最简单的进程长什么样:
process sayHello {
script:
"""
echo "Hello from Nextflow!"
"""
}
这代码很直白。进程内部只做一件事:运行脚本。但要注意,进程之间不能共享内存或变量。每个进程都是独立的,数据只能通过通道传递。
嗯,这里要注意:进程的 script 块里写的是 Bash 命令,它会在隔离的工作目录里跑。这意味着你不用担心路径污染,也不怕不同进程之间互相干扰。
进程还可以定义资源需求:
process alignReads {
cpus 8
memory 32.GB
time 2.h
script:
"""
bwa mem -t ${task.cpus} ref.fa reads.fq > aligned.sam
"""
}
进程的核心属性:
- 唯一性:每个进程在管线里只能定义一次
- 隔离性:进程之间不共享文件系统(除工作目录外)
- 并行性:同一进程的不同实例会自动并行执行
- 有状态:进程执行失败可以自动重试
通道(channel)—— 数据的输送带
现实里,管线的数据不是一块儿全扔进来的。样本都是一个个来的,有的快有的慢。通道就是帮你管理这些异步数据的。
通道分两种:
- 队列通道(Queue channel):一次性消费,数据只能被一个进程读取
- 值通道(Value channel):反复使用,所有进程都能读
我举个例子,你马上就能懂:
// 队列通道 —— 数据流过去就没了
samples_ch = Channel.fromPath("data/*.fastq")
// 值通道 —— 一直存在,谁都可以读
genome_ch = Channel.value("ref/hg38.fa")
我在项目中遇到过最坑的一次:一个人用队列通道传参考基因组,结果跑了10个样本用了10次下载。你想想看,这得多慢。用值通道,一次定义,所有进程共用。
通道的常见操作:
| 操作 | 说明 | 示例 |
|---|---|---|
fromPath |
从文件路径创建通道 | Channel.fromPath("data/*.fastq") |
fromList |
从列表创建通道 | Channel.fromList([1, 2, 3]) |
map |
转换通道中的元素 | ch.map { it.toUpperCase() } |
filter |
过滤元素 | ch.filter { it.size() > 0 } |
combine |
合并多个通道 | ch1.combine(ch2) |
我的一个习惯:定义通道名时,我偏好加个 _ch 后缀。比如 reads_ch、genome_ch。这样在几百行代码里扫一眼就能分清通道和变量。
流程(workflow)—— 把一切串起来
流程就是定义「谁先干、谁后干、数据怎么流动」。在 Nextflow DSL2 里,流程是一个显式的块,里面声明进程的调用关系。
看一个完整的例子:
// 定义数据源
samples_ch = Channel.fromPath("data/*.fastq")
genome_ch = Channel.value("ref/hg38.fa")
// 定义流程
workflow {
trimmed_ch = fastp(samples_ch)
aligned_ch = bwaMem(trimmed_ch, genome_ch)
sorted_ch = samtoolsSort(aligned_ch)
metrics_ch = samtoolsStats(sorted_ch)
}
// 定义进程
process fastp {
input:
path reads
output:
path "trimmed_*.fastq"
script:
"""
fastp -i $reads -o trimmed_${reads}
"""
}
process bwaMem {
input:
path reads
path genome
output:
path "aligned.sam"
script:
"""
bwa mem -t 2 $genome $reads > aligned.sam
"""
}
process samtoolsSort {
input:
path sam
output:
path "sorted.bam"
script:
"""
samtools sort $sam -o sorted.bam
"""
}
process samtoolsStats {
input:
path bam
output:
path "stats.txt"
script:
"""
samtools stats $bam > stats.txt
"""
}
这里有个关键点:进程的输入输出类型必须和通道里的数据类型匹配。你不匹配,Nextflow 会在执行前就报错。这是好事,比运行时才发现问题强得多。
三者如何配合?
其实它们的配合模式就四个字:输入-输出-连接。
- 通道作为进程的输入
- 进程执行后产生输出
- 输出结果再包装成通道
- 通道传给下一个进程
你仔细看,Nextflow 自动帮你完成了第3步。进程的 output 定义了什么,框架就自动创建一个通道,把输出文件扔进去。你只管在流程里把进程的输出赋给变量就行。
我曾经遇到一个新手,他手动创建通道来传递进程的输出。这完全是多此一举。Nextflow 的设计哲学就是让你专注于业务逻辑,数据流是框架帮你管的。
重要提醒:不要在同一流程里重复使用队列通道。队列通道里的数据「用完即焚」,第二次用就是空的。如果多个进程需要同样的数据,用值通道,或者用 .view() 检查一下数据是否还在。
一张图看懂整体架构
下面这个 SVG 图把三者的关系和交互画出来了。我建议你多看两遍,把图的逻辑印在脑子里。
避坑指南
写管线时,我见过最多的错误就出在这三个概念的边界上。我归纳几个典型的:
- 通道复用:队列通道别想着用两次。如果非要复用,用
.buffer()或.view()先检查。 - 进程命名:同一个进程名在流程里只能用一次。但可以用
processName(index)的形式区分实例。 - 输出没定义:进程写了
output却忘了声明类型,Nextflow 会报类型不匹配。我每次都加一行output:哪怕不输出文件,也写stdout兜底。 - 路径别硬编码:进程里的输入路径别写死。用
${reads}这种变量,否则换了数据源还得改进程。
我的个人习惯:写新管线时,我会先在流程里留一行 .view() 看每个通道的内容。这样可以快速发现数据是不是对的,通道是不是空的。检查没问题再删掉这些调试行。
到这,你应该对这三个核心概念有感觉了。进程干活,通道运数据,流程安排次序。把握住这个思路,后面写复杂管线你就不会再被绕晕。