性能文章>记一次JVM OOM 实战优化>

记一次JVM OOM 实战优化原创

1年前
7330210

刚接手的服务,正常稳定运行了很长一段时间,在大家伙收拾东西准备回家过年时,突然就抽风了。

接口失败率居高不下?

看日志!

GC overhead limit exceeded
java.lang.OutOfMemoryError:GC overhead limit exceeded

看一下JVM堆栈

sudo jmap -heap port
#eg:sudo jmap -heap 9999

image.png

很明显,是内存不够了。

我当时的第一想法就是,应该是内存泄漏!我的思路如下:

1、先入为主,因为之前处理过一次因内存泄漏导致的JVM OOM问题,所以当时高度怀疑内存泄漏。

2、导出JVM堆数据,分析、定位问题。

3、fix bug,重新部署,finish!

我按照这个思路,分析堆栈后,发现堆中有大量对象,这些对象还没来得及回收,其他线程又在申请内存,从而导致了OOM。造成问题的直接原因是业务请求量增加了,而现有的机器资源不够用。至于间接原因,下一篇文章再详细描述。

在揭开问题的谜底后,回过头想一想,如果当时能够仔细分析一下问题,或许问题会被更快解决。

事后反思:服务已经稳定正常运行了一段时间,且一个月内未修改代码和更新服务。如果是代码有问题,那么问题极大可能会在新代码上线后的几天内出现。基于这一点,基本可以排除代码问题。

线上服务出现问题,首要的任务就是尽快恢复服务可用。如果下次出现类似问题,我会选择流程一,而非流程二。

image.png

image.png

造成服务不可用的直接原因是服务请求量上升,而根本原因是由于下游服务负载过高,导致微服务调用超时,从而引起连锁反应。

下图呈现了用户发起请求到响应完成的大致流程。

image.png

业务服务接口和算法服务接口使用eureka作为服务注册中心,整体来说,这个服务采用相对简单的微服务架构。

服务接口搭配feign来请求算法服务接口。

@FeignClient(value = "image-service")
public interface ImageService {
    
    @PostMapping(value = "/XXX")
    String XXX(@RequestParam("img_base64") String imgBase64);
}

上面的代码在执行请求的时候,会将请求参数进行拼接。

image-service/xxx?img_base64=fjsfdgldfgrwdfdmgfdglwefsl

当eureka真正确定请求的服务地址后,又会再做一次拼接处理。

127.0.0.1:5000/xxx?img_base64=fjsfdgldfgrwdfdmgfdglwefsl

算法服务接口的处理时间与图片的大小正相关,图片越大,处理时间越长。由于处理图片是一个相对耗时的操作,接口会出现超时的情况。如果请求失败(超时),那么feign会进行重试。

图片base64字符串的长度与图片大小呈正相关关系,图片越大,base64字符串长度越长。一张306K的图片,转成base64格式后,字符串长度为429196。

image.png

因此处理一次正常的请求消耗的内存比较大。图片越大,算法处理时间越久,超时失败后,feign重试,重试之后又失败,导致一个恶性循环(幸好有超时次数限制,否则如此递归下去,后果不堪设想)。

如图是JVM OOM后拿到堆栈的数据,最大的图片base64的大小有5M。

image.png

jdk1.8 JVM参数PretenureSizeThreshold的默认值是2M。

image.png

当base64字符串超过2M时,会直接分配到老年代,这无疑加大了JVM老年代的内存压力,导致频繁Full GC。

image.png

为何使用image base64传输图片?

1、历史原因。

2、开发相对简单。

该如何优化?

1、提高feign请求的超时时间。

2、提高机器配置。

3、将image base64放到请求体中,减少因feign框架对参数进行拼接带来的内存开销。

请先登录,再评论

提供一思路,既然不能更改上下游接口对接方式,可以针对base64字符串超过2M的进行分割处理,可以将对象分配在新生代,同时也可以避免接口失败后的大对象依旧存活在老年代

11年前

base64放到请求体一样会增加内存开销,控制好重试和结果缓存。

21年前

为你推荐

不起眼,但是足以让你有收获的JVM内存分析案例
分析 这个问题说白了,就是说有些int[]对象不知道是哪里来的,于是我拿他的例子跑了跑,好像还真有这么回事。点该 dump 文件详情,查看相关的 int[] 数组,点该对象的“被引用对象”,发现所
从一起GC血案谈到反射原理
前言 首先回答一下提问者的问题。这主要是由于存在大量反射而产生的临时类加载器和 ASM 临时生成的类,这些类会被保留在 Metaspace,一旦 Metaspace 即将满的时候,就会触发 Fu
关于内存溢出,咱再聊点有意思的?
概述 上篇文章讲了JVM在GC上的一个设计缺陷,揪出一个导致GC慢慢变长的JVM设计缺陷,可能有不少人还是没怎么看明白的,今天准备讲的大家应该都很容易看明白 本文其实很犹豫写不写,因为感觉没有
协助美团kafka团队定位到的一个JVM Crash问题
概述 有挺长一段时间没写技术文章了,正好这两天美团kafka团队有位小伙伴加了我微信,然后咨询了一个JVM crash的问题,大家对crash的问题都比较无奈,因为没有现场,信息量不多,碰到这类问题我
又发现一个导致JVM物理内存消耗大的Bug(已提交Patch)
概述 最近我们公司在帮一个客户查一个JVM的问题(JDK1.8.0_191-b12),发现一个系统老是被OS Kill掉,是内存泄露导致的。在查的过程中,阴差阳错地发现了JVM另外的一个Bug。这个B
JVM实战:优化我的IDEA GC
IDEA是个好东西,可以说是地球上最好的Java开发工具,但是偶尔也会卡顿,仔细想想IDEA也是Java开发的,会不会和GC有关,于是就有了接下来对IDEA的GC进行调优 IDEA默认JVM参数: -
不起眼,但是足以让你收获的JVM内存案例
今天的这个案例我觉得应该会让你涨姿势吧,不管你对JVM有多熟悉,看到这篇文章,应该还是会有点小惊讶的,不过我觉得这个案例我分享出来,是想表达不管多么奇怪的现象请一定要追究下去,会让你慢慢变得强大起来,
如何通过反射获得方法的真实参数名(以及扩展研究)
前段时间,在做一个小的工程时,遇到了需要通过反射获得方法真实参数名的场景,在这里我遇到了一些小小的问题,后来在部门老大的指导下,我解决了这个问题。通过解决这个问题,附带着我了解到了很多新的知识,我觉得