生产事故-记一次特殊的OOM排查
入职多年,面对生产环境,尽管都是小心翼翼,慎之又慎,还是难免捅出篓子。轻则满头大汗,面红耳赤。重则系统停摆,损失资金。每一个生产事故的背后,都是宝贵的经验和教训,都是项目成员的血泪史。为了更好地防范和遏制今后的各类事故,特开此专题,长期更新和记录大大小小的各类事故。有些是亲身经历,有些是经人耳传口授,但无一例外都是真实案例。
注意:为了避免不必要的麻烦和商密问题,文中提到的特定名称都将是化名、代称。
0x00 大纲
0x01 事故背景
2023年3月10日14时19分,C公司开发人员向A公司开发人员反映某开放接口从2023年3月10日14时许开始无法访问和使用。该系统为某基础数据接口服务,基于 HTTP 协议进行通信。按照惯例,首先排查网络是否异常,经运维人员检查,证明网络连通性没有问题。A公司开发组于2023年3月10日14时30分通知运维人员重启应用服务,期间短暂恢复正常。但是,很快,十分钟后,电话再次响起,告知服务又出现异常,无法访问。为了避免影响进一步扩大,A公司决定将程序紧急回滚至上一稳定版本。回滚后,系统业务功能恢复正常。短暂松一口气后,开始排查问题。
0x02 事故分析
让运维拷贝和固定了更新前后的系统日志和应用包。根据前面的故障现象,初步猜测是内存问题,好在应用启停脚本中增加了参数-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/app/logs/app.dump(对于无法在生产环境上使用jstack、jmap等命令直接查错的——事实上大多数时候都不能,dump文件显得尤为重要),果不其然,日志目录下出现了app.dump文件,在日志中搜索,找到了若干处内存溢出错误java.lang.OutOfMemoryError: Java heap space,但是令人费解的是每次出现OOM错误的位置居然都不一样,事情逐渐变得复杂起来。
用MAT(Memory Analyzer Tool)工具打开转储文件,原以为会发现某个类型对象占用大量的内存,结果出乎意料,Histogram(直方图)中显示活跃对象居然只有100多M!尝试 Calculate Precise Retained Size(计算精确大小),计算结果与前面相差不大。检查 Outgoing References (追踪引用对象)和 Incoming References(追踪被引用对象)也未见明显异常,令人头大。
擦擦汗,日志已经明确提示我们java.lang.OutOfMemoryError: Java heap space,首先肯定这是一个堆内存空间引起的问题,可能的原因有:
内存加载数据量过大
例如不受行数限制的数据库查询语句,或者不限制字节数的文件读取等,事故系统显然没有这些情况;
内存泄漏(资源未关闭/无法回收)
当系统存在大量未关闭的 IO 资源,或者错误使用
ThreadLocal等场景时也会发生OOM,经排查,也不存在这种情况;系统内存不足
系统内存不足以支撑当前业务场景所需要的内存,过小的机器内存或者不合理的JVM内存参数。
如果排除所有合理选项,最不合理那个会不会就是答案呢?遂开始检查机器的内存,根据运维的说法,机器内存为16GB,top命令查看java进程占用内存约为7.8GB,看起来似乎没毛病。
但是随后另一个同事注意到了一个事情,最后一次系统升级的时候,改动过应用启停脚本,对比旧版本的脚本,发现差异部分就是内存参数:
旧版本原为:
-Xms8g -Xmx8g -Xmn3g
新版本改为:
-Xms8g -Xmx8g -Xmn8g
看到这里,屏幕前的一众同事都无语啊……
0x03 事故原因
为什么-Xmn参数设置成与-Xmx参数一样的大小会导致OOM呢?该项目使用的JDK版本为1.8,看看JDK 8的内存模型:

