十分困难经过了旺季,陆陆续续也经历了Redis数据节点出口带宽ES节点负载倾斜宕机等毛病的产生,关于线上根底设施和组件的问题关注也越来越多。

这次发现的问题来自APM Agent,咱们服务层级的监控是根据Elastic APM Java Agent完成的,在其根底之上做了一些扩展。Elastic APM从2018年开端release 1.0.0版本,至今也现已过去多年,原本认为现已相对稳定,可是还是翻车在SpanPool用到的MpscAtomicArrayQueue上。

问题现象

本年旺季后,用户量再次上涨,随即有事务反馈线上服务呈现99线尖刺,甚至导致服务呈现瞬间限流。

一次Elastic APM导致的线上性能问题

一次Elastic APM导致的线上性能问题

由于该服务为根底服务,被上层事务依靠,对上层的影响较大,直接导致上层事务降级。

问题剖析

由于是概率性偶发问题,无法稳定复现并获取现场,所以只能先测验猜想原因并验证。

猜想与验证

(一)是否存在瞬间流量导致负载短时升高?

不存在瞬时高流量,经过流量曲线图看到流量稳定。

一次Elastic APM导致的线上性能问题

(二)依靠中间件、存储是否存在慢查询/操作?

事务逻辑比较简略,依靠的组件只有RedisMySQL,排查未发现慢指令/SQL。

(三)服务自身是否或许存在CPU密集型操作?

代码自身十分简略,只做数据写入读取,无计算逻辑。

(四)是否服务自身负载已到临界点?

查看服务集群负载,发现CPU运用率不超过35%,可是在服务呈现限流的时刻点,部分实例呈现瞬间高负载的状况。

一次Elastic APM导致的线上性能问题

同时,进入实例查看发现在高负载时刻点,DubboServerHandler线程池耗尽,触发线程池回绝策略,主动dump了JVM中所有线程的仓库

拿到仓库的我大腿一拍,认为本相就在眼前。

一次Elastic APM导致的线上性能问题

但实际上作用并不大,从仓库上看分为两类,一类是DubboServerHandler,正在进行事务逻辑处理;另一类其他场景的线程池,根本闲暇处于等候。整体来看并没有什么有用的信息,仓库的记载的也是”过后“了。

经过这儿也能够根本确认瞬间的某个行为带来高负载,可是立刻又恢复了。 (心里其实也有个猜想,或许存在达观锁,在高并发场景下导致锁失利疯狂重试,终究也验证是这类问题。)

到这儿根本上线索都断了,线上环境也没有更多有用的信息。

为了找到背后的本相,决定测验在独立的环境中进行压测,测验复现问题现象。

压测复现

由于是瞬间高负载带来的问题,所以只需要模仿服务实例高负载。

果不其然,压测后相同呈现了”同款“问题。

一次Elastic APM导致的线上性能问题

一次Elastic APM导致的线上性能问题

虽然能复现,但由于是瞬间的高并发,经过ONCPU火焰图或者Dubbo线程池的ThreadDump都无法识别某个瞬间的问题。

终究从成果来看,ONCPU火焰图原本应该是能发现问题的,可是或许由于ByteBuddy字节码改写后,注入的代码并没有在火焰图记载的仓库中。

所以我把思路调整了一下,既然99线有尖刺,那就先从99线高的接口调用链路开端查。

链路剖析

先从99线高的链路能够追寻,找到对应的慢调用,能够找到这样的一些链路信息:

一次Elastic APM导致的线上性能问题

接口逻辑十分简略,仅仅去DB查一下数据,可是接口耗时到达6s。

其间,耗时比较长的时刻在getConneciton,莫非是衔接池不够用?

为什么有两个?和咱们自定义注入函数和APM SPAN逻辑有关,实际是对一次获取衔接操作记载了两次。

一次Elastic APM导致的线上性能问题

从上图衔接池的衔接运用及行列等候监控来看,衔接数还很足够。

那么为什么会getConnection很久呢?

参与这个进程的耗时,除了获取衔接行为自身,还有一个简单被忽略的行为,APM Span的收集,有没有或许是这个原因呢。

真的是APM Agent的问题吗?

想到这儿其实自己也不太确认,究竟APM Agent这些年一向在用,也并没有发现什么问题。

抱着掐指算命试一试的情绪,暂时把APM Agent关掉,做了一轮压测比照。

一次Elastic APM导致的线上性能问题

一次Elastic APM导致的线上性能问题

不试不知道,一试就有点蚌埠住了。

移除APM Agent:99线根本稳定在100ms内,Load相对平稳。
带着APM Agent:99线根本在500~750ms,而且Load呈现瞬间飙升的状况。

一次Elastic APM导致的线上性能问题

这就根本上能够确实是APM Agent导致的问题了。

根因究竟是什么?

查到这儿还不够,还要继续剖析根本原因是什么。

为了查到根因,去翻了一下Elastic APM和咱们自己APM扩展插件的代码,乍看没什么太大问题,可是既然有99线较高的状况,直接trace一把耗时不久知道了。

首要我找到了APM创立Span的办法,经过Arthas追寻该办法耗时,过滤出大于500ms的链路,终究追寻到了下面这个办法。

一次Elastic APM导致的线上性能问题

一次Elastic APM导致的线上性能问题

其间,spanPool经过工厂类一致创立,是一个Span目标池,每次需要创立Span时,直接从池中获取,用完偿还,防止了目标的频频创立。

感觉离本相越来越近了,既然用到池化办法进行办理,那么很或许需要处理并发获取和偿还的问题,那么这池子是怎样的呢?

一次Elastic APM导致的线上性能问题

一次Elastic APM导致的线上性能问题

