乐趣区

关于后端:JIT逆优化导致ES集群CPU异常的问题分析

背景

在一次全链路压测过程中,逆风车匹配 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);

在优化实现后在雷同压力下进行了压测,发现 CPU 异样问题失去了解决。

(本文作者:郑崇祥)

退出移动版