一.項目架構
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; }