进程与任务概念

这一章我们来聊聊 Nextflow 里最核心的两个抽象:进程(process)和任务(task)。很多人学了半天,代码能跑,但一问到「进程和任务到底什么关系」就卡壳。我个人习惯先把概念理清楚,再上手,后面调优才不慌。

进程的 inputs 和 outputs

进程是 Nextflow 中的基本执行单元。它接收输入,运行一段脚本,产生输出。定义进程时,input 和 output 块就是它的「接口」。

先看一个最简例子:

process sayHello {
    input:
        val x

    output:
        stdout

    script:
    """
    echo "Hello $x!"
    """
}

input 把通道里的值传进来,output 把结果发回通道。我个人在项目中遇到过不少人把 input 和 output 写反,结果数据流全乱了。其实只需要记住一句话:输入是消耗,输出是产出。

除了 val,还有 path、file、env 等修饰符。我最常用的是 path,因为它能自动处理文件暂存和链接,省很多事。

修饰符用途示例
val传递任意值(字符串、数字、布尔)input: val id
path传递文件或目录,自动符号链接input: path fastq
file与 path 类似,但旧版,不推荐input: file bam
env传递环境变量input: env HOME
stdout捕获脚本的标准输出output: stdout

我的一点小技巧:如果你要输出多个文件,用 path "${prefix}_*" 加通配符,比一个个列路径省心。

script 块:真正干活的地方

script 块就是你写 shell 脚本的地方。它支持插值,所有 input 里定义的变量都能直接用。听起来简单,但有个坑:字符串里的美元符号 $ 会被 Nextflow 解析为变量,如果你要输出字面量的 $,得用两个 $$ 转义。

process quote {
    input:
        val name

    script:
    """
    echo "Your nickname is \${name}"
    """

这个 \${name} 里用反斜杠转义 $,实际上插值得到了变量 name。如果你写 ${name},它会去取 Groovy 变量,有时会报错。我曾经在写 BWA 命令时忘加反斜杠,结果 $SAMTOOLS 变量一直解析不对,排错花了半天。

script 块还支持 if/else 条件逻辑,结合 params 能根据不同条件生成不同命令。举个例子:

process align {
    input:
        path reads

    output:
        path "aligned.bam"

    script:
    if (params.aligner == "bwa") {
        """
        bwa mem ref.fa $reads | samtools sort > aligned.bam
        """
    } else if (params.aligner == "bowtie2") {
        """
        bowtie2 -x ref -U $reads | samtools sort > aligned.bam
        """
    } else {
        error "Unknown aligner: ${params.aligner}"
    }
}

这样写比硬编码 module 灵活,也方便切换流程。不过要注意,script 块里的每个字符串前缀 """ 必须顶格写,缩进用空格。

executor 与 task:谁在干活?

进程是一个蓝图,实际执行时才产生一个个 task。每个 task 对应一个具体的工作单元,比如对一个样本跑一次比对。执行这些 task 的「引擎」叫 executor。

Nextflow 默认用 local executor,也就是在本机跑。如果你想上集群,就配置 executor 为 sge、lsf、slurm 或 awsbatch。executor 负责把 task 提交到集群,监控状态,收集结果。

任务和进程的关系,我用一张图来总结:

进程与任务关系图 进程(Process) • input / output 声明 • script 块定义命令 • 只是一个蓝图 • 不直接执行 实例化 任务(Task) • task-1: 样本A 比对 • task-2: 样本B 比对 • task-3: 样本C 比对 • ... 每个 task 独立执行 执行器(Executor) • local / sge / slurm / awsbatch • 负责调度、监控、重试 • 每个 task 由 executor 执行 由 executor 执行 核心关系: 一个进程定义 → 可产生 N 个 task → 每个 task 由 executor 独立调度 task 是调优的最小单位:内存/CPU 限制直接作用在 task 上

看到这里你应该明白了:我们配置的 cpus、memory 都是对 task 而言的,不是对整个进程。同一个进程的不同 task 可以跑在不同集群节点上,各自分配独立资源。

我曾经踩过的坑:在 process 级别设置 cpus 4,以为每个 task 都用 4 核,结果只用了 1 核——因为没理解 task 的并行度是靠 queueSize 和 each 控制的。记住:cpus 给的是单个 task 的核数,不是总并发数。

工作目录与临时文件

每个 task 运行时,Nextflow 会为它创建一个独立的工作目录。默认是 work/,里面按哈希值命名子目录。你看:

work/
  ├── 8a/6f1e.../
  │   ├── input.fastq  → 符号链接
  │   ├── output.bam
  │   ├── .command.sh
  │   ├── .command.out
  │   ├── .command.err
  │   └── .command.begin / .exitcode
  ├── 3c/2d9a.../
  └── ...

这些隐藏文件记录了脚本内容、标准输出、退出码等信息。你如果中途想查某个 task 到底跑了什么命令,去看看 .command.sh 就知道了。

临时文件怎么清理?Nextflow 不会自动删,所以长时间跑下来 work 目录可能占几百 GB。我一般用 nextflow clean -f 来清理已完成的任务目录。注意 -f 会强制删除,别在跑的时候用。

# 只删除已成功完成的任务目录
nextflow clean -f

# 保留最近的 N 次运行
nextflow clean -n 5

另外,scratch 指令可以让你把 task 工作目录放到高性能临时存储上(比如集群的 $TMPDIR)。我建议在 I/O 密集型流程里开启 scratch true,能明显减少 NFS 负载。

process hasLargeFiles {
    scratch true
    input:
        path bigfile

    script:
    """
    # 这里 bigfile 已经自动复制到临时目录
    mytool $bigfile > result
    """
}

嗯,这里要注意:scratch 会复制输入文件,完成后又把输出文件复制回来。如果输入文件特别大(比如几十 GB),反而更慢。所以要根据场景取舍。

总结一下本章要点:

  • 进程是模板,task 是真正执行的工作单元。
  • input/output 定义数据流,script 写具体命令。
  • executor 负责调度 task,每个 task 有独立的工作目录。
  • 工作目录里藏了所有命令行和历史,学会用 .command.sh 排查问题。
  • 临时文件清理和 scratch 选项是生产环境必备技能。