第三讲:配置文件的骨架——参数、作用域、环境变量与包含

配置文件(nextflow.config)是Nextflow管线的中枢神经系统。说白了,所有运行时的行为都靠它来统一管控。我当初第一次写Nextflow管线时,把所有配置都堆在一个文件里,可想而知,后来部署到不同环境时有多痛苦。

这一讲,我们就把配置文件的底裤扒清楚。搞懂参数、作用域、环境变量和文件包含这四样东西,你写出来的配置就能像乐高一样灵活拼接。

3.1 参数定义:最基础的赋值

参数是配置文件里的最小单元。你定义一个变量,给个值,就完了。

params.input = "/data/samples/*.fastq"
params.outdir = "results"
params.genome = "GRCh38"

这很简单,对吧?但我要提醒你一个坑:params 是 Nextflow 预留的关键字。所有你在命令行里通过 --input 传入的参数,都会自动映射到 params.input。所以,千万别把 params 当作普通变量来用。

我的习惯:我会把所有的“用户可调整”参数放在 params 作用域下。那些管线内部使用的、用户不该碰的变量,我放在普通变量或者作用域里。

3.2 作用域:给参数分门别类

作用域就像文件夹。你把不同来源、不同用途的参数放进不同的“抽屉”里。Nextflow 支持的作用域有十几种,但日常我们最常用的是这几个:

作用域 用途 我遇到的典型场景
params 公开参数,用户和命令行可覆盖 输入路径、输出路径、阈值
process 进程的默认资源与指令 cpus、memory、container
executor 执行器配置 queue、queueSize、retry
env 环境变量 PATH、TMPDIR、API_KEY
manifest 管线的元信息 name、author、version

举个例子,你想给某个进程单独分配更多内存,可以这样写:

process {
    withName: 'star_alignment' {
        cpus = 16
        memory = '64.GB'
    }
    withName: 'fastqc' {
        cpus = 2
        memory = '4.GB'
    }
}

作用域的优先级

你想想看,如果同一个参数在多个地方定义了,听谁的?Nextflow 有一套固定的覆盖规则:

  1. 命令行参数(最高优先级)
  2. profile 中的配置
  3. 基础配置文件中的配置
  4. params 默认值(最低优先级)

我在项目中遇到过最典型的问题:一个同事在 profile 里定义了一个参数,又在基础配置里定义了另一个,结果两个地方的值相互覆盖,最后跑出来的结果完全不对。排查了半天——就是因为作用域优先级没搞清。

注意:process 作用域下的 withName 和 withLabel 又有一层自己的优先级。withName(按进程名匹配)高于 withLabel(按标签匹配)。如果你同时定义了针对同一个进程的两种规则,withName 的会覆盖 withLabel 的。

3.3 环境变量:让配置与环境解耦

密码、Token、路径前缀……这些东西不该硬编码在配置文件里。怎么办?用环境变量。

在配置文件中读取环境变量

env {
    // 直接读取系统环境变量
    NXF_HOME = "$HOME/.nextflow"
    TMPDIR = "/scratch"
}

// 在参数中使用环境变量
params.api_token = System.getenv('API_TOKEN') ?: 'default_token'
params.aws_bucket = "${System.getenv('AWS_S3_BUCKET')}/project1"

这段代码里有个技巧:System.getenv() 是 Groovy 的方法,可以在配置文件中直接调用。如果环境变量不存在,就用 ?: 给个默认值。这样你的配置文件在本地开发和服务器部署时都能正常工作,只是行为略有不同。

我的经验:曾经有一次,我把 AWS 的 secret key 直接写死在配置文件里,还上传到了 Git 仓库。结果被安全审计的人找上了门,相当尴尬。后来我强制团队所有敏感信息都必须从环境变量读取,并且在 CI 的配置中严格保密。

在进程级别设置环境变量

process {
    withName: 'run_secret_tool' {
        env {
            API_KEY = "sk-xxxx"
            ENDPOINT = "https://api.example.com"
        }
        script:
        """
        secret_tool --endpoint \$ENDPOINT
        """
    }
}

你可能会奇怪,为什么环境变量不在外面统一定义?嗯,要是每个进程需要的环境变量不同,那你写在全局 env 作用域里就不合适了。每个进程单独定义,清晰又安全。

3.4 配置文件包含:把大文件拆成小模块

一个 nextflow.config 文件,动辄几百行。所有人都改这一个文件,冲突是迟早的事。我建议你把它拆成多个小文件,然后用 includeConfig 来组装。

基础用法

// 主配置文件 nextflow.config
includeConfig 'configs/params.config'
includeConfig 'configs/process.config'
includeConfig 'configs/executor.config'

// 环境相关的配置,根据 profile 条件包含
profiles {
    standard {
        includeConfig 'configs/standard.env'
    }
    cluster {
        includeConfig 'configs/cluster.env'
    }
}

这样一拆,每个人只关心自己那一亩三分地。管参数的人改 params.config,管资源的人改 process.config,互不干扰。

路径解析的陷阱

这里有个容易踩坑的地方:includeConfig 的路径是相对于当前配置文件所在目录的。如果你的主文件在根目录,被包含的文件在 configs/ 子目录里,那路径就是 configs/xxx.config。但如果被包含的文件里又包含另一个文件,路径就要特别注意了。

// 假设主文件在 pipeline/nextflow.config
includeConfig 'configs/common.config'

// 而 common.config 在 pipeline/configs/common.config
// 它里面想包含其他文件,路径是相对于它自己所在的目录
includeConfig 'tools.config'  // 会去找 pipeline/configs/tools.config
我的建议:不要在子配置文件里嵌套 includeConfig。实在要嵌套,就用绝对路径,或者保持所有被包含的文件都在同一目录下。这样路径关系一目了然,不容易出错。

3.5 一张图搞定配置结构

上面讲的这些关系,用一张图来表示更直观:

Nextflow 配置结构关系图 nextflow.config 配置结构 nextflow.config(主入口) params 作用域(公开参数) process 作用域(进程配置) env 作用域(环境变量) --input --outdir withName: cpus, memory withLabel: container NXF_HOME TMPDIR includeConfig(文件包含/模块化) params.config process.config executor.config 优先级:命令行 > profile > 基础配置 > params默认值 环境变量通过 System.getenv() 读取,敏感信息不硬编码

这张图应该能帮你理清关系了。主配置文件就像总开关,通过 includeConfig 拉取各个模块;params、process、env 这三层作用域各自管好自己的事;最后通过优先级规则决定谁说了算。

3.6 小结

参数、作用域、环境变量、配置文件包含——这四个东西是 nextflow.config 的四大支柱。你用好了,配置文件就能做到:

  • 参数清晰:每个人都清楚哪些值是可以调的,哪些是内部固定的
  • 作用域隔离:进程配置、执行器配置、管道参数互不污染
  • 环境解耦:敏感信息和环境差异通过环境变量传递,配置本身保持干净
  • 模块化:不同角色的维护者只关心自己的小文件,减少冲突