进程、通道、工作流:Nextflow 的三大核心角色
要说清楚 Nextflow,其实就三个词:进程(Process)、通道(Channel)、工作流(Workflow)。
我个人理解,这三者的关系就像「工厂流水线」——进程是加工工位,通道是传送带,工作流是车间主任的调度指令。先把这张图刻在脑子里,后面就好办了。
进程(Process):干活的最小单元
进程是 Nextflow 里最核心的执行单元。说白了,一个进程就是一段脚本,加上声明好的输入、输出和指令。
我在项目里见过不少新手,把整个分析流程写成一个大脚本。那可不行。正确的做法是:把每个分析步骤拆成一个独立进程。
process FASTQC {
input:
path read
output:
path "*.html" into ch_qc_report
script:
"""
fastqc ${read}
"""
}
这个例子你看,输入是一个文件路径,输出是一个 html 报告。进程内部只做一件事——运行 fastqc。干净利落。
进程的构成三要素:
- input —— 声明从哪个通道取数据
- output —— 声明结果往哪个通道发
- script —— 真正要执行的命令或脚本
比较特别的是 exec 模式。如果你只想跑一段 Groovy 代码,而不是 shell 命令,可以用它。我个人用得少,但在做简单数据转换时很顺手。
需要注意的是,进程里还能声明资源需求,比如 CPU、内存。我一直强调:资源声明别偷懒。我在生产环境中见过因为忘记声明内存,导致任务挂掉的情况。
process BWA_ALIGN {
cpus 8
memory 32.GB
time 2.h
input:
path index_dir
tuple val(sample_id), path(reads)
output:
tuple val(sample_id), path("*.bam") into ch_bam
script:
"""
bwa mem -t ${task.cpus} ${index_dir}/genome.fa ${reads[0]} ${reads[1]} | samtools sort -o ${sample_id}.bam -
"""
}
看到没?${task.cpus} 直接引用声明好的 CPU 数。这份声明不仅告诉调度器要多少资源,还在脚本里用上了,一举两得。
避坑指南:
我曾经遇到过一个问题,进程里用了绝对路径访问共享存储,结果可执行文件找不到了。后来发现是容器环境的问题。建议:优先使用 $PWD 或相对路径,或者通过配置文件挂载卷。
通道(Channel):数据流动的血管
通道是什么?我的理解是:它就像水管,把数据从一个进程送到另一个进程。但这条水管有两类。
| 类型 | 特点 | 适用场景 |
|---|---|---|
| 队列通道 | 先进先出,可绑定多个消费者 | 文件列表、批量样本 |
| 值通道 | 只能存一个值,新值覆盖旧值 | 参数、全局配置、参考基因组路径 |
你想想看,队列通道适合放样本列表,每个样本过一遍流水线。值通道呢?适合放全局参数,比如参考基因组路径,全流程通用。
创建通道的方法,常用的就这几种:
// 从文件路径列表创建队列通道
ch_samples = Channel.fromPath("data/*.fastq.gz")
// 从文件内容创建(每行一个元素)
ch_metadata = Channel.fromPath("samples.csv")
// 手动创建值通道
ch_genome = Channel.value("/data/reference/hg38.fa")
// 手动创建队列通道(多个元素)
ch_species = Channel.of("human", "mouse", "zebrafish")
我个人习惯用 Channel.fromPath() 来处理测序数据。它会自动做文件匹配,比手写列表省事多了。但要注意通配符的用法——* 匹配所有,? 匹配单个字符。
通道有一个重要特性:异步。也就是说,进程不需要等所有数据读完才开始。生产者一边往通道里放,消费者一边处理。这种设计在大规模数据下特别高效。
不过这里有个坑——通道只能消费一次。
// 错误示范:一个通道被两个进程同时消费
ch_data = Channel.fromPath("data/*.fastq")
process_A(ch_data)
process_B(ch_data) // 这里 process_A 已经消费完了
// 正确做法:分叉通道
ch_data = Channel.fromPath("data/*.fastq")
ch_data.into{ ch_A; ch_B }
process_A(ch_A)
process_B(ch_B)
我曾因为这个问题调试了半天。一个通道被两个进程绑定,结果第二个进程收不到任何数据。用 .into{} 分支一下就搞定了。
工作流(Workflow):指挥一切的大脑
工作流就是整个剧本的总纲。它告诉 Nextflow:进程按什么顺序跑,输入从哪里来,输出到哪里去。
在旧版本 Nextflow 里,工作流是通过模块文件隐式定义的。但 DSL2 引入之后,我们有了显式的 workflow 语法块。我建议你现在就用 DSL2,这是趋势。
nextflow.enable.dsl=2
workflow {
// 定义输入通道
ch_raw_reads = Channel.fromPath("data/*_R1.fastq.gz")
// 按顺序执行进程
FASTQC(ch_raw_reads)
// 进程的输出通道自动成为下一个进程的输入
TRIMMOMATIC(FASTQC.out.ch_qc_report)
// 收集最终结果
ch_results = MULTIQC.collect(TRIMMOMATIC.out.ch_bam)
}
你看,在工作流块里,进程像函数一样被调用。输入参数是通道,输出可以通过 <进程名>.out.<通道名> 来引用。
工作流内置的三个关键通道:
workflow.out—— 工作流的最终输出,可在命令行用--outdir接收workflow.log—— 运行日志params—— 用户传入的参数映射
还有一个很实用的东西:动态输入。你可以在工作流里根据条件决定用哪个通道。
workflow {
ch_input = params.use_remote
? Channel.fromPath("s3://bucket/data/*.fastq.gz")
: Channel.fromPath("local/data/*.fastq.gz")
PROCESS_DATA(ch_input)
}
这个例子,根据 params.use_remote 的真假,决定是从 S3 读取还是从本地读取。我在多个项目里用这个技巧实现了开发环境与生产环境的无缝切换。
又一个避坑指南:
我曾经在一个大型 RNA-seq 项目里,工作流里声明了 40 个进程,结果首次运行报错说「未定义的进程」。排查半天才发现是模块导入路径写错了。DSL2 下,记得用 include 声明模块:include { FASTQC; TRIMMOMATIC } from './modules'
进程、通道、工作流,这三个概念掌握了,Nextflow 的骨架就搭起来了。后面再学参数化运行、容器集成、CI/CD 部署,都是锦上添花。嗯,先把这三个吃透,我们继续。