3. 第一个管道:最简单的单进程管道,理解流程定义与运行

好,我们来写第一个 Nextflow 管道。别紧张,它简单到只有一步——一个进程,一个输出。但这一步通了,你就能理解 Nextflow 的核心逻辑。

我刚开始学 Nextflow 的时候,也被它那套 “声明式” 的写法搞蒙过。说白了,它不像 Bash 脚本那样一步步告诉你 “先做 A,再做 B”,而是用一套 DSL(领域特定语言)来描述 “我想干什么”,Nextflow 自己帮你编排。这听起来有点抽象,跑个例子就懂了。

3.1 管道长什么样?

一个最小的 Nextflow 管道,就是一个 .nf 文件。里面最核心的有两部分:workflow 块和 process 块。workflow 块是主入口,process 块是具体的执行步骤。

我们写一个最简单的:输入一个文件,统计行数。

#!/usr/bin/env nextflow
nextflow.enable.dsl=2

process COUNT_LINES {
    input:
    path(x)

    output:
    stdout

    script:
    """
    wc -l $x
    """
}

workflow {
    lines = COUNT_LINES(file("/some/path/data.txt"))
    lines.view { "文件行数: $it" }
}

你看,这个管道没有复杂的条件判断,没有循环。但它把 Nextflow 的骨架都展现出来了:

  • 进程 (process):定义了一个叫 COUNT_LINES 的步骤。它接收一个文件 (path(x)),输出标准输出 (stdout),然后执行一段 shell 命令 (script{})。
  • 工作流 (workflow):调用这个进程,并把结果打印出来。

实际上,这也是很多真实管道的起点。我在项目中遇到过,新手第一件事就是想把一个已有的 Shell 脚本直接怼进 script{} 里。可以,但要注意输入输出声明。如果你漏了 output 声明,管道虽然能跑,但 Nextflow 不知道你产出了什么,后续步骤就拿不到数据。

3.2 运行你的第一个管道

保存上面的代码为 hello.nf。然后运行:

nextflow run hello.nf

嗯,等等。上面的代码里我写死了路径 /some/path/data.txt。在实际中,你通常会把输入文件作为参数传进来。Nextflow 推荐用 params 定义输入:

#!/usr/bin/env nextflow
nextflow.enable.dsl=2

params.input_file = "/some/path/data.txt"

process COUNT_LINES {
    input:
    path(x)

    output:
    stdout

    script:
    """
    wc -l $x
    """
}

workflow {
    lines = COUNT_LINES(file(params.input_file))
    lines.view { "文件行数: $it" }
}

现在你可以这样启动:

nextflow run hello.nf --input_file /path/to/your/data.txt

看到没?--input_file 会把值自动赋给 params.input_file。这就是 Nextflow 的参数传递方式,比写死路径优雅多了。

3.3 运行日志里藏着的秘密

管道跑起来后,终端会输出类似这样的信息:

N E X T F L O W  ~  version 23.10.0
Launching `hello.nf` [distinguished_woese] DSL2 - revision: a1b2c3d4e5
executor >  local (1)
[cd/123456] process > COUNT_LINES (1) [100%] 1 of 1 ✔
文件行数: 42

解读一下:

  • [distinguished_woese] 是这次运行的会话名称,自动生成的。你可以用 -name myrun 自己指定一个有意义的名称,方便根据名字识别不同运行。
  • [cd/123456] 是进程的工作目录哈希。每个进程都在一个独立的临时目录里执行。这也是 Nextflow 能实现 resume 的关键——每个任务有唯一的 hash,只要输入、命令、参数没变,下次运行就可以直接跳过。
  • 1 of 1 ✔ 表示所有任务都成功完成。

3.4 理解 workDir 与运行目录

默认情况下,Nextflow 会在你启动管道的目录下创建一个 work 目录。所有进程的工作目录都在 work/ 下面,按 哈希值/任务序号 命名。

你可以用 -w 参数指定一个其他路径,比如:

nextflow run hello.nf -w /data/nextflow_work

为什么要关心这个?因为 work 目录会迅速膨胀。我建议你养成习惯:

  • 把 work 目录放在一个有大空间的位置。
  • 定期清理:管道成功后,用 nextflow clean -f 删除旧的工作目录,只保留最后的 results 或 output 文件夹。
  • 不要修改 work 目录里的文件。它是 Nextflow 的缓存仓库,你改了它,下次 resume 可能会报错或者得到不一致的结果。

我个人习惯在项目根目录下放一个 nextflow.config 文件,把 workDir 和 publishDir 统一配置好,这样团队成员跑起来就只用关心参数,不用操心目录结构。

3.5 理解 resume 机制(最简单的场景)

现在,假设管道执行成功了。你再次运行同样的命令:

nextflow run hello.nf --input_file /path/to/your/data.txt

你猜怎么着?它瞬间就完成了,并且告诉你:

[-        ] process > COUNT_LINES (1) [100%] 1 of 1 ✔
Cached: 0 of 1 tasks staged

看到 Cached 了吗?这就是 Nextflow 的缓存命中。它发现输入、脚本、参数都没变,直接从缓存里拿结果,不再执行任务。

这个特性在调试时特别有用。比如你写了一个 50 步的管道,跑到第 30 步挂了。你修复 bug 后,只需要加个 -resume 参数:

nextflow run hello.nf -resume

它会自动跳过前 29 个成功的步骤,从第 30 步开始重跑。我曾经用这个特性把一个反复调试的变异检测管道从每次 6 小时缩减到 5 分钟。基本上,只要你的输入文件和代码没变,resume 就管用。但是,如果你改了 script{}、input 或 params,它就会重新运行受影响的进程。

避坑指南
我曾经在 -resume 上栽过跟头。事情是这样的:我修改了参考基因组的路径(在 params 里改的),但没改文件名。Nextflow 只检查文件内容哈希,它发现文件内容没变,就用了缓存,结果跑出来的结果还是旧参考基因组的。后来我养成了一个习惯:每次 -resume 前,先执行一次 nextflow log 看看缓存情况,确认哪些任务是从缓存获取的。

3.6 第一个管道的知识图谱

下面这张图概括了“最简单的单进程管道”中每个概念的位置与关系,你可以对照上面的代码来看:

最简单的单进程管道知识结构 流程定义 hello.nf 参数输入 params / --input 运行命令 nextflow run 进程执行 COUNT_LINES 工作目录 work/ 哈希目录 缓存机制 hash / resume 输出产物 stdout / 文件 核心要点 1. workflow 块是入口,process 块是步骤。 2. 参数通过 params 传递,命令行用 -- 前缀。 3. 工作目录自动管理,resume 依赖输入的 hash。

3.7 总结一下关键点

运行第一个管道,你其实完成了这些:

  • 理解了一个 .nf 文件的基本结构:workflow + process。
  • 知道了如何用 params 和命令行参数控制输入。
  • 观察了运行日志,理解了 workDir 和任务哈希的作用。
  • 体验了 resume 的威力——不重复造轮子。

接下来,等你把这段代码跑通,再试着加第二个进程,把 COUNT_LINES 的输出传给下一个进程,那你就真正进入“管道”的流水线思维了。