zoukankan      html  css  js  c++  java
  • 【OOM】记录一次生产上的OutOfMemory解决过程

    一.项目架构

      SpringCloud  Dalston.SR1 + SpringBoot 1.5.9 + Mysql +Redis + RabbitMQ

      所有的业务模块的应用服务都部署在同一个服务器,且单实例部署,服务器配置4核32G,

    二. 原因分析:

      自己所负责的data模块这两天OOM较多,导致服务重启;

      data服务主要业务是报表相关,数仓对接的业务以及多个外部数据相关的小程序的后台,与数据库的交互比较多,业务逻辑相对其他模块较为简单,

      第一次:2月25日OOM情况:

        由于Redis反序列化失败导致的OOM

      

       第二次:2月26日的OOM情况:

        由于GC无法回收对象导致

       

       第一次发生OOM时,觉得可能就是由于Redis序列化器和反序列化器不一致,原有的JVM参数仅设置时-Xmx:512m -Xms:512m, 老年代:年轻代=2:1 ,老年代大概分配有300M内存

        时候排查问题时,发现Redis的使用都是用自己用RedisTemplate封装的工具类,按道理说不会出现什么问题,并未过多关注;

       第二次发生OOM时,与第一次相距的时间仅为1天,当时就觉得问题不对了,

        1.首先使用jmap -histo:live pid 查看 服务内存活的对象,发现 [C 类型的数组和ConcurrentHashMap对象都存活较多;

             检查代码后发现并未有显示的使用该两类类型,怀疑时String字符串过多导致的;

        2.其次使用JDK自带的分析工具:jmap -dump:format=b,file=文件名 [pid] 导出OOM时的dump日志;

          导出时间非常慢,且占用线上系统的CPU,导致CPU达到100%

        3.使用jstat -gc pid /jstat -gcutil pid 查看gc的状况

          发现gc和fgc的都非常多,特别是fgc已经达到1000多次;

       

      

       初步解决方案:(2月26日)

         最后仍然是重启服务,-添加参数Xmx1024m -Xms:1024m

         然后添加JVM参数(使用jinfo -flag可以在生产环境上直接添加)

         jinfo -flag +HeapDumpBeforeFullGC pid

         jinfo -flag +HeapDumpAfterFullGC pid

         jinfo -flag +HeapDumpOnOutOfMemoryError pid

         jinfo -flag +HeapDumpPath=/home/xxx/xxx pid 添加dump日志的目录(需要提前建好)

         jinfo -flag -XX:+PrintGCDetails pid      开启gc日志

         jinfo -flag -XX:+PrintGCDateStamps -Xloggc:/xxx/xxx  设置gc日志的目录

         修改完成后第二天根据fgc产生的dump日志,加载到jvisualVM里面之后发现也是[C占用内存较多

         下午 2点左右,监控线上服务时发现Old老年代的内存占用为300M,总大小为700M,经过一次FGC之后占用70M,这就比较正常了;

      

      重点来了:

        在2月26日添加完成JVM参数后,第二天同样的接口,FGC之前终于拿到了dump文件,大小是1.4G,接下来就是分析dump文件了,这里我选择了两个工具:

        MAT与Jvisualvm

          在使用体验来说JDK自带的Jvisualvm真的很垃圾,文件打开都要半个小时,果断放弃,转而使用MAT

        导入dump文件以后如图

        

        这里主要是看Leak Suspects:其他的几个指标在此也说明一下:

          1. Histogram可以列出内存中的对象,对象的个数以及大小。
          2. Dominator Tree可以列出那个线程,以及线程下面的那些对象占用的空间。
          3.Top consumers通过图形列出最大的object。
          4.Leak Suspects通过MA自动分析泄漏的原因。

       打开Leak Suspects后可以看到线程堆栈如图

         

         再继续找,找到是否有我们的业务代码。找到如图

         

       这里其实已经定位到具体的业务代码了,但是MAT的强大之处就是可以定位究竟是什么大对象,

      如图,这里已经可以看到了6W多个HashMap被Object[]引用,这里是内存占用的主要原因

       OK,接下来可以取看业务代码了

        

             

       业务代码如下,只展示关键代码,这个接口是报表数据导出的接口,查询mysql后使用HashMap去接收结果集,

       Object[]用于是用于写入报表工具类的入参;

      查看服务器日志后,发现这条SQL有6W多条数据,而且在一分钟之内有人操作导出了两次,导致数据封装到HashMap里面,发生FGC

        

    三   最终解决方案: 

      1.加大堆内存 原来由512扩大到1024M;

      2.HashMap改为JavaBean对象去封装结果集,因为HashMap底层是数组,还有其他的引用成员变量,更加有频繁的扩容,

        查资料后发现HashMap在数据量的情况下内存占用比Java对象要大;

      3.导出接口添加限流注解,防止在短时间内多次请求;

      以下是限流代码:使用Guava的限流组件实现,当然也可以基于Redis的实现,或者自己实现一套

      4.由于EasyExcel内存占用少,可以将poi换成阿里的EasyExcel,实现多条数据分页导出;

    /**
     * @author: Gabriel
     * @date: 2020/2/18 12:03
     * @description 自定义接口限流注解
     */
    @Target({ElementType.TYPE,ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface RateLimitAnno {
    
        /** 每秒放入令牌桶中的token */
        double limitNum() default 20;
    }
    
    
    /**
     * @author: Gabriel
     * @date: 2020/2/18 12:07
     * @description
     */
    @Slf4j
    @Aspect
    @Component
    public class RateLimitAspect {
    
        /**
         * 用来存放不同接口的RateLimiter(key为接口名称,value为RateLimiter)
         */
        private ConcurrentHashMap<String, RateLimiter> map = new ConcurrentHashMap<>();
    
        private RateLimiter rateLimiter;
    
        @Autowired
        private static ObjectMapper objectMapper = new ObjectMapper();
    
        @Autowired
        private HttpServletResponse httpServletResponse;
    
        @Pointcut("@annotation(com.gabriel.stage.annotation.RateLimitAnno)")
        public void rateLimit() {
        }
    
        /**
         * 环绕通知
         *
         * @param joinPoint
         * @return
         * @throws Exception
         */
        @Around("rateLimit()")
        public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
            Object obj = null;
            //获取拦截的方法签名
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            Object target = joinPoint.getTarget();
            //获取注解信息
            Method method = target.getClass().getMethod(signature.getName(), signature.getParameterTypes());
            RateLimitAnno annotation = method.getAnnotation(RateLimitAnno.class);
            double limitNum = annotation.limitNum();
            //获取方法名
            String functionName = signature.getName();
            //获取类名
            String className = signature.getDeclaringTypeName();
            signature.getDeclaringTypeName();
            if (StringUtils.isNotBlank(className)) {
                className = StringUtils.substringAfterLast(className, ".");
            }
            //拼接类名和方法名,保证key唯一
            String joinName = StringUtils.join(functionName, className);
    
            //获取rateLimiter
            if (map.containsKey(joinName)) {
                rateLimiter = map.get(joinName);
            } else {
                map.put(joinName, RateLimiter.create(limitNum));
                rateLimiter = map.get(joinName);
            }
    
            if (rateLimiter.tryAcquire()) {
                    obj = joinPoint.proceed();
            } else {
                System.err.println("接口限流,请求降级。。。。。。。。。。。。。。。。。");
                throw new BusinessException(ResultCode.SERVER_ERROR);
            }
            return obj;
        }

      

       

  • 相关阅读:
    CentOS7 1 安装 ansible
    CentOS7 prometheus +node_exporter+Grafana 配置篇(原创)
    CentOS7 prometheus +node_exporter+Grafana 安装篇
    Oracle 查询重复记录 只保留一条 (转载)
    MySQL 查询重复数据(转载)
    Mongodb 查询重复数据(转载)
    Python报错:pymongo.errors.CursorNotFound: Cursor not found
    百万级高并发MongoDB集群性能数十倍提升优化实践(上篇)
    OPPO百万级高并发mongodb集群性能数十倍提升优化实践(下篇) 转载
    Oracle 常见等待事件及处理方法
  • 原文地址:https://www.cnblogs.com/july-sunny/p/12370615.html
Copyright © 2011-2022 走看看