一.项目架构

  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;
}

  

   

最新文章

  1. BZOJ 1003 物流运输【最短路】【动态规划】
  2. Vector和ArrayList的比较
  3. Android adb命令 一
  4. [poj1200]Crazy Search(hash)
  5. mysql 在insert 时防止出现主键冲突错误的方法
  6. HDU4831&amp;&amp;4832&amp;&amp;4834
  7. MVVM解决方案的一般结构
  8. Chrome 插件vimium快捷键大全
  9. STM32 定时器用于外部脉冲计数(转)
  10. Python3基础 用 while循环实现 斐波那契数列
  11. LazyInitializationException--由于session关闭引发的异常
  12. 大数相加 Big Num
  13. [最新]ABP ASP.NET Zero v5.5.2 破解
  14. 循环队列搜索 Search in Rotated Sorted Array
  15. 【原创】ucos信号量的操作及原理
  16. Linux LB--负载均衡和高可靠
  17. PAT A1013 Battle Over Cities (25 分)——图遍历,联通块个数
  18. [LintCode] Binary Tree Level Order Traversal(二叉树的层次遍历)
  19. [JZOJ 5402] God Knows
  20. 利用rundll32执行程序的函数执行程序

热门文章

  1. [同步到 MaixPy3 文档] 使用 Python 编程入门开源硬件项目
  2. CVE-2019-10758-Mongo-express-远程代码执行
  3. TiDB在更新版本的时候初始化Prometheus的配置文件失败
  4. Android R 新特性分析及适配指南
  5. js输入框只能输入数字
  6. 【odoo14】第四章、应用模型
  7. Android的Proxy/Delegate Application框架
  8. Java入门环境的搭建
  9. CPU 权限划分
  10. PAT (Basic Level) Practice (中文)1054 求平均值 (20 分) 凌宸1642