4、进程(process)与executor:进程定义、输入输出声明、executor选择

说实话,我刚开始接触 Nextflow 那会儿,觉得它就是个能写循环的 shell 脚本工具。但真正用起来才发现,process 才是整个流水线的灵魂。每个 process 就是一个独立的任务单元,你能控制它跑什么、怎么跑、跑在哪里。而 executor,说白了就是告诉你“这活儿该交给谁干”。这一节我就把这两个核心概念掰开揉碎了讲清楚。

4.1 进程(process)长什么样?

一个 process 的基本框架很简单:

process myProcess {
    // 指令区(directives)
    executor 'local'
    cpus 2
    memory '4 GB'

    // 输入声明
    input:
    path input_file

    // 输出声明
    output:
    path output_file

    // 执行脚本
    script:
    """
    your_command --input ${input_file} --output ${output_file}
    """
}

我个人的习惯是先写指令区,再写输入输出,最后写脚本。这样逻辑顺一些。不然你脚本写到一半发现忘了声明资源,还得回头补,挺打断思路的。

4.2 输入输出声明——别小看这两行

输入输出声明是 Nextflow 的“契约机制”。你声明了 input,Nextflow 就会自动帮你组织数据、处理通道;你声明了 output,它就能把产物传递给下游。这是个典型的声明式编程思想——你只说“我要什么”,不用写复杂的文件传递逻辑。

4.2.1 常用的输入类型

类型 说明 示例
path 文件或目录路径 path input_file
val 值类型(字符串、数字等) val species_id
env 环境变量 env PATH
stdin 标准输入 stdin
tuple 组合多种类型 tuple val(id), path(file)

曾经有个项目,我光顾着声明 path 却忘了处理目录结构,结果下游 process 拿到的文件路径全乱了。后来我养成了一个习惯:在输入声明里加上 .sort() 或者用 .map{ it -> ... } 做一次快速校验,确保文件列表的顺序和内容是对的。

4.2.2 输出声明与多输出

一个 process 可以输出一个文件、一个值,也可以是多个。用 emit 给输出打标签是个好做法:

process alignReads {
    input:
    path reads

    output:
    path "*.bam" into bam_output
    path "*.bai" into bai_output

    script:
    """
    bwa mem reference.fa ${reads} | samtools sort > sorted.bam
    samtools index sorted.bam
    """
}

这里我用了 into 把不同输出送入不同的通道。这样下游的 process variantCalling 就能单独拿到 bam 文件和 index 文件,不用自己再去解析目录。

小技巧:如果输出文件很多很杂,可以用 path("${id}/*") 配合通配符一次性捕获。但注意,这样会丢失文件的结构信息,我一般只在调试阶段这么用。

4.3 executor——你的任务交给谁?

executor 决定了 process 的脚本会被提交到哪个计算环境运行。Nextflow 支持多种 executor,我列一下最常用的:

Executor 适用场景 特点
local 单机调试、小数据量 直接在本地 shell 执行,无需额外配置
slurm HPC 集群 使用 SLURM 调度器,适合大批量并行作业
lsf IBM LSF 集群 与 SLURM 类似,但某些老集群还在用
sge Sun Grid Engine 很多高校机房还在用
awsbatch AWS 云 配合 AWS Batch,资源弹性伸缩
k8s Kubernetes 集群 容器化部署,适合 DevOps 团队
google-lifesciences Google Cloud 专为生信设计的云 executor

4.3.1 怎么选 executor?

我的经验就一句话:开发测试用 local,上线生产用集群。但这里面有几个坑,我给你列一下:

  1. local executor 不检查资源限制。你在指令里写 cpus 8,local 模式并不会真的帮你绑定 CPU,所有进程共享宿主机资源。我曾经在测试时写了个 64 核的并行任务,结果笔记本风扇直接起飞。嗯,从那以后我每次 local 测试都会先加 maxForks 2 限制并发数。
  2. slurm executor 需要你正确配置分区和队列。很多初学者只改了 executor 名称,忘了写 queue 或者 clusterOptions,结果任务一直排队。
  3. 云 executor(awsbatch / google-lifesciences)要额外配置 IAM 权限和存储挂载。这个相对麻烦,但一旦配好了,弹性伸缩真的爽。
注意:executor 可以在 nextflow.config 里全局设置,也可以在每个 process 指令区单独覆盖。我建议:全局用 HPC 的 executor,个别需要特殊资源(比如 GPU 加速)的 process 单独指定。这样既统一,又灵活。
// nextflow.config 示例
process {
    executor = 'slurm'
    queue = 'normal'
    // 针对 gpu 进程单独覆盖
    withName: gpuProcess {
        executor = 'local'
        cpus = 1
        memory = '8 GB'
    }
}

4.4 进程与 executor 的关系图

下面这张图帮你理清 process 和 executor 在流水线中的位置:

进程(process)与 executor 关系图 输入通道 process 指令区 · 输入 · 脚本 · 输出 输出通道 executor local / slurm / awsbatch ... 关键关系说明 · 每个 process 关联一个 executor,决定脚本执行在哪 · 输入通道提供文件/值,脚本处理完后放入输出通道 · 多个 process 通过通道链式连接,组成完整流水线

这张图里,executor 位于 process 下方,像个“底座”一样支撑着 process 的运行。你想想看,不指定 executor,系统就会用默认的 local,而且 Nextflow 会自动查找环境中的调度器(比如你机器上装了 SLURM 的话,它可能会自动切换)。我个人的建议是:显式写死在 nextflow.config 里,避免因环境差异造成意外。

4.5 实战中的避坑指南

  • 路径问题:process 默认的工作目录是 work/ 下的一个散列目录。你如果直接写绝对路径,很可能会出权限问题。用 path 类型,Nextflow 会自动帮你 staging 文件。
  • 资源声明不等同于硬限制:在 local executor 下,你写 cpus 4 只是给调度器一个提示,并不会真的限制 CPU 使用。在 SLURM 下,它会自动转成 #SBATCH --cpus-per-task=4。务必确认你的集群配置。
  • 输出文件要显式捕获:如果脚本产生了临时文件但你没写在 output 里,这些文件会在 process 结束后被清理掉。我曾经掉过这个坑,调试了半天才发现中间产物被删了。
  • executor 切换时的资源匹配:从 local 切到 slurm,可能你的 memory 写的是 '4 GB',但集群默认单位是 MB,Nextflow 会自动换算,不过最好显式写单位。
我的小习惯:在每个 process 开始前,我会在 script: 里加一行 echo "Starting $(date)",这样既能方便查看日志,也能确认 process 是否真的跑起来了。

好了,这一节的内容其实很硬核,但理解了 process 的结构和 executor 的选型,你的流水线才真正有“骨架”。你想想看,没有 process 通道就到不了下一个步骤,没有 executor 代码就无处执行。这两个概念搞定了,后面学 channel、operator 这些就要轻松多了。