第2章:内存溢出基础概念

聊内存溢出之前,我先说个真事。

我刚开始搞生物信息学那会儿,觉得内存管够就行。有一次跑BLAST,序列库不大,我就直接上了默认参数。结果跑了半小时,服务器直接卡死,连SSH都连不上。后来一查日志,OOM,系统把BLAST进程给杀了。

内存溢出(OOM),说白了就是你程序跑着跑着突然发现内存不够用了。操作系统给你两个选择:要么程序自己崩溃,要么系统把某个进程杀掉来腾空间。大部分情况——尤其是BLAST这种大内存应用——都是后者。

OOM的常见原因

为什么会发生OOM?我总结下来,无非这几种情况:

  • 一次性分配太多:你构建的索引太大,或者读入了整个数据库,内存直接爆了。
  • 内存泄漏:申请了内存没释放,慢慢把内存吃光。我见过有的BLAST二次开发代码,每次查询都new一个对象,就是没delete。
  • 峰值太高:短时间内并发请求太多,内存被瞬时打满。
  • 配置限制:JVM堆内存设得太小,或者容器内存上限偏低。
我的建议:排查OOM时,先看是不是峰值导致的问题。很多团队一上来就怀疑泄漏,结果查了半天,其实是并发量上来了,内存没扩。

JVM层面 vs 系统层面OOM

搞清楚OOM发生在哪一层,很重要。我遇到过不少人,把系统OOM当成JVM OOM来排查,绕了一大圈。

维度 JVM层面OOM 系统层面OOM
触发位置 Java堆、元空间、栈 操作系统内核、物理内存
表现形式 抛出 java.lang.OutOfMemoryError dmesg显示OOM Killer,进程被kill
日志位置 应用日志、JVM日志 /var/log/messages、dmesg
排查切入点 堆转储、GC日志 内存占用排名、cgroup限制
恢复手段 调整堆大小、优化代码 增加物理内存、缩小进程、限制内存上限

JVM层面的OOM,JVM自己会抛异常。你可以在应用日志里看到类似这样的信息:

java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOf(Arrays.java:3236)
    at java.io.ByteArrayOutputStream.grow(ByteArrayOutputStream.java:118)

系统层面的OOM,不会给你这么温柔的通知。操作系统直接干掉进程,你只能在系统日志里看到类似这样的记录:

kernel: [12345.678901] Out of memory: Killed process 12345 (blastn)
kernel: [12345.678902] Killed process 12345 (blastn) total-vm:12345678kB, anon-rss:9876543kB, file-rss:0kB

我见过最坑的一次,是Docker容器里的BLAST报OOM,开发查了三天JVM堆设置,结果发现是容器内存上限只给了2GB,而BLAST单次查询就要吃3GB。这就是系统层面的OOM,跟JVM关系不大。

注意:如果你在容器或虚拟机里跑BLAST,系统层面OOM的概率比JVM层面高得多。先看宿主机或cgroup的内存限制,别一上来就调JVM参数。

OOM对BLAST的影响

BLAST这玩意儿对内存特别敏感。我的经验是,它一旦OOM,后果往往是:

  • 序列搜索中断:跑了几个小时的比对,突然被杀,之前的结果全白费。
  • 索引文件损坏:BLAST构建的索引如果写到一半被kill,下次再读就直接报错。
  • 数据库连接池被打满:如果你用BLAST做在线服务,OOM导致节点下线,客户端全部超时重试,更容易把剩余节点打挂。
  • 日志暴涨:有些情况下,OOM前JVM会疯狂写GC日志,结果没把内存省出来,反而把磁盘写满了。
一句话总结:BLAST OOM不只是性能问题,它有可能连锁导致整个分析平台挂掉。我见过一个团队因为BLAST索引构建OOM,导致生产环境半小时不可用。

BLAST OOM的日志特征

怎么判断你的BLAST是不是OOM了?我从日志里总结了几个特征:

  1. 进程突然消失,没有正常结束的日志输出
  2. 系统日志里出现 Out of memory: Killed process 字样
  3. BLAST输出文件不完整或CRC校验失败
  4. 如果用了JNI或者C扩展,可能会看到 signal 9 (SIGKILL)

我个人习惯,每次部署BLAST前都先测一下压测下的内存占用量。拿一个已知的大库跑一遍,看看峰值内存,然后预留30%的buffer。这个习惯帮我省了很多次半夜被叫起来排查问题的经历。

OOM排查关键路径 应用层OOM JVM层面OOM 系统层面OOM 堆转储 / GC日志 dmesg / cgroup限制 定位根因:扩容 / 优化代码 排查时先看应用层日志,如果抛OutOfMemoryError就走JVM分支;如果进程直接被kill就走系统分支