背景

图片

在一次全链路压测过程中,顺风车匹配ES集群出现了个别节点CPU几乎被打满的情况。第二轮压测,我们关闭了最近上线的H3召回匹配升级AB实验,在同样压力下集群cpu运行平稳,保持在35%左右,开启AB实验后之前异常节点cpu又急速增加,初步定位到节点异常应该和H3召回升级实验相关。

问题复现

由于压测是在凌晨进行,当时并没有对异常节点的堆栈信息进行详细的拉取和分析。所以要找到问题的原因我们首先需要复现问题,刚好上半年我们完成线上es双集群的项目,在业务低峰期可以把线上流量切换到其中一个es集群中,另外一个集群和预发环境的应用进行连接,通过在预发环境发起请求进行模拟压测,很快便复现了问题,es集群cpu从10%急速增长到100%,详见下图。

图片

在整个过程中网络、磁盘、内存相关指标均未出现大的波动,主要消耗是在system load(90%)。

图片

然后,使用Arthas工具生成异常节点的火焰图和热点线程占用,进行分析。

图片

通过以上火焰图数据分析,我们发现在程序的主要CPU浪费在Deoptimization::uncommon_trap里。

然后开始从Deoptimization::uncommon_trap 入手查阅一些相关的资料,发现这个原来是jit的一种逆优化策略,我们先来看下什么是jit和逆优化。

背景知识:JIT优化和逆优化

为了提高热点代码(Hot Spot Code)的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(JIT)。

图片

“热点代码”两类:
被多次调用的方法。
被多次执行的循环体 – 尽管编译动作是由循环体所触发的,但编译器依然会以整个方法(而不是单独的循环体)作为编译对象。

Deoptimization::uncommon_trap是一个在JIT编译器中用于处理不常见陷阱的机制。当JIT编译器对一个方法进行编译时,会根据当前的执行环境和代码的特性进行优化,生成相应的本地机器代码。但是,由于一些特殊情况或者代码变动,之前优化的代码可能不再适用。

当发生这种情况时,JIT编译器会触发Deoptimization::uncommon_trap机制。这个机制会将当前的执行状态标记为不常见,然后将控制流返回到解释器或者其他备用代码路径,以便重新执行相应的代码。在重新执行的过程中,JIT编译器会重新生成适用于新情况的本地机器代码。

Deoptimization::uncommon_trap机制的目的是为了保证编译后的代码的正确性和可靠性。当代码执行环境或者代码本身发生变化时,通过触发不常见陷阱,可以及时修复和重新优化代码,以保证程序的正确性和性能。

如:

static void test(Object input) {  
 if (input == null) {  
 return;  
 }  
 // do something  
}

如果input一直不为空,执行1W次时,那么上述代码将优化为:

static void test(Object input) {  
// do something  
}

但是如果之后出现input为空,那么将会触发Uncommon Trap,通过逆优化(Deoptimization)退回到解释状态继续执行。

如果程序一直在执行Deoptimization::uncommon_trap,可能有以下几个可能的原因:

  • 频繁的代码变动:如果程序中频繁地修改代码,特别是对于经过优化的热点代码,会导致JIT编译器反复触发不常见陷阱来重新优化代码。这可能是因为代码变动导致了之前的优化假设不再成立,需要重新优化代码。
  • 动态类型变化:如果程序中存在频繁的动态类型变化,例如方法的参数类型经常变化,JIT编译器可能会触发不常见陷阱来处理类型不匹配的情况。这种情况下,可以考虑使用类型稳定的代码模式或进行类型检查来减少不常见陷阱的发生。
  • 程序本身的特性:某些程序的特性可能会导致频繁的不常见陷阱,例如大量的动态代码生成、复杂的多态调用等。在这种情况下,可能需要重新设计程序结构或使用其他优化技术来减少不常见陷阱的发生。

定位问题&解决方案

了解了相关jit和逆优化的知识后,开始结合节点异常期间的热点线程进行具体代码的跟进分析。

图片

经过进一步分析,我们发现在对三个实验组进行了处理逻辑区分的时候使用switch的方式进行判断,java虚拟机对这块代码设别为热点代码,当方法被执行的次数+方法体内总循环的执行次数 > 阈值,会触发JIT编译成本地代码进行了优化。如果当时按照其中一个实验组进行编译的优化,当其他实验组开流量时这种优化策略便不成立,这时候就出现了逆优化(Deoptimization)。

解决方案:把原来使用switch方式进行实验组逻辑判断的代码改造成使用map函数式编程的方式。通过优化可以减少程序中的条件分支,避免逆优化问题的出现。

HashMap<String, Function<Map<String, ScriptDocValues<?>>, Boolean>> methodMap = new HashMap<>();  
methodMap.put("exp1", this::executeForExp1);  
methodMap.put("exp2", this::executeForExp2);  
methodMap.put("exp3", this::executeForExp3);  
executeFunction = methodMap.get(h3Version);

image.png

在优化完成后在相同压力下进行了压测,发现CPU异常问题得到了解决。

(本文作者:郑崇祥)

图片


哈啰技术
89 声望53 粉丝

哈啰官方技术号,不定期分享哈啰的相关技术产出。