3. 流程基本概念:进程、通道、工作流
刚接触 Nextflow 的时候,我其实被这三个名字绕晕过——进程、通道、工作流,听着都挺像。但用了一段时间才发现,这三兄弟分工特别清楚。说白了,进程是干活的人,通道是传话的信使,工作流是总指挥。今天咱们就把它们一个一个掰开来看。
3.1 进程(process)
进程是 Nextflow 里最基础的执行单元。每个进程会拿到输入数据,跑一段脚本,然后产生输出。进程的脚本可以是任何你能在命令行里敲的东西——Bash、Python、R、甚至编译好的二进制。我见过最极端的例子,有人把整个 Snakemake 流程直接嵌进一个 process 里跑(虽然我不推荐这么干)。
一个典型的 process 结构长这样:
process SAY_HELLO {
input:
val name
output:
path "greeting.txt"
script:
"""
echo "Hello, $name!" > greeting.txt
"""
}
注意看:input 和 output 定义了数据的流向,script 部分才是真正执行的命令。指令(directives)比如 cpus、memory 可以放在 process 名字后面,用来告诉调度器你要多少资源。
executor 'local',结果一上集群就报错。记住,process 本身不关心你在哪跑,但指令配置如果写死了执行器,迁移就会出问题。正确的做法是在 nextflow.config 里统一设执行器,process 里只写算法相关的逻辑。
进程还可以包含多种脚本风格,比如 shell、python、R。我个人习惯用 script: 块写 Bash,遇到复杂数据处理就外挂一个 Python 脚本,通过 module 方式调用。
3.2 通道(channel)
通道是连接进程的管道,负责在进程之间传递数据。你想想看,如果没有通道,每个进程产出的结果就得手动写文件路径去传递,那维护起来得多痛苦。Nextflow 的通道系统帮我们自动管理了数据依赖和并发。
通道有两种基本类型:
| 类型 | 描述 | 创建示例 |
|---|---|---|
| 队列通道 | 可以包含多个元素,进程消费一个少一个(类似队列) | Channel.from(1,2,3) |
| 值通道 | 只会保存一个值,可以被无限次读取但不会消耗掉 | Channel.value("ref.fa") |
值通道我特别常用——用来传递参考基因组、参数文件这些全局不变的资源。队列通道则用来传递样本列表,一个样本进一个元素,Nextflow 会自动做并行。
实际创建通道的方法有很多,下面列几个最常见的:
// 从列表创建队列通道
Channel.from([1,2,3,4])
// 从文件路径创建值通道
Channel.value(params.outdir)
// 从通配符批量创建队列通道
Channel.fromPath("data/*.fastq")
// 从文件内容逐行读取
Channel.fromPath("samples.csv").splitCsv(header:true)
Channel.value 传就特别自然。如果全用队列通道,你会发现参考文件被莫名其妙消费掉了——别问我是怎么知道的。
3.3 工作流(workflow)
工作流是 Nextflow DSL2 引入的一等公民。它像一个舞台导演,把进程和通道组织起来,定义执行的顺序和数据的路由。从 DSL2 开始,Nextflow 鼓励我们用 workflow {} 块来显式声明流程结构。
一个简单的工作流示例:
workflow {
// 创建输入通道
ch_samples = Channel.fromPath("data/*.fastq")
// 调用进程,传入通道
FASTQC(ch_samples)
TRIMMOMATIC(ch_samples)
// 利用 .out 获取上一个进程的输出
ch_trimmed = TRIMMOMATIC.out.reads
// 传给下游
STAR_ALIGN(ch_trimmed)
}
你发现没有,工作流里直接调用进程的名字,像调用函数一样。每个进程的 .out 属性会返回它的输出通道,你可以把通道拉出来继续传。这样做的好处是:流程的拓扑结构一目了然,修改某一步的输出路径不会影响其他进程的代码。
nextflow.config 和 params 里。这样其他同事来读流程,只需要看 workflow 块就能理解全貌。
工作流还支持嵌套、复用,你可以把一组功能相近的进程打包成一个子工作流,然后像普通进程一样调用。这个玩法在大型项目中特别有用——我管它叫「模块化积木」。不过那是更后面的内容了,今天先把基础打牢。
3.4 三者的关系
用一个表格来总结它们的角色:
| 概念 | 类比 | 作用 | 典型代码/引用 |
|---|---|---|---|
| 进程 process | 工人 | 执行具体计算,一个容器/任务 | process ALIGN { ... } |
| 通道 channel | 传送带 | 在进程间传输数据,实现解耦 | Channel.fromPath(...) |
| 工作流 workflow | 车间主任 | 编排进程调用关系,定义流程结构 | workflow { ... } |
下面这张图描绘了三者如何配合起来跑一个典型的数据处理管线:
你看,Workflow 先调起 FACSTQC 和 TRIMMOMATIC 两个进程(可以并行),然后它们各自的输出通道把数据传给下游的 ALIGN 进程。假如没有通道,我们必须手动写中间文件路径,换个人来读代码就得从头追踪文件去哪了。
嗯,到这里三个基本概念就理清了。我刚开始学的时候,总是搞混 process 和 workflow 的职责边界,后来想通一个窍门:如果一件事是“怎么做”(执行什么命令),写进 process;如果一件事是“什么时候做”(顺序、条件、并行),写进 workflow。以后你自己写流程时也可以试试这个原则。