4. Nextflow 基本概念:流程、通道、进程

好,咱们现在开始正式接触 Nextflow 的核心。说实话,我第一次看 Nextflow 的文档时,被一堆术语搞得有点晕。什么 Process、Channel、Workflow,感觉像是在学一门新语言。

但后来我发现,别被它们唬住。这三个东西,说白了就是 Nextflow 世界的“砖块”、“水管”和“施工图”。你只要搞懂它们各自是干嘛的,整个框架就清晰了。

4.1 流程(Workflow)—— 总指挥

流程,也就是 Workflow,是整个分析任务的骨架。它不干具体的活,它只负责“调度”。它决定哪个 Process 先跑,哪个 Process 后跑,数据怎么流动。

我个人习惯把 Workflow 看作一个“总指挥”。它把各个 Process 串起来,定义好执行顺序。注意,Nextflow 的 Workflow 默认是声明式的。什么意思?就是你只管告诉它“我要做什么”,而不用管“具体怎么做”。

举个例子,一个标准的单细胞分析流程,在 Workflow 里看起来可能是这样的:

workflow {
    // 输入通道(后面会讲)
    Channel.fromPath(params.fastq) | set { raw_reads }

    // 调用进程
    FASTQC(raw_reads)
    STAR_ALIGN(FASTQC.out.trimmed_reads)
    CELL_BARCODING(STAR_ALIGN.out.bam)
}

你看,这里没有 for 循环,没有 if-else。Nextflow 会自动分析数据依赖关系,决定哪个任务并行、哪个任务等待。

我记得刚开始迁移一个 10x 流程时,我习惯性地写了个 Python 脚本来控制流程顺序。结果 Nextflow 直接告诉我:你定义的依赖关系冲突了。你想想看,如果用人脑去管理几十上百个样本、多个软件步骤的复杂依赖,很容易出错。Workflow 这块,Nextflow 比我们靠谱得多。

关键点: Workflow 块内部调用 Process 时,会自动推导依赖。你不必手动指定“前一个完成后再跑下一个”,除非你有特殊需求。

4.2 通道(Channel)—— 数据运输带

通道,是 Nextflow 里我最喜欢的设计。它像个“智能数据运输带”。

在一个流程里,数据怎么从进程 A 传到进程 B?就是通过通道。通道可以发送文件、值、甚至整个目录。

通道分两种:值通道(Value Channel)和队列通道(Queue Channel)。

特性 值通道 队列通道
内容 单个值,只发送一次 元素队列,逐个消费
使用场景 参数、全局配置 样本列表、文件列表
行为 无论读多少次,都返回同一值 每个元素只能被消费一次
典型创建方式 Channel.value() Channel.fromPath() .fromList()

嗯,这里要注意队列通道的一个坑:它的元素是“一次性的”。如果两个进程都从同一个队列通道读取,那只有一个进程能拿到数据,另一个会空手而归。解决办法是使用 .into() 来分叉通道:

// 错误的做法:两个进程抢一个通道
Channel.fromPath("data/*.fastq") | set { raw }
PROCESS_A(raw)
PROCESS_B(raw)  // 这里 PROCESS_B 什么都拿不到

// 正确的做法:将通道分叉
Channel.fromPath("data/*.fastq")
    | into { raw_a; raw_b }
PROCESS_A(raw_a)
PROCESS_B(raw_b)

我曾经在做一个多组学流程时,就因为没分叉通道,导致下游进程莫名其妙丢数据。排查了半天,最后发现是通道被提前消费完了。从那以后,我养成了习惯:只要一个通道供给多个 Process,就立刻用 .into() 分叉。

4.3 进程(Process)—— 真正的干活人

进程,就是实际执行计算任务的单元。每个 Process 里定义:用什么命令、用什么软件、需要多少资源。

一个典型的进程定义包含三个核心部分:

process FASTQC {
    // 1. 指令区(Directives)—— 告诉调度器怎么跑
    label 'short'
    cpus 2
    memory 4.GB

    // 2. 输入定义区(Input)
    input:
    path fastq_file

    // 3. 输出定义区(Output)
    output:
    path "*.zip" into qc_report

    // 4. 脚本区(Script)—— 实际执行的命令
    script:
    """
    fastqc -t ${task.cpus} ${fastq_file}
    """
}

你看,一个 Process 自己就知道它要多大内存、几个 CPU。你不用在集群配置里到处找。这一点对单细胞流程特别重要,因为不同工具的资源需求差距极大。

小技巧: 在脚本区里,我习惯用 Groovy 字符串插值 ${...} 来引用变量。注意,如果命令里有用到 shell 变量(比如 $HOME),记得转义:\$HOME。

4.4 三者如何配合?

咱们来看看这三个概念是怎么在真实流程里跑起来的。我画了张图,帮大家直观理解:

Nextflow 核心概念关系图:流程、通道、进程 Nextflow 核心概念:流程、通道、进程 Workflow (流程) Channel.fromPath() Channel.value() Process A (FASTQC) input: path fastq output: qc_report Process B (STAR) input: trimmed_reads output: bam qc_report (queue) bam (queue) 流程驱动:Workflow 定义依赖 → Channel 传递数据 → Process 执行任务

这张图展示了一个典型的 Nextflow 流程。从上到下:Workflow 先创建通道,通道再把数据喂给 Process,Process 加工后通过输出通道交给下一个 Process。

你想想看,如果没有通道,你得手动写文件路径传递、检查文件是否存在、处理中间文件名冲突。有了通道,这些杂活全交给框架了。我当时第一次跑通一个多步骤流程时,最大的感受就是:终于不用自己写任务调度脚本了。

避坑指南: 我曾经在 Process 里直接写死了输入路径,结果 Nextflow 缓存系统搞混了,怎么跑都是旧结果。记住:Process 的所有输入数据都必须通过 input 块声明,哪怕是一个简单到不行的数字。这样 Nextflow 才能正确追踪依赖。

4.5 小结

到目前为止,我们拆解了 Nextflow 的三个核心概念:

  • 流程(Workflow):定义整个分析流程的步骤和依赖关系。像个总指挥官。
  • 通道(Channel):负责在进程之间传递数据。注意区分值通道和队列通道,别让数据被提前消费掉。
  • 进程(Process):实际执行计算任务的单元。资源需求、输入输出都在这里定义。

这三个概念互相配合,构成了 Nextflow 的骨架。接下来的章节,咱们会深入到进程编写细节和通道操作技巧。等你真正动手写几个流程后,这些概念就会变成你的肌肉记忆。