3. 重复标记失败:内存不足(OutOfMemoryError)与Spark模式下的资源调优
重复标记(MarkDuplicates)这一步,可以说是GATK流程里最让人头疼的环节之一。
你想想看,几十G甚至上百G的BAM文件,上千条的reads,要全部读到内存里比对、标记、排序。一旦内存不够,直接抛一个OutOfMemoryError。我见过不少团队,早上提交任务,下午回来看,整个集群就卡在这一步。
为什么会这样?简单说,就是Java堆内存(Heap Memory)不够用了。MarkDuplicates会把所有read的坐标信息加载到内存里,如果文件太大、片段太多,堆空间就撑爆了。
解决思路其实不复杂:要么给JVM喂足内存,要么把计算拆分到Spark上跑。我今天就把两种方案的调优经验从头捋一遍。
3.1 内存不足:先从JVM参数查起
最直接的错误日志长这样:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3332)
at picard.sam.markduplicates.MarkDuplicates.buildSortingCollection(MarkDuplicates.java:487)
嗯,看到这个别慌。先确认你给了多少堆内存。
我之前遇到过一个小团队,用默认的-Xmx2g跑了90G的WGS数据。结果不言而喻,跑了8小时直接崩了。我的习惯是,先看输入文件大小,再定内存。
调整方法很简单,在命令行里显式指定:
java -Xmx64g -jar gatk-package-4.3.0.0-local.jar \
MarkDuplicates \
-I aligned.bam \
-O marked_duplicates.bam \
-M metrics.txt
这里 -Xmx64g 表示最大堆内存64G。你还要确保物理内存够用,别把物理机撑爆了。
3.2 常见陷阱:碎片文件与临时目录
光给内存还不够,MarkDuplicates内部用了大量的临时文件来做排序和去重。如果临时目录所在的磁盘空间不够,或者读写太慢,同样会引发OOM或者IO瓶颈。
我习惯这样设置临时目录:
java -Xmx64g -Djava.io.tmpdir=/scratch/tmp \
-jar gatk-package-4.3.0.0-local.jar \
MarkDuplicates \
-I aligned.bam \
-O marked_duplicates.bam \
-M metrics.txt \
--TMP_DIR /scratch/tmp
为什么强调这个?
有一次我在一个共享集群上跑,默认临时目录在 /tmp,结果别的任务把磁盘写满了。MarkDuplicates写到一半,磁盘满了,然后Java GC疯狂触发,最后OOM。从那以后,我再也不用系统默认的临时目录了。
3.3 内存不够?试试Spark模式
如果单机内存实在不够,或者文件大到100G以上,我个人强烈推荐用Spark模式跑。说白了就是把去重任务分到多个节点上,每个节点只处理一部分。
Spark模式下,MarkDuplicates会利用Spark的RDD来分布式处理,不再把所有数据加载到单个Java进程里。
基本用法:
gatk --spark-runner SPARK \
MarkDuplicatesSpark \
-I aligned.bam \
-O marked_duplicates.bam \
-M metrics.txt \
--conf spark.executor.memory=8g \
--conf spark.driver.memory=8g \
--conf spark.executor.cores=4 \
--conf spark.executor.instances=8
这里重点说一下几个参数:
- spark.executor.memory:每个executor的堆内存。建议8-16G,别太贪心,给多了反而GC频繁。
- spark.driver.memory:driver端内存,负责收集结果。如果数据量大,给16-32G。
- spark.executor.cores:每个executor的核数。一般给2-4,多了容易IO争抢。
- spark.executor.instances:并行度。根据集群规模调整,一般10-20就行。
3.4 三种运行模式的对比
我把常见的三种模式列一张表,方便你按场景选:
| 模式 | 适用场景 | 内存需求 | 速度 | 我的建议 |
|---|---|---|---|---|
| 本地单机 | BAM文件 < 60GB | 32-64G堆内存 | 快,无网络开销 | 推荐WES和中小WGS |
| Spark on YARN | BAM文件 > 80GB,多节点集群 | 每个executor 8-16G | 中等,取决于shuffle | 适用于大型WGS、全基因组测序 |
| Spark local模式 | 单节点但内存不够 | 与本地相当,但可拆分 | 比本地慢一点 | 不常用,临时救急可以 |
3.5 案例:一个真实的OOM排错过程
去年我帮一个基因测序公司排查流程。他们的GATK pipeline在重复标记步骤总是报OOM,日志里全是 heap space。
第一反应:检查-Xmx。发现他们只给了8G,而输入BAM文件是120G。这明显不够。我把内存调整到64G,还是崩。
为什么呢?进一步查发现,他们使用了 --REMOVE_DUPLICATES true 参数。这个参数会让程序在标记同时删除重复reads,需要额外缓存。我把这个参数去掉,只做标记不做删除,内存压力立刻降下来了。
最后我把模式改成了Spark on YARN,参数如下:
gatk --spark-runner SPARK \
--spark-master yarn \
MarkDuplicatesSpark \
-I aligned.bam \
-O marked_duplicates.bam \
-M metrics.txt \
--conf spark.executor.memory=12g \
--conf spark.driver.memory=16g \
--conf spark.executor.cores=4 \
--conf spark.executor.instances=12 \
--conf spark.yarn.executor.memoryOverhead=2g
注意最后一行 spark.yarn.executor.memoryOverhead=2g。这个参数很多人会漏掉。它负责executor的堆外内存,用于缓存、网络等开销。不加的话,executor会因为堆外内存不足被YARN杀死。
调完以后,120G的BAM文件跑了5小时就通过了。团队老大后来请我喝咖啡,说“早知道就早点找你了”。其实核心就是三个点:内存给足、临时目录放对、选对运行模式。
3.6 排查流程总结
最后,我把自己平时排查OOM问题的流程总结成一张图。你以后遇到类似问题,按这个顺序查,基本不会走偏。
嗯,以上就是我处理MarkDuplicates OOM问题的核心思路。从给足内存、调整临时目录,到优雅切到Spark模式,每步都有坑。下次你再遇到这个错误,可以直接翻出这张图对照排查。问题不大。