☰
Nextflow报错排查:SAM_FOR_STRAND进程失败原因与修复指南
2026/10/19 9:31:36 网站建设 项目流程

bowtie2比对之后接链特异性拆分的流程,跑nextflow时报了这么一行:“Error executing process > 'SAM_FOR_STRAND (1)' Caused by: Process SAM_FOR_STRAND (1)”。我敢说,凡是跑过nextflow pipeline的人,看到“Error executing process”这行字,心里都会一沉——因为它只告诉你进程挂了,剩下的全靠自己去翻日志。SAM_FOR_STRAND这名字一看就知道是处理SAM文件的,把比对结果按链方向拆开,为后续链特异性分析做准备。这个报错在转录组、小RNA、宏转录组流程里都挺常见,问题也不一定在进程本身,很多时候是上游比对结果有问题,或者脚本里FLAG判断的细节写错了。

这篇文章就把我排查和修复这类报错的过程完整拆开讲。从nextflow报错信息的结构、work目录里怎么看现场日志,到SAM拆分脚本里最容易踩的FLAG坑、channel传递文件名的隐患,最后给一套可以直接照做的排查步骤和速查表。不管你是刚接触nextflow的新手,还是已经被这个问题折磨过几次的老手,按着这个思路走,基本都能把问题定位并解决掉。

1. 先搞清楚这条报错到底在说什么

1.1 nextflow报错消息的结构拆解

nextflow的报错信息看起来简单,但其实每条都有固定格式。拿这条来说:“Caused by: Process SAM_FOR_STRAND (1)”。

  • “SAM_FOR_STRAND”是进程名,也就是流程定义里写的那个process。
  • “(1)”是进程实例编号,这个数字代表它是该进程的第几个实例。如果流程里跑了多个样本,每个样本都会生成一个同名进程实例,实例编号从1开始递增。看到(1),说明是第一个样本对应的实例挂了。
  • “Error executing process”是nextflow的通用错误前缀,表示这个进程在指定环境中执行时返回了非零退出码。

这行的关键信息只有一个:SAM_FOR_STRAND进程的某个实例失败了,但具体原因不在这一行里。真正的错误日志在work目录下的执行现场文件里,需要进去找。还有一个容易误导人的地方:有时进程失败,报错信息会指向类似于“bash: command not found”或“segmentation fault”等,但更多时候,报错本身只显示“process exited with non-zero status”这类笼统描述,所以必须进现场看。

1.2 SAM_FOR_STRAND在流程里的真实任务

在多数自定义的转录组或微生物链特异性分析流程中,bowtie2比对完会输出一个SAM文件,这个文件里每条read的标准输出都附带了FLAG字段,记录了read是否比对到参考序列、比对方向、是否paired等。SAM_FOR_STRAND进程通常负责做这样两件事:

  • 读取bowtie2输出的SAM文件。
  • 把reads按“链方向”拆分为forward和reverse两份,分别保存为两个SAM/BAM文件,供下游链特异性统计或定量工具使用。

为什么要拆链?在RNA-seq里,链特异性建库时,reads比对到正义链或反义链的方向是有生物学意义的。比如在做sRNA分析时,siRNA和piRNA对应的sense/antisense链丰度是分开统计的,如果直接混在一起算,结果会被严重稀释。而bowtie2比对结果虽然给出了FLAG,但不会替你按链拆文件,这一步就需要流程里的自定义脚本来完成。

很多人的SAM_FOR_STRAND脚本写成这样:

awk '{ if ($2 == 0) print $0 }' input.sam > forward.sam awk '{ if ($2 == 16) print $0 }' input.sam > reverse.sam

这段脚本看似逻辑清晰,实际运行起来问题不少。常见的就是输出文件为空、进程报“No such file or directory”,或者更隐蔽的——拆出来的reads数量远小于预期,但又不报错。这些坑会在后面详细展开。

2. 排查的第一步:找到真正的报错现场

2.1 用nextflow日志定位work目录

nextflow有一个特点:所有中间文件和脚本都留在work目录里,每个进程实例对应一个唯一的子目录。定位这个目录的方法有好几种,我常用的是这样:

打开.nextflow.log,搜索“ERROR”或者那个报错的进程名。日志末尾附近能看到类似这样的记录:

ERROR ~ Error executing process > 'SAM_FOR_STRAND (1)' Caused by: Process 'SAM_FOR_STRAND (1)' terminated with an error exit status (1) Command executed: awk '{ if ($2 == 0) print $0 }' input.sam > forward.sam Command exit status: 1 Command output: (empty)

注意,这里面会提到“Command executed”和“Command exit status”,但真正的输出和错误输出不一定在这个摘要里。想拿到完整现场,还要靠第二招。

另一种定位方式是在nextflow运行目录下用find直接搜。nextflow的work目录结构通常是work/xx/yyyyyyyyyyyyyyyyyyyyyyyy/sample/这样的多层路径,可以用以下命令查找所有.command.err文件:

find work/ -name ".command.err" -newermt "2024-06-01" | xargs grep -l "SAM_FOR_STRAND"

不过命令行工具始终没有一条命令来得直观。在管道运行结束后,可以直接在终端输入nextflow log查看运行记录,然后指定run id,输出目录结构:

nextflow log <run-id>

这个可以列出每个进程对应的work目录路径。拿到路径后,进入那个目录就能看到.command.sh、.command.log、.command.err、exitcode这几个文件。它们分别对应脚本内容、标准输出、标准错误和退出码,这是还原现场的关键。

2.2 .command.sh和.command.err里藏着真凶

.command.sh是进程实际执行的shell脚本,里面是nextflow把process脚本块和环境变量组装起来的完整可执行命令;.command.err就是脚本运行时产生的标准错误输出。这两份文件配合起来使用,基本就能确定是哪个命令挂的。

有一次我排查类似报错,看到.command.err里写着:

