4. 资源需求声明:在 process 中设置 cpus、memory、time、disk

声明资源的写法,说白了就是告诉 Nextflow:“我这个任务需要多少家底”。

你想想看,如果不说清楚,调度器要么给太少,任务跑一半就被 OOM 杀掉;要么给太多,浪费集群资源,排队排到天荒地老。

我最初接触 Nextflow 的时候,就在这上面吃过亏。一个 BWA 比对任务,默认给 1 个 CPU,跑了一个通宵都没完。后来我才意识到,资源声明是性能调优的第一步。

4.1 四个核心参数:cpus、memory、time、disk

每个 process 都可以声明这四个参数。它们直接告诉调度器(比如 SLURM、SGE、Kubernetes)你需要多少资源。

参数 含义 常用单位
cpus CPU 核心数 整数,如 4、8、16
memory 内存大小 MB、GB
time 运行时长上限 s、m、h、d
disk 本地磁盘需求 MB、GB

举个例子。一个典型的比对任务,你可以这样写:

process align_bwa {
    cpus 8
    memory '16.GB'
    time '2.h'
    disk '50.GB'

    script:
    """
    bwa mem -t ${task.cpus} ref.fa reads.fq > aligned.sam
    """
}

注意一点:在 script 块里,用 ${task.cpus} 来引用你声明的 CPU 数量。这样真实的 BWA 命令就知道该用多少个线程了。我见过不少新手直接写 -t 8,结果后面改成 4 核还得手动改脚本。

核心原则:声明的资源要和实际命令匹配。你声明了 8 核,但软件只用 1 个线程,那就白费了。

4.2 资源单位的正确写法

Nextflow 对单位要求很严格。写错了直接报错。

单位 写法示例 注意点
MB '1024.MB' 数字 + 点 + 单位
GB '2.GB' 不要写成 '2 GB' 或 '2GB'
小时 '1.h' 支持 s、m、h、d
分钟 '30.m' m 表示分钟,不是月份

为什么不能写成 '2GB'?因为 Nextflow 的解析器要求数字和单位之间必须有一个点。我一开始也踩过这个坑,报错信息还不明显,找了半天才发现是多写了个空格。

另外,memory 和 disk 支持小单位:

memory '500.MB'   # 半 G
disk '10.GB'      # 10 G

避坑:我曾经把一个基因组组装任务写成 memory '500.GB',结果集群根本没有这么大内存的节点,任务一直排队。后来才发现是数据估错了。建议先在小样本上测一下真实内存消耗,再写死资源大小。

4.3 理解动态资源分配:省时又省钱

静态声明虽然简单,但有个问题:你写的资源是固定的。假如一个任务平均只要 4 GB,但你写死了 16 GB,调度器就会给你分配 16 GB 的节点。集群里大内存节点本身就少,排队时间会变长。

动态资源分配就是来解决这个问题的。核心就一个思路:先给少量资源,失败了再自动增加,直到成功或达到上限。

实现方式是用 errorStrategy 'retry' 结合 maxRetries 和 maxErrors,配合 memory 的翻倍策略。

看一个完整的例子:

process dynamic_mem_task {
    memory { 2.GB * task.attempt }
    time   { 1.h * task.attempt }

    errorStrategy 'retry'
    maxRetries 3
    maxErrors 5

    script:
    """
    your_command_here --mem ${task.memory.toGiga()}G
    """
}

这里的关键是 task.attempt。第一次执行时 attempt = 1,内存 = 2 GB。如果内存不够失败了,第二次 attempt = 2,内存变成 4 GB。第三次是 8 GB。最多重试 3 次。

我有个 RNA-seq 定量流程就用这个模式。大部分样本 4 GB 就够,偶尔遇到高表达基因多的样本会爆内存。动态重试让整个流程跑得非常稳,又不会浪费资源。

小技巧:对于 IO 密集型任务,你可以把 cpus 也做成动态的。比如 cpus { 1 * task.attempt },第一次跑 1 核,不够就加。不过 CPU 一般不会因为不够而失败,更多是内存不足,所以动态 CPU 用得少。

4.4 用 labels 做资源分组

当你写了几十个 process 之后,会发现很多任务的资源需求是类似的。比如比对、排序、标记重复这些,都可以归为一类。

这时候可以用 labels 来分组,然后在配置文件里统一指定资源。

先给 process 打标签:

process align_bwa {
    label 'high_mem'

    script:
    """
    bwa mem ref.fa reads.fq > aligned.sam
    """
}

process sort_sam {
    label 'high_mem'

    script:
    """
    samtools sort aligned.sam > sorted.bam
    """
}

然后在 nextflow.config 里统一配置:

process {
    withLabel: 'high_mem' {
        cpus   = 8
        memory = '16.GB'
        time   = '2.h'
    }
    withName: 'sort_sam' {
        cpus = 4
    }
}

用 withLabel 给同类任务统一配置,再用 withName 微调个别进程。我习惯把最通用的资源写到标签里,特殊任务再单独覆写。这样配置很干净,改一个地方就能影响一批进程。

4.5 知识结构速览

下面这张图能帮你把今天的内容串起来:

资源需求声明核心逻辑 process 资源声明 cpus memory time disk 整数:4、8、16 单位:MB / GB 单位:h / m / s 单位:GB / MB 动态资源分配 labels 分组管理 核心路径:静态声明 → 动态调节 → labels 分组 → 统一配置 目标是:够用不浪费,失败能自愈

4.6 避坑与最佳实践

  • 不要写死大数字:我以前觉得 "给大点肯定没错",结果 256 GB 的任务在只有 128 GB 节点的集群里永远跑不了。先测,再写。
  • 注意单位写法:'2.GB' 不是 '2GB'。这个点一定要有。我还见过有人写 '2 gb',小写也不行。
  • disk 不是存储盘:disk 声明的是工作目录需要的本地 scratch 空间,不是最终的输出存储。如果你的输出很大,记得配置 scratch 'false' 或者指向大的挂载盘。
  • time 超时会被杀掉:如果任务实际跑超了声明的时间,调度器会发 SIGTERM 杀掉进程。建议声明的时间比预估值多 30% 作为余量。
  • 动态重试的尝试次数不要太多:我一般设 3 次。超过 3 次还失败,多半是代码或者数据有问题,重试也没用。
  • GPU 资源怎么办:Nextflow 原生不支持 gpus 声明。但你可以通过自定义配置文件传给调度器。后面会有专门章节讲。