生产事故-记一次特殊的OOM排查

news2025/1/16 2:34:39

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(对于无法在生产环境上使用jstackjmap等命令直接查错的——事实上大多数时候都不能,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的内存模型:

JDK8内存模型

不难发现,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

然后借助Apacheab.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错误,如下图所示:

模拟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公司相关负责人连夜编写事故报告一份。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/829985.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【NX】NX开发入门练习寻找倒圆角设置颜色和添加属性

实现功能如标题,当时学习nx开发用的练习题,用的是nx8.5的32位版本,对于设置属性,一种是可以在界面中查看的属性,一种是隐藏的不能再界面中查看的自定义属性(但是可以正常读取)。 下面是完整代码…

Netty:ByteBuf的引用计数

说明 Netty的ByteBuf有一个对它本身的引用计数。 可以通过ByteBuf的retain()增加1个引用计数,通过retain(int increment)增加一定数量的引用计数。 通过release()减少1个引用计数,通过release(int decrement)减少一定数量的引用计数。 通过refCnt()查看…

科普 | 以太坊坎昆升级是什么

坎昆升级是什么 坎昆,是墨西哥一个著名的旅游城市,也是 Devcon 3 大会的举办地,按照以太坊升级命名的规律,以地名命名的升级,是针对以太坊执行层的升级。 之前同样命名的还有柏林升级、伦敦升级和这次的上海升级等。…

webapi部署几个错误

第一个错误:无法读取配置节“system.serviceModel”,因为它缺少节声明 发现服务器上没有安装.net Framework 3.5.1 第二个错误:未能从程序集“SYSTEM.SERVICEMODEL, VERSION3.0.0.0 先安装.net framework 4.0再安装 3.5 就会出现这个问题…

8.3线程状态

new: 调用start()方法之前的状态. blocked: 因为锁而阻塞. waiting: 因为调用wait()方法而阻塞. timed_waiting: 因为调用sleep()方法而阻塞. terminated: run()方法执行完毕之后线程的状态.

opencv-33 图像平滑处理-中值滤波cv2.medianBlur()

中值滤波是一种常见的图像处理滤波技术,用于去除图像中的噪声。它的原理是用一个滑动窗口(也称为卷积核)在图像上移动,对窗口中的像素值进行排序,然后用窗口中像素值的中值来替换中心像素的值。这样,中值滤…

【Valgrind】Valgrind安装(ubuntu系统)

👉博__主👈:米码收割机 👉技__能👈:C/Python语言 👉公众号👈:测试开发自动化【获取源码商业合作】 👉荣__誉👈:阿里云博客专家博主、5…

SonarQube入门 - 搭建本地环境

一、SonarQube是什么? SonarQube是一种自我管理的自动代码审查工具,可以系统地帮助您交付干净的代码。作为我们Sonar 解决方案的核心元素 ,SonarQube 集成到您现有的工作流程中并检测代码中的问题,以帮助您对项目执行持续的代码检…

GD32F103VE点灯

GD32F103VE点灯主要用来学习端口引脚的输出配置。它由LED.c,LED.h,SoftDelay.c和main.c组成。 #include "gd32f10x.h" //使能uint8_t,uint16_t,uint32_t,uint64_t,int8_t,int16_t,int32_t,int64_t #include "SoftDelay.h"#include …

企业微信小程序在调用wx.qy.login时返回错误信息qy.login:fail

原因是大概是绑定了多个企业但是在开发者工具中没有选择正确的企业 解决方法: 重新选择企业后即可成功获取code

BS框架说明

B/S架构 1.B/S框架,意思是前端(Browser 浏览器,小程序、app、自己写的)和服务器端(Server)组成的系统的框架结构 2.B/S框架,也可理解为web架构,包含前端、后端、数据库三大组成部分…

考研/面试 数据结构大题必会代码(理解+记忆,实现使用C++,STL库)

文章目录 一. 线性表1. 逆置顺序表所有元素2. 删除线性链表中数据域为 item 的所有结点3. 逆转线性链表(递归(快速解题)非递归)4. 复制线性链表(递归)5. 将两个按值有序排列的非空线性链表合并为一个按值有序的线性链表 二. 树1. 建立二叉树(…

【快应用】同时传递事件对象和自定义参数 ,无法正确接收事件对象

【关键词】 事件绑定、自定义参数、事件对象 【问题背景】 在快应用中采用如下方式绑定组件的事件方法&#xff0c;同时传递事件对象和自定义参数&#xff0c;发现回调方法中没有正确接收到事件对象。 问题代码如下&#xff1a; <template><!-- Only one root nod…

2023年信息系统项目管理师-粗略了解整体主要知识脉络

1. 五大过程组 2.十大知识领域 3. 十大知识领域与五大过程组的关系 4. 十大知识领域的关系图

Meta 烧掉百亿豪赌的未来:元宇宙,还有出路吗?

社交平台的下一步是什么&#xff1f; 2021年10月&#xff0c;Facebook联合创始人兼CEO马克扎克伯格在年度 Connect 开发者大会上正式宣布&#xff0c;Facebook公司将更名为Meta。这一举动标志着扎克伯格决心将公司转型为一家“元宇宙公司”&#xff0c;全力投入到元宇宙的技术…

【物联网】DMA传输原理与实现详解(超详细)

DMA&#xff08;Direct Memory Access&#xff0c;直接内存访问&#xff09;是一种计算机数据传输方式&#xff0c;允许外围设备直接访问系统内存&#xff0c;而无需CPU的干预。 文章目录 Part 1: DMA的工作原理配置阶段&#xff1a;数据传输阶段&#xff1a; Part 2: DMA数据…

java读写properties文件和xml文件,解决中文乱码问题

文章目录 前言一、properties文件1.1properties格式介绍1.2读取项目resource/templates下面properties并处理中文乱码问题1.3读取本地properties并处理中文乱码问题1.4修改properties文件 二、XML文件2.1xml文件格式2.2读取xml文件2.3写xml文件 前言 在开发当中我们经常需要用…

【NX】NX二次开发中拉伸曲线完整范例

之前做项目测试用到的代码&#xff0c;开发的版本是NX8.5的32位版本&#xff0c;这个代码实现起来也不难&#xff0c;其实就是调用了UG的API而已。 那么我在这里提供完整的代码&#xff1a; //author&#xff1a;autumoon //邮箱&#xff1a;9506163.com //日期&#xff1a;20…

spring AOP学习

概念 面向切面编程横向扩展动态代理 相关术语 动态代理 spring在运行期&#xff0c;生成动态代理对象&#xff0c;不需要特殊的编译器 Spring AOP的底层就是通过JDK动态代理或者CGLIb动态代理技术为目标Bean执行横向织入 目标对象实现了接口&#xff0c;spring使用JDK的ja…

利用微软Bing AI语言大模型辅助提高写代码、阅读代码、解bug的效率

目录 1 怎么才能使用Bing AI 2 Bing AI使用举例 2.1 代码没看懂&#xff0c;可以问Bing AI 2.2 当你不确定你程序理解的是否正确时&#xff0c;可以问Bing AI 2.3 程序编译出现bug&#xff0c;可以问Bing AI 1 怎么才能使用Bing AI 要想使用Bing AI&#xff0c;必须用微软…