而该池子的参数经过ConcurrentQueueSpec指定,是巨细限制为capacityproducersconsumers设置为0则意味着该目标池有多个生产者和多个顾客。

而在AtomicQueueFactory工厂类中,经过newQueue创立了一个JCToolsMpmcAtomicArrayQueue

一次Elastic APM导致的线上性能问题

经过上图能够看到,SpanPool目标池终究是一个MpmcAtomicArrayQueue,那么进一步验证猜想,看看目标获取和偿还的逻辑。

一次Elastic APM导致的线上性能问题

发现获取和偿还都是时,都是经过index来判别运用位置,并经过CAS来处理并发问题。

CAS是一种达观的无锁化设计,适合在抵触并不高的场景下运用,而在高并发的场景下,不断抵触带来不停的自旋,很或许导致瞬间的高负载,有没有或许是这个问题呢?

进一步验证一把,trace一下MpmcAtomicArrayQueue中的办法调用,来判别该CAS是否会带来问题。

一次Elastic APM导致的线上性能问题

能够看到,在极点状况下,该CAS自旋300次+,耗时到达200ms以上。

由此能够判别,在高并发场景里多线程并发创立Span时,需要从MpmcAtomicArrayQueue中获取Span目标,而获取进程中的CAS自旋带来了高负载,导致耗时添加,99线呈现尖刺。

看到这儿你或许会吐槽Elastic APM真辣鸡,怎样挑选了MpmcAtomicArrayQueue这种行列,还用了CAS来完成并发操控。

那么这个MpmcAtomicArrayQueue究竟是什么呢?

要了解MpmcAtomicArrayQueue,先来看看JCTools

JCTools

JCTools是为JVM提供的Java并发工具集,其目的是为补偿其时JDK中缺失的一系列非堵塞并发数据结构。

在传统的多线程场景中,都需要一个可变的同享状态变量作为锁,来确保数据的一致性,也确保数据的变更为外界所知。

可是这个办法存在一些问题:

  • 线程需要堵塞并等候获取锁,直到另一个线程完毕并开释锁,这会下降程序的并发度;
  • 重锁抵触和抢夺,会导致JVM需要花费更多的时刻处理线程调度、办理锁竞赛、行列化办理等候线程;
  • 或许的死锁;
  • 粗粒度的锁也会导致锁时刻提高,下降程序并发度;

面对这个问题,可替代的办法便是选用非堵塞的锁算法,CAS指令便是一种无锁化、非堵塞的lock-free算法。

在咱们的事例中,Elastic APM就运用了JCTools所包含的并发行列完成:

SPSC - Single Producer Single Consumer (Wait Free, bounded and unbounded)
MPSC - Multi Producer Single Consumer (Lock less, bounded and unbounded)
SPMC - Single Producer Multi Consumer (Lock less, bounded)
MPMC - Multi Producer Multi Consumer (Lock less, bounded)

而这些并发行列底层均是运用Index标记进行行列办理,经过CAS将进行并发操控。

而且运用JCTools并发行列的组件不在少数,NettyRxJava等均有运用。

根因验证

综上所述,根本能够确认MpmcAtomicArrayQueueCAS会带来性能问题,可是怎样验证咱们发现的现象便是该问题导致的呢?

有办法~,由于呈现自旋比较严重的场景是在取Spanpoll()办法上,那么能够暂时切换成MpscLinkedAtomicQueue

MpscLinkedAtomicQueue是一个有界的多生产者、单顾客行列,所以在poll()的时候并不会进行并发操控,经过添加JVM参数-Delastic.apm.max_queue_size=-1能够进行切换。

一次Elastic APM导致的线上性能问题

能够看到换成MpscLinkedAtomicQueue后,服务99线也恢复正常,证明该问题是由MpmcAtomicArrayQueueCAS锁导致。

该办法仅可用于验证问题,实际上Consumer为多个,不能运用MPSC。

处理计划

从根因来看,是高并发场景下频频请求Span,导致从目标池获取Span时,CAS不断抵触引发负载过高。

但这儿假如不运用无锁化设计,选用锁进行并发操控,会大大下降程序并发度,那有什么更好的办法呢?

细细想一想,有没有相相似的场景呢?

答案当然是有的,

(1)JVM为了处理所有线程在堆上分配空间的并发性能问题,运用了TLAB(Thread Local Allocation Buffer),每个线程独享一块空间,用于目标分配;

(2)在资源分配的场景中,为了处理资源频频请求的问题,会选用批量预取+缓存的方式,减少请求次数。

终究咱们选用了计划一的完成,每个线程都有一块有限巨细的ThreadLocal独享空间,优先从其间获取Span,防止了并发场景下的资源竞赛。

选用新计划后的作用如下:

一次Elastic APM导致的线上性能问题

其时也猜想这个计划或许会带来Agent内存运用上涨,由于每个线程具有一块独享Span空间。从终究测试的成果上来看,咱们有设置ThreadLocal目标池的巨细,内存运用上涨不大。

总结

Elastic APM经过运用JCToolsMpmcAtomicArrayQueue进行Span目标池的办理,本意是想经过无锁化的行列来防止锁堵塞及竞赛带来的额外开销,用CAS来处理并发操控问题。

可是,这仍然没有从根本上处理高并发场景下的资源抢夺问题,终究咱们经过相似JVM中TLAB的思维进行优化,处理了这个问题。

Elastic APM自身也有测验处理这个问题,大佬张师傅在其源码中发现了ObjectPoolThreadLocal版,类名为ThreadLocalObjectPool,在2018年就现已存在,但从其注释上看,现在仅会用于压测模块中,并未在生产环境运用。

一次Elastic APM导致的线上性能问题