Nextflow 配置文件基础:结构、作用域与参数层级

Nextflow 的配置文件 nextflow.config,说白了就是一个 DSL 脚本。但很多初学者一上来就被各种作用域绕晕了。我个人习惯先把配置文件拆成三块来看:结构长什么样、作用域怎么划分、参数怎么覆盖。搞懂这三块,你就能看懂大部分别人的配置,也能写出自己的。

配置文件——它就是个 DSL 脚本

你可能会觉得 nextflow.config 是个简单的键值对文件,像 YAML 那样。其实不是。它用的是 Groovy 语法,支持变量、函数、甚至循环。但咱们不用怕,大部分时间只用到它的「作用域块」。

一个典型的配置文件长这样:

// nextflow.config

params {
    genome = "GRCh38"
    reads  = "data/*.fastq.gz"
    outdir = "results"
}

process {
    executor = 'local'
    cpus     = 4
    memory   = '8 GB'
}

executor {
    name = 'slurm'
    queueSize = 100
}

你看,每个块都是一个作用域。我刚开始用 Nextflow 的时候,总把 params 和 process 的配置写混。当时还在想,为什么我明明设了 cpus = 8,进程还是只跑 1 个 CPU?后来才发现我写在了 params 块里。嗯,字段名写对了,但作用域不对,等于白写。

三个核心作用域:params、process、executor

作用域是 Nextflow 配置的灵魂。咱们先记住这三个最常用的。

作用域 功能 键值举例 覆盖方式
params 定义 pipeline 参数,用户可传入 genome, reads, outdir 命令行 --genome 覆盖
process 控制单个进程资源、标签、容器等 executor, cpus, memory, container 模块内 withLabel 覆盖
executor 定义集群调度器行为 name, queueSize, submitRateLimit 全局覆盖

这三个作用域覆盖了 90% 的日常配置需求。你想想看,维护一个 pipeline 最怕什么?不是写错值,而是写对了值,但放错了作用域。我在项目中遇到过好几次这种问题,最后排查发现都是作用域搞混了。

我的小技巧:把 params 看作「用户能改的开关」,process 看作「每个进程的装备」,executor 看作「怎么把任务丢给集群」。这样分类,你就不会放错地方。

参数层级——谁覆盖谁?

参数覆盖规则,说白了就是一层一层往上盖。Nextflow 里总共有 4 个层级:

  1. 内置默认值:Nextflow 自带的,比如 executor = 'local'。
  2. 配置文件:nextflow.config 里定义的值。
  3. 模块配置:modules/xxx.config 里用 withName 或 withLabel 写的配置。
  4. 命令行覆盖:运行 nextflow run 时带的 --param value,优先级最高。

为什么会这样设计?

因为 Nextflow 的作者希望你能「全局设定 + 模块微调 + 临时重写」。我经常用这个特性:先在 nextflow.config 里定一套通用的资源参数,然后在模块里对某个特别耗内存的进程单独调大 memory,最后调试时用命令行临时改个小参数——不用动配置文件,很方便。

一张图搞懂层级与覆盖

下面这张 SVG 图展示了我上面讲的 4 层结构,以及它们在运行时如何合并成最终配置。

Nextflow 参数层级与覆盖机制 内置默认值 executor = 'local' nextflow.config params.genome = 'GRCh38' 模块配置 withName: align { cpus = 16 } 命令行参数 --genome 'CHM13' 覆盖规则 🔺 优先级从下往上递增 🔹 命令行参数 > 模块配置 🔹 模块配置 > 配置文件 🔹 配置文件 > 内置默认值 🔍 实际合并结果 如果命令行传了 --genome 'CHM13' 最终 params.genome = 'CHM13' 即使配置文件设了 GRCh38 💡 我的建议 全局默认放 nextflow.config 模块特例放 modules/*.config 临时调整用命令行覆盖 优先级:命令行 > 模块配置 > nextflow.config > 内置默认值

避坑指南——我曾经犯过的错

讲完理论,我分享三个真实踩过的坑。

坑1:作用域写错
我曾经把 params.cpus = 8 写在 params 块里,但进程根本没有读取 params.cpus 的能力。进程只认 process.cpus。正确写法是 process { cpus = 8 },或者用 $params.cpus 赋值给进程。记住:params 是用户参数,process 是进程资源。
坑2:模块内命名冲突
有一次我写了个模块配置,里面用了 params.sample_id,但主配置文件里也有同名的 params.sample_id。我以为模块里的会覆盖主配置,结果发现 Nextflow 的模块配置不会覆盖 params——它只覆盖 process 和 executor。我当时排查了半小时,才意识到 params 在模块里不会自动覆盖。
坑3:executor 写在 process 块里
这个更常见。有人会在 process 块里写 executor = 'slurm',这也没错。但如果你同时在 executor 顶层作用域里写了 queueSize,这两个作用域是平行的,不会自动合并。我建议统一写在 executor 块里,然后在 process 块里只写 executor = 'slurm',这样清晰。

多环境配置——我推荐的方式

项目大了,你通常需要开发、测试、生产三套配置。我不建议写三个 nextflow.config 文件,而是用一个主配置文件,然后 includeConfig 加载环境特化配置。

做法很简单:

// nextflow.config

params {
    genome = "GRCh38"
}

process {
    executor = 'local'
}

// 通过环境变量加载不同配置
if (params.exec_mode == 'slurm') {
    includeConfig 'configs/slurm.config'
} else {
    includeConfig 'configs/local.config'
}

运行时这样选:

nextflow run main.nf --exec_mode slurm

这个技巧我用了两年了,真的很省心。你想想看,如果每次切环境都要改文件名或者手动复制,迟早会出错。把环境选择暴露成 params,清晰又安全。

优先级总结——记这三句话就够了

  • params:写在 nextflow.config,命令行用 --key value 覆盖。
  • process:先设全局值,再用 withLabel 或 withName 微调模块。
  • executor:只设一次,全 pipeline 生效。如果要改某个进程的执行方式,在 process 块里单独写 executor = 'pbspro'。