awk: fatal: cannot open file `input.sam' for reading (No such file or directory)

这说明进程脚本本身没问题,问题是输入文件input.sam在这个work目录下不存在。也就是说,上游bowtie2进程的输出并没有正确传递给这个进程。那问题就转移到上游比对结果是否生成了,或者channel里传的文件路径和实际文件名不一致。

还有一次看到的是:

samtools view: failed to read header from "forward.sam"

这个就更好定位了,说明拆分逻辑把SAM文件里的header行弄丢了。后面会讲到如何正确地保留header。

所以每当看到“Caused by: Process SAM_FOR_STRAND (1)”这种报错,不要急着怀疑脚本写错,先干三件事:

  1. 定位work目录里的.command.sh和.command.err。
  2. 看.command.err里有没有明确的错误信息。
  3. 手动在work目录里逐条执行.command.sh里的命令,复现问题。

提示:work目录里的文件就是流程运行的"案发现场"。永远以现场为准,不要凭经验猜。

3. 常见的出错点和对应的解决方案

3.1 上游比对结果为空导致下游连锁崩溃

这是我看过最多的一种情况。bowtie2本身运行正常,比对也执行了,但输出SAM文件里几乎没有比对上的reads。导致SAM文件“空”的原因很多:

  • 参考基因索引路径写错了,bowtie2报错但进程被nextflow吞掉了。
  • 输入fastq里reads数太少,全部被过滤掉。
  • 用了--no-unal参数,未比对的reads不写输出。
  • 参考基因组和测序数据物种对不上,比对上数量极少。

当SAM文件只有header行而没有alignment记录时,跟进脚本里如果写了类似这样的内容:

samtools view -f 0x10 input.sam > reverse.sam

samtools不会报错,但输出的reverse.sam可能是空文件。后续如果有统计reads数的步骤,比如wc -l或者fastqc,就会因为空文件或0行数据而报错,而且报错位置往往不在拆分这一步,而是在更下游,排查时容易被带偏。

判断SAM文件是否为空的命令很简单:

grep -vc '^@' input.sam

如果输出是0,说明没有任何比对记录。这种情况下,修复的重点不是SAM_FOR_STRAND脚本,而是要对上游比对参数和输入数据进行排查。比如检查bowtie2的索引前缀名是否正确:

bowtie2 -x /path/to/ref_index -1 R1.fastq -2 R2.fastq -S output.sam

-x参数的路径前缀必须和索引实际文件名完全匹配。我的习惯是比对之前先用ls确认:

ls /path/to/ref_index*.bt2

3.2 awk/samtools拆链逻辑的FLAG大坑

拆链脚本里最核心的是如何判断“正向链”和“反向链”。很多人会写成刚才那种:

awk '{ if ($2 == 0) print $0 }' input.sam awk '{ if ($2 == 16) print $0 }' input.sam

这个写法有严重的隐患。因为SAM的FLAG字段是位标志,不是简单的整数等值判断。一个read的FLAG值是多个标志位相加的结果,比如0表示“read没有特殊标志,正向唯一比对”,但如果read是pair中第一条,且比对到反向链,FLAG值可能是83;第二条可能是163。单纯判断等于16,等于0,会把大量有效的链特异reads过滤掉。

正确的判断方式应该是用位运算:

  • FLAG的第5位(二进制10000,即16)表示read是否比对到参考序列的反向链。
  • 所以判断“是否反向链”,要看FLAG值包含不包含16这个位,而不是等于不等于16。

推荐用awk做位运算判断:

awk '!/^@/ && and($2, 16) == 0 { print }' input.sam > forward.sam awk '!/^@/ && and($2, 16) == 16 { print }' input.sam > reverse.sam

核心是and($2, 16)。这个函数在gawk里可用,但在mawk里不行。如果你在debian/ubuntu默认环境里用的是mawk,要记得先安装gawk,或者改用samtools view。

用samtools view也可以实现同样的效果,而且兼容性更好:

# 反向链:包含0x10标志的比对记录 samtools view -f 0x10 input.sam > reverse.sam # 正向链:不包含0x10标志的比对记录 samtools view -F 0x10 input.sam > forward.sam

这里-f 0x10表示“只保留包含该标志的记录”,-F 0x10表示“排除包含该标志的记录”。注意-f和-F的大小写含义完全不同,一个是“包含”,一个是“排除”,写反了结果就正好颠倒。

还有一个新手必踩的坑:samtools view默认不输出SAM header。拆分后的文件如果需要保留header,要给samtools加-h参数:

samtools view -h -f 0x10 input.sam > reverse.sam

而使用awk方式时,要记得用!/^@/过滤掉以@开头的header行,否则这些header行会同时被拆到两个文件里。虽然awk本身不会报错,但下游工具如果对重复header敏感,就会出现第二个奇怪的问题。

3.3 nextflow channel传递文件名不匹配的坑

nextflow进程之间的数据传递依赖channel。如果上游进程的输出文件名和下游进程期望的输入文件名不一致,就会出现“文件找不到”的报错。这个问题在SAM_FOR_STRAND报错中占比不低。

一个典型场景是:上游bowtie2进程使用.trim.fastq.gz作为输入,输出被命名为sample.sam;下游SAM_FOR_STRAND进程的channel却接收了trim.fastq.gz对应的basename,然后脚本里固定写死了sample.sam。结果就是channel里的文件路径指向的目录里根本没有这个文件,或者文件名对不上。

排查这类问题有个快速方法:在work目录里执行ls -l,看看实际有哪些文件。比如发现自己期望的sample.sam不存在,存在的却是sample.sam.gz或SRR1234567.sam,那问题就很清楚了。

让我给你看一个我跑过的实际错误案例。流程定义大概是这样:

process BOWTIE2 { input: tuple val(sample_id), path(reads) output: tuple val(sample_id), path("*.sam") script: """ bowtie2 -x ${params.index} -1 ${reads[0]} -2 ${reads[1]} \ -S ${sample_id}.sam """ } process SAM_FOR_STRAND { input: tuple val(sample_id), path(sam_file) output: path("forward_*.sam") path("reverse_*.sam") script: """ samtools view -h -F 0x10 ${sam_file} > forward_${sample_id}.sam """ }

看起来没问题吧?但实际运行时,SAM_FOR_STRAND的work目录里就是找不到sam文件。后来才发现是上游BOWTIE2的output定义有问题:

output: tuple val(sample_id), path("*.sam")

这里*.sam的匹配规则是把当前目录下所有.sam文件都作为结果输出。如果目录里有多个.sam文件,channel里就会传递多个文件,但下游的path(sam_file)只会取第一个。而当目录里实际没生成.sam(比如bowtie2输出到了别的位置),就会报“No such file”。

更麻烦的是,如果上游用了publishDir做了输出发布,但发布位置和work目录不一致,下游拿到的是s3或共享存储上的路径,找不到本地文件,也会出现这个报错。所以排查时一定要看channel里传递的路径是本地work路径还是发布目录路径。

4. 实操记录:一次完整的排查修复过程

4.1 现场还原

某次我帮一个团队排查测序数据分析流程,报错信息就是这个SAM_FOR_STRAND。那是一个宏转录组的链特异性分析流程,16个样本并行跑,跑到第4个样本时报错。nextflow给出的消息很简单:

Error executing process > 'SAM_FOR_STRAND (1)' Caused by: Process SAM_FOR_STRAND (1)

我做的第一件事是进入.nextflow.log搜索详细信息。日志里Command exit status是1,Command output是空。然后我用find在work目录里定位那个实例:

find work/ -name ".command.err" | xargs grep -l "SAM_FOR_STRAND" | head

找到了对应实例的work目录后,进去执行:

ls -l cat .command.err

.command.err里写着:

samtools view: failed to open "sample1.sam" for reading: No such file or directory

这个信息非常关键。进程无法打开上游传下来的SAM文件。但我很快排除了channel问题,因为channel传递的路径在work目录里确实能看到文件——只不过文件名不叫sample1.sam,而是叫sample1.aligned.sam。我又往下看.command.sh,发现脚本里明确写的是sample1.sam,而channel里实际传进来的文件是sample1.aligned.sam。

这问题就清楚了:上游进程输出文件名与脚本里硬编码的文件名对不上。具体情况是上游bowtie2进程用--basename参数把输出文件名设置成了sample1.aligned.sam,但SAM_FOR_STRAND进程的脚本里仍然写成sample1.sam。修改脚本,让文件名完全由channel传递的文件名决定,问题瞬间解决。

4.2 修复过程中的验证方法

修复代码并不难,难的是确保修复后真的没问题。我用了一套逐个击破的验证流程:

  1. 手动重跑失败的命令。进入work目录,直接执行.command.sh,观察能否正常生成输出文件。
  2. 检查拆分结果:用zcat或less翻看拆出来的samtools文件,这里需要注意——拆分结果本来就是SAM文件,可以继续用grep检查头文件和reads数。
  3. 校验统计数字:拆完的两个文件reads数量加起来,应该等于输入SAM里非header行的总数。如果数量对不上,说明拆分逻辑里FLAG判断有问题。

统计校验命令:

# 输入文件总reads数 grep -vc '^@' input.sam # 拆分后等效判断 samtools view -c -F 0x10 input.sam samtools view -c -f 0x10 input.sam

两条命令的结果相加应该等于第一个命令的结果。

4.3 修复后的标准进程脚本长什么样

经过这次排查,我总结了一套在我看来比较稳妥的写法。用samtools替代awk来做FLAG判断,因为samtools的-f/-F参数语义清晰,不容易踩位运算的坑,还自动处理了一些边界情况:

#!/bin/bash set -euo pipefail # 输入SAM文件路径来自nextflow channel,不能写死 SAM_FILE="$1" SAMPLE_ID="$2" # 保留header,拆分forward链,排除反向链 samtools view -h -F 0x10 "${SAM_FILE}" > "${SAMPLE_ID}.forward.sam" # 保留header,拆分reverse链,只保留反向链 samtools view -h -f 0x10 "${SAM_FILE}" > "${SAMPLE_ID}.reverse.sam" # 可选:输出统计,便于后续核对 echo "${SAMPLE_ID} total_reads: $(samtools view -c "${SAM_FILE}")" echo "${SAMPLE_ID} forward_reads: $(samtools view -c "${SAMPLE_ID}.forward.sam")" echo "${SAMPLE_ID} reverse_reads: $(samtools view -c "${SAMPLE_ID}.reverse.sam")"

其中set -euo pipefail是bash脚本里非常值得养成的习惯。-e让任何一条命令失败都让整个进程退出,-u防止变量未定义时继续跑,-o pipefail保证管道中间步骤出错一样能捕获。少了这三行,很多脚本会悄悄把错误吞掉,最终留下一个难以定位的半成品结果。

在nextflow进程块里这样定义:

process SAM_FOR_STRAND { input: tuple val(sample_id), path(sam_file) output: tuple val(sample_id), path("*_forward.sam"), emit: forward_sam tuple val(sample_id), path("*_reverse.sam"), emit: reverse_sam script: """ sam-file="$sam_file" sample-id="$sample_id" samtools view -h -F 0x10 "\$sam-file" > "\${sample-id}.forward.sam" samtools view -h -f 0x10 "\$sam-file" > "\${sample-id}.reverse.sam" """ }

注意在nextflow的script块里,单引号和美元符号的转义规则要很小心。上面脚本里"\$sam-file"和"\${sample-id}"中的反斜杠是为了让nextflow不要提前做字符串插值,把这些变量保留给shell在运行时解析。如果用Groovy的双引号直接包起来,nextflow会误以为$sam_file是Groovy变量,最终传成空值,这就是很多脚本拼出来之后命令里“变量是空的”的原因。

5. 常见错误速查表与避坑心得

5.1 我遇到过的高频错误和对应解法

把多次踩坑总结成了下面这张速查表,照着表查,能省掉不少翻日志的时间:

错误现象大概率原因确认方法解决办法
No such file or directory文件名对不上或channel传递路径错误work目录里ls -l看实际文件名修改process的input定义,统一文件名
拆分结果forward或reverse为空FLAG判断用了等值而非位运算samtools view -c统计对比改用-f 0x10/-F 0x10
拆分文件没有header忘记-h参数或awk过滤了headerhead查看文件开头加上-h,或awk里保留^@行
进程退出但.command.err为空进程被OOM杀掉或磁盘写满查看.command.log和系统dmesg加大内存、清理磁盘空间
输入SAM没有任何比对记录上游比对参数出错或参考索引不对grep -vc '^@'统计修复上游比对参数,检查索引路径
所有sample的实例全挂参考索引文件缺失或全局路径错误汇总日志看错误模式检查索引文件和全局配置文件

有两个例外要看仔细。第一个是进程OOM被系统杀掉时,.command.err往往为空,但.command.log里可能也没有内容,真正的痕迹在系统的dmesg或journalctl里。如果你发现脚本本身怎么看都没问题,但进程反复被杀,记得去看内存限制和磁盘剩余空间。

第二个是磁盘写满的问题。拆分SAM文件时如果磁盘满了,awk或samtools会报No space left on device,但有时候写一半进程崩溃,.command.err里留下的只是一个莫名其妙的Segmentation fault。这类问题靠grep错误信息很难抓到,直接df -h看磁盘使用率最省事。

5.2 几个写成习惯就能避免踩坑的操作经验

简单说几个养活了我多年的小习惯。

一是永远不要在nextflow进程脚本里硬编码文件名。如果文件名里有样本ID,就从channel里拿;如果没有,就用通配符让nextflow自己匹配。硬编码文件名是“本地能跑,一上流程就挂”的最常见原因。

二是在拆分脚本里显式区分“header行”和“比对记录行”。如果用awk,开头两行先处理好header:

/^@/ { print; next }

这样header不管走到哪一步都不会丢,后面的比对记录统一处理。

三是养成写统计信息的习惯。进程跑完顺手输出一个echo,把原始reads数、forward数、reverse数、比例打出来,放在.command.log里。出问题的时候,一眼就能看出是数据量异常还是脚本逻辑异常,不用再去重新数reads。

四是注意samtools版本差异。老版本-f和-F的语义和现代版本完全一样,但有些老版本对BAM输入会更严格,如果上游用了比较新的samtools压缩成BAM,下游用老版本可能读不了。解决方案是流程内固定samtools版本,或者统一使用.sam文本格式传递文件。

以上是我在真实项目里排查这类问题时的完整思路。下次再看到“Caused by: Process SAM_FOR_STRAND (1)”这种报错,别慌,按着这套流程走一遍:先看.command.err定位具体的缺失或退出原因,再检查上游输出文件名和channel定义,最后核对FLAG拆分逻辑。三个方向都查一遍,八成问题都能水落石出。

最后再分享一个小技巧:如果你经常需要排查nextflow的流程报错,可以把.command.err内容自动追加到日志里。在process块里加一段|| true来保留退出码之外的日志信息,或者在脚本开头加入exec 2> >(tee -a sample.stderr.log),这样即使进程崩溃,日志内容也会留到输出目录,不会因为work目录被清理而丢失。这个习惯在我做大规模样本流程时帮了我很多次,希望你也能用上。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询