故障现象
生产环境上线新功能,我们采用灰度的方式足台更新服务节点,保证能持续提供服务。最近一次更新发现在节点启动完后,接入流量运行小会节点会出现无法响应的情况,此时cpu占用非常高,而且切断流量后也无法恢复。担心是新上线逻辑问题,马上执行回滚操作(更新回之前的稳定版本),但是,回滚后重新启动接入流量也还是出现同样的问题。
问题分析
导出问题节点故障时的jstack日志,
jstack -l pid
jvm线程cpu情况分析
top -Hp pid
发现大量请求卡在需要分配内存的代码片段,于是分析gc情况,发现一直在持续的进行full GC,没法提供服务。
结合jstack日志分析代码逻辑,发现大量线程都在初始化一份接口映射需要的基础数据。从代码逻辑分析,首次请求时,会先判断这份数据是否存在,不存在则会执行初始化。编写这段代码的开发人员意图应该是想在使用到的时候再加载,相当于对数据进行懒加载处理。但是,没有考虑到如果大量的请求同时进入到这个逻辑,将会并发初始化这份数据,导致cpu使用暴涨,内存开销巨大,从而引起上面的故障现象,jvm持续full GC无法恢复。
分析清楚原因后,问题处理就比较容易了,有两种方案:
1、将初始化数据的逻辑放入启动的阶段进行;
2、对初始化数据的逻辑做线程锁控制,大量线程请求,只会有一个线程获取锁去执行数据初始化的逻辑,其他线程阻塞等待初始化完成。
为了尽快恢复服务,我们采用第二种方案先紧急修复。修复后,重新更新测试,问题解决。
总结
平时开发中,很多时候也会采用数据懒加载的方式,在需要使用的时候再加载,避免多余的初始化浪费。但必须注意线程的控制,避免并发执行这些初始化操作。
同时,上面的问题现象还有个疑问点:为什么以前更新服务未出现问题(有问题的代码已经运行很久)。猜想应该和最近新上线大量用户有关,以前偶尔也会出现,由于没重视而忽略了。