返回

APM引起的血案:一次Elastic APM导致的线上性能问题

后端

这个问题发现的起因很简单:

我们对线上所有应用进行定期压测,进行持续交付部署后,线上出现了延迟增大1-2s的现象。

第一反应是怀疑我们的代码,但是检查日志和应用自身监控发现并没有任何异常。我们运维的同学也进行了排查,发现并没有明显的资源争抢。

我们运维的同学想起了我们埋在各个应用里的Elastic APM监控,基于Elastic APM我们可以追踪各个接口的性能,所以我们想看看能不能从APM里看出什么问题。

使用Elastic APM我们可以看到两个比较重要的指标,一个是请求耗时,一个是CPU耗时。根据这两个指标,我们可以粗略判断出瓶颈在哪个地方。

在对比之前和之后的APM指标时,我们发现了一个明显的变化:

请求耗时基本没变化,但是CPU耗时增长了十几倍

这意味着请求并没有真正的变慢,但是CPU占用率大幅度上升,很明显,这是JVM的JIT编译触发的问题。

既然是JIT编译的问题,那么大概率是我们的代码写法有问题,导致JIT编译失败,引发了大量的Full GC,进而导致了应用性能下降。

我们找到了压测代码,进行了对比,发现了一个很明显的区别:

之前的代码是这么写的:

for (int i = 0; i < 100000; i++) {
  handle(i);
}

新的代码是这么写的:

int[] arr = new int[100000];
for (int i = 0; i < 100000; i++) {
  arr[i] = i;
  handle(arr[i]);
}

很明显,这是一个数组分配的问题,新代码分配了100000个元素的数组,而老代码没有。

我们知道,数组分配会触发GC,而GC会带来STW(Stop-the-World),导致应用暂停。

所以,我们猜测是数组分配触发的GC导致了JIT编译失败,引发了Full GC,进而导致了应用性能下降

为了验证这个猜测,我们做了如下的实验:

  1. 在压测代码中增加数组分配,但是不进行循环操作
  2. 在压测代码中进行循环操作,但是不进行数组分配

结果发现,只有同时进行数组分配和循环操作时,才会触发性能下降。

最终,我们通过将数组分配移出循环,解决了这个问题

总结一下,这次线上性能问题的原因是:

  1. 数组分配触发了GC
  2. GC导致了JIT编译失败
  3. JIT编译失败引发了Full GC
  4. Full GC导致了应用性能下降

解决方法是:

将数组分配移出循环

这个案例告诉我们,在写代码的时候,一定要注意数组分配的时机,避免在循环中进行数组分配,以免触发GC,导致JIT编译失败,引发Full GC,进而导致应用性能下降。