核心概念:流程 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 会在执行前就报错。这是好事,比运行时才发现问题强得多。

三者如何配合?

其实它们的配合模式就四个字:输入-输出-连接。

  1. 通道作为进程的输入
  2. 进程执行后产生输出
  3. 输出结果再包装成通道
  4. 通道传给下一个进程

你仔细看,Nextflow 自动帮你完成了第3步。进程的 output 定义了什么,框架就自动创建一个通道,把输出文件扔进去。你只管在流程里把进程的输出赋给变量就行。

我曾经遇到一个新手,他手动创建通道来传递进程的输出。这完全是多此一举。Nextflow 的设计哲学就是让你专注于业务逻辑,数据流是框架帮你管的。

重要提醒:不要在同一流程里重复使用队列通道。队列通道里的数据「用完即焚」,第二次用就是空的。如果多个进程需要同样的数据,用值通道,或者用 .view() 检查一下数据是否还在。

一张图看懂整体架构

下面这个 SVG 图把三者的关系和交互画出来了。我建议你多看两遍,把图的逻辑印在脑子里。

核心概念关系图 数据源 通道 (数据管道) 进程 (执行单元) 输出 输出自动包装为新通道 Workflow 流程(编排层) 核心逻辑 ① 数据源 → 通道 → 进程 → 新通道 → 下一进程 ② 通道解决异步数据传递问题 ③ 进程之间完全隔离,数据只能通过通道交换

避坑指南

写管线时,我见过最多的错误就出在这三个概念的边界上。我归纳几个典型的:

  • 通道复用:队列通道别想着用两次。如果非要复用,用 .buffer() 或 .view() 先检查。
  • 进程命名:同一个进程名在流程里只能用一次。但可以用 processName(index) 的形式区分实例。
  • 输出没定义:进程写了 output 却忘了声明类型,Nextflow 会报类型不匹配。我每次都加一行 output: 哪怕不输出文件,也写 stdout 兜底。
  • 路径别硬编码:进程里的输入路径别写死。用 ${reads} 这种变量,否则换了数据源还得改进程。

我的个人习惯:写新管线时,我会先在流程里留一行 .view() 看每个通道的内容。这样可以快速发现数据是不是对的,通道是不是空的。检查没问题再删掉这些调试行。

到这,你应该对这三个核心概念有感觉了。进程干活,通道运数据,流程安排次序。把握住这个思路,后面写复杂管线你就不会再被绕晕。