第3章:Nextflow错误模型——进程退出码、异常类型、stderr输出解析

写工作流最头疼的是什么?我估计十有八九是“跑着跑着就崩了,你还得去翻日志找原因”。Nextflow的容错能力再强,也得先弄清楚它到底怎么判断失败。这一章我们就把它内部那套错误模型拆开来看。

说白了,Nextflow把进程失败分成了三层:操作系统层的退出码、Nextflow自身的异常类型、以及进程吐出来的标准错误。这三层会层层向上传递,最后决定了重试策略怎么触发。

3.1 进程退出码:最底层的信号灯

每个进程跑完都会返回一个整数退出码。0表示一切正常,非0就表示有问题。Nextflow拿到退出码后,会先做一次基础判断。

我自己习惯把退出码分成三类:

  • 0 —— 成功,继续往下走
  • 1~127 —— 常规错误,比如输入文件不存在、参数不对
  • 128+ —— 被信号终止,比如128+9 = 137 就是被SIGKILL杀掉

下面这张表列出了我工作中最常碰到的几个退出码,建议你保存下来当速查表用。

退出码信号/含义典型原因
0正常结束任务完成
1通用错误参数错误、文件缺失、脚本内部错误
2误用shell内建命令比如shift越界
126命令不可执行权限问题或脚本第一行错了
127命令未找到你用了samtools却没装,或者路径没配
137SIGKILL (9)OOM被系统杀了,或者超时被cgroup杀掉
139SIGSEGV (11)段错误,一般是软件bug或内存越界
141SIGPIPE (13)管道下游关了,上游还在写

注意:我把137专门标红了。曾经有一次生产环境下,一批任务反复重试了三次才成功,就是因为每次都被OOM kill掉,退出码137。你如果看到大量137,别急着加重试次数,先看看内存是不是给少了。

Nextflow默认行为是:退出码非0就标记为失败。但有个小细节——如果你的进程以exit 0结尾但实际输出文件没生成,Nextflow不会只看退出码,还会检查输出文件是否存在。这就引出了下一层。

3.2 异常类型:Nextflow内部的错误分类

退出码到了Nextflow引擎里,会被包装成具体的异常类型。不同异常类型会触发不同的行为。这里我画了一张图,帮你理清楚它们之间的关系。

Nextflow 错误类型层级结构 NextflowException ProcessException GroovyException MissingOutputFileException CommandException AbortedException(被系统终止,如OOM杀死) ScriptCompilationError 说明: ProcessException 是进程级别错误,Nextflow 会根据退出码自动包装成对应子类。 AbortedException 一般由外部信号(SIGKILL/SIGTERM)触发,通常不可重试。

嗯,图里最常用的就是 ProcessException,它下面还有几个子类。我捡重点说:

  • MissingOutputFileException:进程退出码为0但产出文件没生成。我就踩过这个坑——有个工具正常结束但输出目录写错了,Nextflow一直重试,后来才发现是文件路径问题。
  • CommandException:退出码非0,直接包成这个。大多数重试策略都是针对它。
  • AbortedException:被外部信号打断,比如跑一半被管理员手动kill了,或者超时被cgroup杀掉。这种情况重试可能白费力气,得先确认资源够不够。

核心要点:你写 errorStrategy 的时候,其实就是在告诉Nextflow“遇到哪些异常我认了,哪些想再试一把”。后面章节会详细讲怎么配置,但前提是你得先懂异常类型。

3.3 stderr输出解析:藏在日志里的线索

光看退出码和异常类型还不够,真正有用的诊断信息往往藏在标准错误(stderr)里。我记得有个老项目,流程跑了三天,某个样本总是在比对那一步挂掉。退出码137,看到我就知道是内存溢出,但到底是哪个工具耗内存?翻stderr才发现,bwa mem在分配哈希表时,系统给的内存比它要求的少了一块。

Nextflow不会替你解析stderr内容,但它提供了一个好用的机制:你可以用 $task.exitStatus 和 $task.errorReport 拿到进程退出码和stderr后半段内容。结合自定义解析,能实现更聪明的重试。

下面是一个常见的stderr解析场景:

// 在你的 process 定义里,可以用一个 wrapper 脚本来解析 stderr
// 比如你想把 OutOfMemory 的错误与别的错误分开看待

#!/bin/bash
# 假设你的真实命令是 bwa mem
bwa mem ref.fa sample.fq > aligned.sam 2> bwa.stderr
EXIT_CODE=$?
if [ $EXIT_CODE -ne 0 ]; then
    if grep -qi "out of memory" bwa.stderr || grep -qi "killed" bwa.stderr; then
        echo "OOM detected, exit code: $EXIT_CODE"
        exit 137  // 强制转为 OOM 信号
    else
        cat bwa.stderr >&2
        exit $EXIT_CODE
    fi
fi

这种包装脚本的好处是:把不同错误类型“映射”成不同的退出码,Nextflow就可以根据退出码来精确控制重试策略。比如137就加内存再重试,1就直接放弃。

我的小技巧:别指望Nextflow自己能看懂stderr里的“segmentation fault”或者“cannot allocate memory”。最好在进程脚本里主动做一次解析,标准化退出码。这样后面的 errorStrategy 配置才干净利落。

3.4 三者如何联动——一个真实案例

我拿上个月遇到的一个例子串一下整个过程。一个变体检测流程,某个样本在freebayes这一步挂了。流程日志里看到了这个:

Process `freebayes` failed — exit status: 9 (SIGKILL)
Cause: Process was killed by the operating system (out of memory?)
Stdout: (empty)
Stderr:
slurmstepd: error: Detected 1 oom-kill event(s) in step 12345
slurmstepd: error: Step 12345 was cancelled.

来,我们逐层拆:

  1. 退出码:9对应SIGKILL,说明是被强制杀死的,不是程序自己退出的。
  2. 异常类型:Nextflow收到退出码9后,内部抛出了AbortedException,底层是ProcessException。
  3. stderr解析:slurm集群明确告诉你这是oom-kill事件。但Nextflow默认不会识别“oom-kill”这几个字。

解决方案是什么?我在进程里加了一段检查:如果stderr包含“oom-kill”,就把退出码改成137(正好等于128+9)。然后配置 errorStrategy { exitStatus in 137..137 ? 'retry' : 'ignore' },并在重试时增加内存。只重试两次,因为你不能无限加内存吧?

提醒:AbortedException 默认是不触发重试的(因为Nextflow认为是被外部因素中断,重试可能重蹈覆辙)。但如果你像我这样手动转码到137,它就会变成一般的 CommandException,从而进入正常重试逻辑。记得在 errorStrategy 里明确处理137,否则默认的重试次数不够你调的。

3.5 本章小结——一张表帮你记住

到这儿,你应该能明白Nextflow怎么判断一个进程失败了。我把三个层次的对应关系整理成了一张表,方便你在写配置时对照:

层次信息来源Nextflow如何获取你该怎么利用
退出码$? 或 exitStatus自动获取,整数在 errorStrategy 里用 exitStatus 匹配
异常类型Nextflow根据退出码和文件缺失自动生成内部异常对象在流程里 try-catch 或靠 errorStrategy 的 errors 变量
stderr内容标准错误流文本可通过 $task.errorReport 获取后半段主动解析并转码退出码,实现精细重试

一句话总结:Nextflow只懂退出码和异常类型,stderr的细节你得自己提炼,然后把提炼结果“喂”给退出码。这样重试逻辑才能智能起来,而不是傻傻地重试三次然后放弃。