不难发现,Heap Space Size = Young Space Size + Old Space Size,而-Xmn参数控制的正是 Young 区的大小,当堆区被 Young Gen 完全挤占,又有对象想要升代到 Old Gen 时,发现 Old 区空间不足,于是触发 Full GC,触发 Full GC 以后呢,通常又会面临两种情况:
- Young 区又刚好腾出来一点空间,对象又不用放到 Old 区里面了,皆大欢喜
- Young 区空间还是不够,对象还是得放到 Old 区,Old 区空间不够,卒,喜提
OOM - 诶,就是奔着 Old 区去的,管你 Young 不 Young,Old 区空间不够,卒,喜提
OOM
这个就解释了为什么系统刚刚启动时,会有一个短时间正常工作的现象,随后,当某段程序触发 Old Gen 升代时,就会发生随机的OOM错误。那么什么时候对象会进入老年代呢?这里也很有意思,不妨结合日志里面出现OOM的地方,对号入座:
- 经历足够多次数 GC 依然存活的对象
- 申请一个大对象(比如超过 Eden 区一半大小)
- GC 后 Eden 区对象大小超过 S 区之和
- Eden 区 + S0 区 GC 后,S1 区放不下
换言之,正常情况下,-Xmn参数总是应当小于-Xmx参数,否则就会触发OOM错误。我们可以构造一个简单的例子来验证这个场景。首先是一个简单的SpringBoot程序:
package com.example.oom;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Random;
@SpringBootApplication
public class OomApplication {
static final byte[] ARRAY = new byte[128 * 1024 * 1024];
public static void main(String[] args) {
SpringApplication.run(OomApplication.class, args);
}
@RestController
public static class OomExampleController {
@GetMapping("/oom")
public int oom() {
byte[] temp = new byte[128 * 1024 * 1024];
temp[0] = (byte) 0xff;
temp[temp.length - 1] = (byte) 0xef;
int noise = new Random().nextInt();
ARRAY[0] = (byte) (temp[0] + temp[temp.length - 1] + noise);
return ARRAY[0];
}
}
}
使用mvn clean package命令打包后,我们用下面的命令启动它:
java -Xms512m -Xmx512m -Xmn512m -XX:+HeapDumpOnOutOfMemoryError -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -Xloggc:gc.log -jar oom-1.0.0-RELEASE.jar
然后借助Apache的ab.exe,完成我们的验证测试。先是以1个并发访问100次上面的SpringBoot接口:
ab -c 1 -n 100 http://localhost:8080/oom
你会发现,它居然是可以正常运行的,然后我们模拟用户负载上来之后的情况,使用2个并发访问100次:
ab -c 2 -n 100 http://localhost:8080/oom
如果前面的步骤都没错,此时应该在SpringBoot应用控制台看到大量的OOM错误,如下图所示:

