记生产环境因流量激增后,因Shiro内存溢出,pod频繁重启,问题定位及解决方案
一、背景
2023年新年过后,迎来了第一次技术难题和挑战,系统在前两天用户量激增导致部分功能降级,临时增加了pod数量进行系统扩容,心惊胆颤的扛过了用户高峰期,本以为之后一帆风顺,谁知灾难随之降临,多个服务的pod频繁重启;经历了高峰星期二,迎来了黑色星期四;
二、分析问题
pod频繁重启,第一个想到的是不是业务量增加导致的,通过监控平台发现,现有业务量非常小,部分服务请求量基本为个位数,那到底是什么原因导致pod在短时间内频繁重启?进行服务扩容后依然无效。
联系我们运维兄弟拉取了k8s日志,
发现因oom-kill掉了响应进程。
查看pod也发现了上个pod杀掉的原因是OOMKill的;
很奇怪为啥基本没有用户量为啥还会OOM呢?
2.2、Java手术刀
为了进一步发现问题,上Java的手术刀,我是这样称呼这个工具的,阿里的arthas;这个工具非常适合监控JVM运行情况;
看当前活动线程和内存使用量,活动线程基本没有,大部分都是守护线程,说明现在服务基本上没有外部请求,内存使用837M也在正常范围内。我们继续持续监控;
经过2分钟后,我们发现执行了两次Young GC,内存已经降到了297M,说明此时GC还是正常的,也没有额外的活动线程,没有发现问题,我们继续观察;
运行了10分钟后,内存使用到1232M,还没有执行GC,整个pod突然重启了,死的有点搓手不及...
唯一留下的就是这张“死前仪容”,我们看到内存使用率只有63%,就死了,原因还是OOM,这个情况让我有点措手不及,为啥OOM,内存还有很多,这个时候,我分析问题的方向已经开始转移了,会不会k8s误杀了?
我们知道k8s在检查pod健康状态时有个内存的临界值,超过pod资源分配的85%时,会进行重启,而这里我们配置的资源量是2G,而JVM配置的也是2G;是不是因为健康检查机制触发了pod频繁重启。
为了排除k8s的健康检查机制,我把资源限制改成了3G,jvm还是2G,这样就不会触发健康检查导致的pod重启。
配置修改后,再次进行监控...
内存没有限制后,最后频繁执行Full GC 但内存已经无法回收了,此时服务进入到了假死状态...
看到这里,我基本上已经确定服务发生了内存泄露,按照老规矩,获取当前状态下的dump文件吧。
通过dump文件,我们可以看到char[]、String、HashMap三个对象占用了绝大多数的内存,根据经验,我们分析下HashMap对象绝大多数都是这个对象是罪魁祸首。
我们看到这个对象占了1G以上的内存,占总用量的92%,还是存在引用的不能进行回收。我们继续分析看看它是什么鬼?
看到这里,是不是有点熟悉,getActiveSessions?
然后去看下了Shiro配置,发现果然配置了setSessionValidationSchedulerEnabled为true;
sessionManager.setGlobalSessionTimeout(EXPIRE_SECOND * 1000);
//是否开启删除无效的session对象 默认为true
sessionManager.setDeleteInvalidSessions(true);
//是否开启定时调度器进行检测过期session 默认为true
sessionManager.setSessionValidationSchedulerEnabled(true);
//设置session失效的扫描时间, 清理用户直接关闭浏览器造成的孤立会话 默认为 1个小时
//设置该属性 就不需要设置 ExecutorServiceSessionValidationScheduler 底层也是默认自动调用ExecutorServiceSessionValidationScheduler
sessionManager.setSessionValidationInterval(600000);
经过研究源代码发现有需要进行认证授权等验证的请求进入的时候,DefaultSessionManager对象(即上面配置的"sessionManager"bean)会判断其中的sessionValidationScheduler属性对象是否存在(该对象负责执行会话超时校验逻辑)。如果不存在,会创建一个该对象,并通过ScheduledExecutorService创建守护线程定时执行。
但是该创建过程没有做并发控制,当请求并发大的时候,会创建很多个sessionValidationScheduler对象。虽然DefaultSessionManager对象中只保留了一个引用,但是ScheduledExecutorService创建守护线程中还维持了这些多余的sessionValidationScheduler对象。这样当执行周期到的时候,会有很多个线程一起将会话对象加载到内存,从而导致内存瞬时飙升。这也是为啥内存使用60%的时候直接挂掉了。
通过监控平台,也发现服务挂掉存在周期性。基本上定位到是这个问题导致的。
三、解决问题
问题定位到了,我们想办法去解决问题吧;
解决方法很简单:将ExecutorServiceSessionValidationScheduler配置成bean,并注入到"sessionManager"bean中。这样就不会生成重复的SessionValidationScheduler对象。
代码如下:
......
//全局会话超时时间(单位毫秒),默认30分钟 暂时设置为10秒钟 用来测试
sessionManager.setGlobalSessionTimeout(EXPIRE_SECOND * 1000);
//是否开启删除无效的session对象 默认为true
sessionManager.setDeleteInvalidSessions(true);
//是否开启定时调度器进行检测过期session 默认为true
sessionManager.setSessionValidationSchedulerEnabled(true);
//设置session失效的扫描时间, 清理用户直接关闭浏览器造成的孤立会话 默认为 1个小时
//设置该属性 就不需要设置 ExecutorServiceSessionValidationScheduler 底层也是默认自动调用ExecutorServiceSessionValidationScheduler
sessionManager.setSessionValidationInterval(600000);
//取消url 后面的 JSESSIONID
sessionManager.setSessionIdUrlRewritingEnabled(false);
sessionManager.setSessionValidationScheduler(sessionValidationScheduler());
.....
@Bean
public ExecutorServiceSessionValidationScheduler sessionValidationScheduler() {
return new ExecutorServiceSessionValidationScheduler();
}
发布上线...完美解决;周五可以愉快的下班了...