第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却没装,或者路径没配 |
| 137 | SIGKILL (9) | OOM被系统杀了,或者超时被cgroup杀掉 |
| 139 | SIGSEGV (11) | 段错误,一般是软件bug或内存越界 |
| 141 | SIGPIPE (13) | 管道下游关了,上游还在写 |
注意:我把137专门标红了。曾经有一次生产环境下,一批任务反复重试了三次才成功,就是因为每次都被OOM kill掉,退出码137。你如果看到大量137,别急着加重试次数,先看看内存是不是给少了。
Nextflow默认行为是:退出码非0就标记为失败。但有个小细节——如果你的进程以exit 0结尾但实际输出文件没生成,Nextflow不会只看退出码,还会检查输出文件是否存在。这就引出了下一层。
3.2 异常类型:Nextflow内部的错误分类
退出码到了Nextflow引擎里,会被包装成具体的异常类型。不同异常类型会触发不同的行为。这里我画了一张图,帮你理清楚它们之间的关系。
嗯,图里最常用的就是 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.
来,我们逐层拆:
- 退出码:9对应SIGKILL,说明是被强制杀死的,不是程序自己退出的。
- 异常类型:Nextflow收到退出码9后,内部抛出了
AbortedException,底层是ProcessException。 - 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的细节你得自己提炼,然后把提炼结果“喂”给退出码。这样重试逻辑才能智能起来,而不是傻傻地重试三次然后放弃。