然后在 GC 日志里面会看到,触发 GC 的前后,Old 区几乎都没有空间,仅有的一点点还是JDK强行分配的(在启动JVM时强制覆写了我们的-Xmn参数):
{Heap before GC invocations=279 (full 139):
PSYoungGen total 458752K, used 273877K [0x00000000e0080000, 0x0000000100000000, 0x0000000100000000)
eden space 393728K, 69% used [0x00000000e0080000,0x00000000f0bf5798,0x00000000f8100000)
from space 65024K, 0% used [0x00000000fc080000,0x00000000fc080000,0x0000000100000000)
to space 65024K, 0% used [0x00000000f8100000,0x00000000f8100000,0x00000000fc080000)
ParOldGen total 512K, used 506K [0x00000000e0000000, 0x00000000e0080000, 0x00000000e0080000)
object space 512K, 98% used [0x00000000e0000000,0x00000000e007e910,0x00000000e0080000)
Metaspace used 35959K, capacity 38240K, committed 38872K, reserved 1083392K
class space used 4533K, capacity 4953K, committed 5080K, reserved 1048576K
2023-04-07T01:44:25.348+0800: 57.446: [GC (Allocation Failure) --[PSYoungGen: 273877K->273877K(458752K)] 274384K->274384K(459264K), 0.0441401 secs] [Times: user=0.06 sys=0.30, real=0.04 secs]
Heap after GC invocations=279 (full 139):
PSYoungGen total 458752K, used 273877K [0x00000000e0080000, 0x0000000100000000, 0x0000000100000000)
eden space 393728K, 69% used [0x00000000e0080000,0x00000000f0bf5798,0x00000000f8100000)
from space 65024K, 0% used [0x00000000fc080000,0x00000000fc080000,0x0000000100000000)
to space 65024K, 9% used [0x00000000f8100000,0x00000000f86e2070,0x00000000fc080000)
ParOldGen total 512K, used 506K [0x00000000e0000000, 0x00000000e0080000, 0x00000000e0080000)
object space 512K, 98% used [0x00000000e0000000,0x00000000e007e910,0x00000000e0080000)
Metaspace used 35959K, capacity 38240K, committed 38872K, reserved 1083392K
class space used 4533K, capacity 4953K, committed 5080K, reserved 1048576K
}
{Heap before GC invocations=280 (full 140):
PSYoungGen total 458752K, used 273877K [0x00000000e0080000, 0x0000000100000000, 0x0000000100000000)
eden space 393728K, 69% used [0x00000000e0080000,0x00000000f0bf5798,0x00000000f8100000)
from space 65024K, 0% used [0x00000000fc080000,0x00000000fc080000,0x0000000100000000)
to space 65024K, 9% used [0x00000000f8100000,0x00000000f86e2070,0x00000000fc080000)
ParOldGen total 512K, used 506K [0x00000000e0000000, 0x00000000e0080000, 0x00000000e0080000)
object space 512K, 98% used [0x00000000e0000000,0x00000000e007e910,0x00000000e0080000)
Metaspace used 35959K, capacity 38240K, committed 38872K, reserved 1083392K
class space used 4533K, capacity 4953K, committed 5080K, reserved 1048576K
2023-04-07T01:44:25.392+0800: 57.490: [Full GC (Ergonomics) [PSYoungGen: 273877K->142631K(458752K)] [ParOldGen: 506K->506K(512K)] 274384K->143137K(459264K), [Metaspace: 35959K->35959K(1083392K)], 0.0248171 secs] [Times: user=0.14 sys=0.00, real=0.03 secs]
接着无需改动任何代码,我们调整下启动参数,像这样:
java -Xms512m -Xmx512m -Xmn64m -XX:+HeapDumpOnOutOfMemoryError -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -Xloggc:gc.log -jar oom-1.0.0-RELEASE.jar
你会发现它又可以了。这是一个为了验证而打造的极端例子,实际上生产的应用情况会比这个复杂得多,但这并不妨碍我们理解它的意图。
0x04 事故复盘
这是一场典型的”人祸“,来源于某个同事的”调优“,比起追究责任,更重要的是带给我们的启发:
- 即使是应用启停脚本,也应该作为程序的一部分,纳入测试验证流程和上线检查清单,禁止随意变更;
- 很多时候,默认的就是最好的,矫枉则常常过正。
0x05 事故影响
造成C公司关键业务停摆半小时,生产系统紧急回滚一次。A公司相关负责人连夜编写事故报告一份。
生产事故-记一次特殊的OOM排查的更多相关文章
- 一次 select for update 的悲观锁使用引发的生产事故
1.事故描述 本月 8 日上午十点多,我们的基础应用发生生产事故.具体表象为系统出现假死无响应.查看事发时间段的基础应用 error 日志,没发现明显异常.查看基础应用业务日志,银行结果处理的部分普遍 ...
- 【ABAP系列】SAP 读取生产订单 记入文档的货物移动明细
公众号:SAP Technical 本文作者:matinal 原文出处:http://www.cnblogs.com/SAPmatinal/ 原文链接:[ABAP系列]SAP 读取生产订单 记入文档的 ...
- 生产事故(MongoDB数据分布不均解决方案)
可以很明显可以看到我们这个集合的数据严重分布不均匀. 一共有8个分片,面对这个情况我首先想到的是手动拆分数据块,但这不是解决此问题的根本办法. 造成此次生产事故的首要原因就是片键选择上的问题,由于片键 ...
- 记一次线上bug排查-quartz线程调度相关
记一次线上bug排查,与各位共同探讨. 概述:使用quartz做的定时任务,正式生产环境有个任务延迟了1小时之久才触发.在这一小时里各种排查找不出问题,直到延迟时间结束了,该任务才珊珊触发.原因主要就 ...
- 【转】又一次线上 OOM 排查经过
又一次线上OOM排查经过 最近线上一个服务又出现了频繁Full GC的情况,导致提供的业务经常超时.问题出现非常不稳定,经过两周的时候,终于又捕捉到了一次Full GC,于是联系运维做Heap Dum ...
- 解Bug之路-记一次存储故障的排查过程
解Bug之路-记一次存储故障的排查过程 高可用真是一丝细节都不得马虎.平时跑的好好的系统,在相应硬件出现故障时就会引发出潜在的Bug.偏偏这些故障在应用层的表现稀奇古怪,很难让人联想到是硬件出了问题, ...
- 记一次重大生产事故,在那 0.1s 我想辞职不干了!
一.发生了什么? 1.那是一个阳光明媚的下午,老婆和她的闺蜜正在美丽的湖边公园闲逛(我是拎包拍照的). 2.突然接到甲方运营小妹的微信:有个顾客线上付款了,但是没有到账,后台卡在微信支付成功(正常状态 ...
- 记一次生产事故的排查与优化——Java服务假死
一.现象 在服务器上通过curl命令调用一个Java服务的查询接口,半天没有任何响应.关于该服务的基本功能如下: 1.该服务是一个后台刷新指示器的服务,即该服务会将用户需要的指示器数据提前计算好,放入 ...
- 记一次小型生产事故 | BeyondComper跨编码方式复制文件内容
前言 今天组长在做站内巡检的时候,发现header内有一条meta标签的content显示为乱码. <meta name="description" content=&quo ...
- 记一个有趣的Java OOM!
原文:https://my.oschina.net/u/1462914/blog/1630086 引言 熟悉Java的童鞋,应该对OOM比较熟悉.该类问题,一般都比较棘手.因为造成此类问题的原因有很多 ...
随机推荐
- 一招彻底解决——双系统下windows时间错误显示
双系统下windows与ubuntu系统时间不同步 我的笔电是windows系统,工作需要安装了ubuntu,于是成为双系统,于是从18年我就遇到了装了ubuntu系统之后windows时间显示错误的 ...
- 【python】第一模块 步骤五 第一课、内存管理机制
第一课.内存管理机制 一.课程介绍 1.1 课程概要 课程概要 赋值语句的内存分析 垃圾回收机制 内存管理机制 课程目标 掌握赋值语句内存分析方法 掌握id()和is()的使用 了解python的垃圾 ...
- vue组件传参,父子组件以及兄弟组件(非常详细)
一,父子组件传参. 1.首先在项目目录中新建template文件夹,里边包含父组件:List.vue以及子组件:firstComponent.vue,secondComponent.vue. 2.父组 ...
- 面向对象2(Java)
封装 基本介绍 该露的露,该藏的藏,我们的程序设计要追求"高内聚,低耦合": 高内聚:类的内部数据操作细节自己完成,不允许外部干涉 低耦合:仅暴露少量的方法给外部使用 封装(数据的 ...
- thymeleaf基础学习
Thymeleaf 1.标准表达式 ${...}:变量表达式 *{...}: 选择表达式 #{...} : 消息表达式 @{...}: 连接表达式 <img th:src="@{url ...
- MySQL分库分表原理
转自https://www.jianshu.com/p/7aec260ca1a2 前言 在互联网还未崛起的时代,我们的传统应用都有这样一个特点:访问量.数据量都比较小,单库单表都完全可以支撑整个业务. ...
- Python自学day03
1.数据类型 int 1,2,3用于计算 bool True,False用于判断 str 存储少量数据,用于操作 list 存储大量数据 [1,2,3,'a ...
- 使用JAX构建强化学习agent并借助TensorFlowLite将其部署到Android应用中
在之前发布文章<一个新 TensorFlow Lite 示例应用:棋盘游戏>中,展示了如何使用 TensorFlow 和 TensorFlow Agents 来训练强化学习 (RL) ag ...
- 服务器中VirtualBox子网访问
本人常用的虚拟机软件是VirtualBox,由于笔记本性能,磁盘存储大下限制,以及VirtualBox客户机无法在多个设备间直接方便的使用等原因,我把几个虚拟的系统全部移动到便携式服务器中. 移动之后 ...
- OSPF之Router id冲突