第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了?我从日志里总结了几个特征:
- 进程突然消失,没有正常结束的日志输出
- 系统日志里出现
Out of memory: Killed process字样 - BLAST输出文件不完整或CRC校验失败
- 如果用了JNI或者C扩展,可能会看到
signal 9 (SIGKILL)
我个人习惯,每次部署BLAST前都先测一下压测下的内存占用量。拿一个已知的大库跑一遍,看看峰值内存,然后预留30%的buffer。这个习惯帮我省了很多次半夜被叫起来排查问题的经历。