线上故障分析: Memory cgroup out of memory: Kill process ...(结合dmesg 、jstack命令和jvisualvm工具综合分析)
1、在某一天时段,我生产上的程序突然宕机了,使用dmesg -T >my.log 命令输出到日志,查看系统日志,至于dmesg有什么用,参考这篇博客:https://blog.csdn.net/shuihupo/article/details/80905641?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMa
1、在某一天时段,我生产上的程序突然宕机了,部署在linux 程序被Killed掉;
使用 dmesg -T >my.log 命令输出到my.log日志,查看系统日志,至于dmesg有什么用,参考这篇博客:
2、根据dmesg -T的结果观察日志,发现如下:
查找到这条信息:Memory cgroup out of memory: Kill process 7187 (java) score 1007 or sacrifice child
为什么程序会被kill掉了?原来在Linux下, 允许应用可以预申请内存,预申请的内存如果超过了Linux最大剩余内存,由于系统的保护机制,就会停掉默认内存占用最大的应用进程;
参考这篇博客:https://blog.csdn.net/renfufei/article/details/78178757?utm_source=blogxgwz5
3、查看GC日志,发现一分钟内进行了3次Full GC
在哪些情况下,程序会进行Full GC呢,继续参考博客:
4、登录了生产的应用,发现除了宕机的其它机器上,其它机器都没有业务请求的日志,只有宕机的这一台有程序运行业务日志,是因为运维的nigix配置问题,把所有的请求都分到了宕机的这台机器上,另外的应用机器,虽然服务正常启动,但是没有接收到分发过来的用户请求;
所以我暂时把此问题理解成了运维的锅;
解决方案:给机器加内存,并且修改nigix配置,使用户请求均衡分发到每一台应用上;
5、以为问题就这样解决了,但是没有过多久,半个月以后,又发生了程序内存占用高,频繁报错的情况,虽然没有由于内存过高导致系统被Linux停掉,但很多用户的请求接口超时,无法正常使用;
先从logback中找到了这样的异常日志:Could not get a resource from the pool#012#011at redis.clients.util.Pool.getResource
查看代码,之前redisPool没有设置大小,默认为8,尝试修改一下,修改为配置文件中设置的100
spring.redis.pool.max-active=100
6、然后使用jstack -l pid,查看线程stack有什么异常:
发现里面有大量的redisson waiting(程序里面使用到的redis客户端)状态:
其实在上面,我们看到已经使用了JedisPool来作为redis客户端连接,这里又引入了RedissonClient,显得有些重复多余了
(Redisson 不仅封装了 redis ,还封装了对更多数据结构的支持,以及锁等功能,相比于Jedis 的话功能更加强大)
7、继续分析jstack输出的线程stack日志,有很多 Mysql TIMED_WAITING(先关注,这个问题在下面继续分析)
8、这里logback还改了一下输出日志级别,之前只打印了info日志,现在BusinessException异常的改成error级别日志,看看还有哪些程序错误没有通过error日志抛出来:
9、然后程序上线了一版,继续监控可能会出现的问题:
发现了mysql锁等待超时异常(也印证了上面第7步 jstack日志中,很多 Mysql TIMED_WAITING 状态的问题):
Error updating database. Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction#012### The error may involve com********.dao.iface.SessionRecordMapper.updateByPrimaryKey
这里的异常可能之前日期就有发生过,由于上面第8步,之前的info日志输出,可能导致了忽略关注,现在才看到;
10、分析Mysql Lock wait timeout exceeded异常:
com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: Lock wait timeout exceeded:
这里的一行sql语句,执行update *** where 的时候,报错;先看一下这个表结构:
在表t_sys_session_rec中,sessionId是作为主键,主键是默认走索引的,并且在innodb引擎下,是支持行锁的;这一行数据,相同sessionId的情况,sql执行的并发量也不是很大,为什么会频繁出现等待锁超时呢?
找到执行sql的代码,updateByPrimaryKey方法:
找 updateSession调用的方法,在/ticket校验方法里面,用户每次过来校验ticket的时候,都会调用这个方法,更新updateSession:
就是这里,发生mysql锁频繁超时的地方,是否还有其它地方去竞争这行数据,t_sys_session_rec 这张表的资源呢?
11、发现定时任务在竞争表资源:
这个定时的大概思路是:每60秒执行一次,写一个线程监听,while(true),然后一直轮询,查询数据库后台表t_sys_session_rec,当前时间now(),比update字段时间值,计算出来大于 overTime 6个小时的session记录,逐条置为无效
(这种定时任务执行方式不推荐):
12,找到mapper.xml中的 sessionRecordMapper.selectAllWillExpireSession 方法:
上面where条件里面的createIp字段和updateTime字段并没有加索引,导致全表扫描,并且随着表中数据越来越多的情况下,导致了锁表情况发生,所以上面的 sessionRecordMapper.updateByPrimaryKey()方法才会发生锁等待超时;
(其实根源问题在于这里,由于这个锁表操作,/ticket接口的其它请求线程,是先获取redis连接,并且获取以后没有释放redis连接,然后再去获取mysql锁,由于拿不到mysql锁,并且redis连接池默认连接数8,并且mysql连接池设置的是100,所以先是redis连接不够用,在上面第5、第6步抛出了异常Could not get a resource from the pool at redis.clients.util.Pool.getResource)
13、解决方案:
开始想了一种简单解决方案,清除表中记录,createIp字段和updateTime字段增加索引,那么就不需要重启应用程序,让用户正常继续使用程序,但是能解决问题吗?
updateTime经常被更新,如果字段值经常被修改,那么每次数据库重建索引的开销也很大,短期来看,可以临时解决现在的问题,就算加了索引以后,这里的where条件查询对updateTime使用了函数计算,表达式,也会导致查询索引失效;
为了验证这一点,在测试环境,先给updateTime字段加上索引,去掉其它几个字段的where条件检索,然后explain执行一下sql语句,发现扫描的type 是 all:
(还有哪些情况会导致索引失效,可以参考这里:
所以最后的解决方案:是删除t_sys_session_rec记录表,把用户校验的ticket存取都用redis(这里涉及修改的代码就比较多了,不全贴出来);
14、到这里还没有结束,由于还有内存占用过高的问题,继续从线上服务器导出堆dump文件分析:
jmap -dump:live,format=b,file=/data1/springBoot/heap.hprof pid
(上面的命令,是实时导出dump文件,会影响系统运行性能;还有另外一种导出dump文件方式,在java进程启动参数配置:
加上 例如 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/heap.hprof,
完整启动参数:
JAVA_OPTS="$JAVA_OPTS -server -Xms3G -Xmx3G -Xss256k -XX:PermSize=128m -XX:MaxPermSize=128m -XX:+UseParallelOldGC -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/heap.hprof -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:/usr/local/gc.log -XX:NewSize=1G -XX:MaxNewSize=1G"
这样在程序宕机之前,会给你生成一份dump文件,参考这篇博客:https://www.cnblogs.com/m2492565210/p/7498438.html)
15、使用jdk本地自带的jvisualvm工具,打开D:\Program Files\Java\jdk1.8.0_201\bin\jvisualvm.exe,分析dump出来的文件:点击文件,装入:
选择刚才dump出来的文件heap.hprof:
16、发现如下些对象,占用比例是最多的:
HashMap里面的Node对象内部类(HashMap$Node,HashMap底层其实就是Node对象组成的数组,Node是内部类),Hashtable对象里面的Entry内部类(Hashtable$Entry),还有char[],byte[],jdbc连接(Mysql Lock wait timeout exceeded 产生的过多对象侧面在这里印证了)等等,这些是占用内存最高,或者实例创建最多的对象:
17、结合代码再来看上面的堆内存分析问题:
这个系统,是一个单点集成登录功能,程序调用最多的就是 */ticket 票据校验接口,看接口入口的第50行代码,
仅仅是取参数,里面就创建了两个HashMap对象 ,为了就是把Map对象中的key和value全部遍历一遍然后去空格?这样的开销很大,本来HashMap对象就是用哈希算法存取对象的一种方式,以存储空间换时间的理念集合对象,HashMap会比其它集合更占用内存空间
退出这个方法,再回到刚才/ticket校验接口那里,52行代码处,paramMap = javaUtil.getMapForSync(request); 进方法:
里面又创建了HashMap:
这样一来,每天/ticket 校验接口访问量在40~50w左右,那么光HashMap参数对象就会创建150w个左右。
还有程序其它有使用com.alibaba.fastjson.JSONObject地方:
进入 com.alibaba.fastjson.JSONObject 对象中:在JSONObject对象的底层,也是基于Map实现的
所以也就解释了jvisualvm分析出来那么多Map对象的原因了;
所以除了上面mysql的 Lock wait timeout exceeded异常,这里的代码也是需要优化的地方;
18、继续看jvisualvm中String对象占用的问题:
代码这里,创建了好几个字符串对象,只是用来校验一下是否必填?(类似这样的写法,在其他地方也发现了)
String类的底层实现,其实是char[]数据,我们可以在源码中看到,常用构造方法:这里的this.value就是一个char[]数组:
this.value = original.value;
所以也就解释了,上图中用jvisualvm分享出来,那么多String和char[]对象了:
19、关于Hasstable$Entry 那么多对象,搜了一下,代码中并没有使用过Hashtable,
但是使用了Properties,Properties的底层就是一个Hashtable,但是程序中也没有使用太多Properties对象,所以为什么那么多Hashtable$Entry对象,也暂时不知道了
总结问题:主要还是之前系统设计不合理导致锁表,代码编写不规范,导致程序内存过高的问题;
在这里总结分析一下,如果有不对的地方,欢迎指出;
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)