背景
线上系统出现CPU利用率告警,告警阈值为65%。通过观察监控发现,单机CPU使用MAX已经达到了88%,并且相对于前几天,CPU利用率的日平均值同步上升了一倍。
排查过程
准备工作_保留现场
逐步回滚(直接使用上个稳定包,而不是重新打包编译),先保证业务不出问题。并保留一个节点不回滚,便于排查问题。
步骤1_唯一变量压测
思路:结合最近一次的迭代内容,将被修改过的代码进行唯一实验变量,分别部署到预发布环境,进行压测。目的是找出是哪部分被修改代码引起的问题。
现象:但是分别多三个改动进行压测后,被压容器的各项指标(CPU、JVM、网络IO)均保持一致
结论:引起问题的原因很有可能不在被修改代码中。
步骤2_回滚版本压测
思路:使用最近上线前代码部署预发布环境,进行压测。目的是确认原有代码是否已经存在问题。
现象:最近上线前代码压测,被压容器的各项指标依然与上述容器现象保持一致,同样存在CPU利用率飙高的问题。这明显与几天前上线前代码在线上时CPU利用率的监控结果存在差异。
结论:本次压测虽然我们自己的项目是用上线前代码,但是CPU指标不同,说明最终运行的程序还是存在差异,差异一定存在于依赖的JAR包中。
步骤3_使用上一个稳定包进行压测
思路:使用最近上线前的稳定包直接发布,避免重新打包编译带来的影响。
现象:压测后发现各项指标恢复正常。
结论:问题一定出在我们项目所依赖的JAR包中。
步骤4_找到前后两个包的依赖差别
思路:解压产物包,观察其引入JAR包的版本差异,找到有区别的依赖。
现象:找到若干有区别的依赖
结论:明确了排查方向
步骤5_找到前后两个包压测时的线程信息
思路:通过命令,在两个容器中,分别找到Jvm中CPU占用较高的线程,利用这些线程id,通过jstack {id}命令,输出这些线程的堆栈信息。在堆栈信息中,找到与上一步确认的依赖中的包名、类名相关的方法。
现象:发现某个新版本依赖中的方法,上线前包的CPU占用率为0.1%,上线后包的CPU占用率为0.6%,锁定该包。对比内存堆栈信息,发现该包的新版本中,某个方法内部增加了正则表达式Parttern.match,最终导致CPU使用率飙高。
原因分析_5why
1Q:为什么要增加这个正则代码?
1A:该组件为一个基于ZK的配置服务的客户端,增加正则,目标是在系统初始化构建ZK监听时,验证被监听路径是否符合规范。
2Q:为什么该组件研发认为增加正则没有风险?
2A:该正则只会在建立对某ZK路径的监听实例时,才会被应用,组件研发预期此动作只在系统启动时发生一次。
3Q:为什么我们的系统会反复调用到这个正则?
3A:我们的系统中对该组件的用法出现了错误,导致在代码中每次用到该ZK路径,都重新初始化了一次。
4Q:为什么反复初始化ZK服务端也没有感知到压力上升?
4A:ZK客户端组件在初始化时,是在该类的Builder类中进行得正则验证,验证成功后再build实例。但是build后的实例实际上在组件中是有localCache的。也就是说后续的Builder虽然被创建了,但是并没有真正的去build。
5Q:为什么之前反复Builder没有出现业务问题?
5A:首先由于4Q中的回答,反复创建Builder但是最终不会build,并且获取到的结果依然是ZK服务端配置的结果,所以没有对业务造成影响。
最终结论
是我们项目对组件理解程度较低导致的错误使用。我们的项目中针对该组件的监听类,只在启动时初始化一次,即可解决问题。
修复方案
短期方案:线上代码对该组件的依赖版本回退到没有正则验证的版本,保证线上CPU利用率恢复正常,消除风险。
长期方案:将我们所有项目中对该客户端组件的错误使用进行修复。并推广至兄弟部门进行全面排查。