十分困难经过了旺季,陆陆续续也经历了Redis数据节点出口带宽
、ES节点负载倾斜宕机
等毛病的产生,关于线上根底设施和组件的问题关注也越来越多。
这次发现的问题来自APM Agent
,咱们服务层级的监控是根据Elastic APM Java Agent
完成的,在其根底之上做了一些扩展。Elastic APM
从2018年开端release 1.0.0版本,至今也现已过去多年,原本认为现已相对稳定,可是还是翻车在SpanPool
用到的MpscAtomicArrayQueue
上。
问题现象
本年旺季后,用户量再次上涨,随即有事务反馈线上服务呈现99线尖刺,甚至导致服务呈现瞬间限流。
由于该服务为根底服务,被上层事务依靠,对上层的影响较大,直接导致上层事务降级。
问题剖析
由于是概率性偶发问题,无法稳定复现并获取现场,所以只能先测验猜想原因并验证。
猜想与验证
(一)是否存在瞬间流量导致负载短时升高?
不存在瞬时高流量,经过流量曲线图看到流量稳定。
(二)依靠中间件、存储是否存在慢查询/操作?
事务逻辑比较简略,依靠的组件只有Redis
和MySQL
,排查未发现慢指令/SQL。
(三)服务自身是否或许存在CPU密集型操作?
代码自身十分简略,只做数据写入读取,无计算逻辑。
(四)是否服务自身负载已到临界点?
查看服务集群负载,发现CPU运用率不超过35%,可是在服务呈现限流的时刻点,部分实例呈现瞬间高负载的状况。
同时,进入实例查看发现在高负载时刻点,DubboServerHandler
线程池耗尽,触发线程池回绝策略,主动dump了JVM中所有线程的仓库。
拿到仓库的我大腿一拍,认为本相就在眼前。
但实际上作用并不大,从仓库上看分为两类,一类是DubboServerHandler
,正在进行事务逻辑处理;另一类其他场景的线程池,根本闲暇处于等候。整体来看并没有什么有用的信息,仓库的记载的也是”过后“了。
经过这儿也能够根本确认瞬间的某个行为带来高负载,可是立刻又恢复了。 (心里其实也有个猜想,或许存在达观锁,在高并发场景下导致锁失利疯狂重试,终究也验证是这类问题。)
到这儿根本上线索都断了,线上环境也没有更多有用的信息。
为了找到背后的本相,决定测验在独立的环境中进行压测,测验复现问题现象。
压测复现
由于是瞬间高负载带来的问题,所以只需要模仿服务实例高负载。
果不其然,压测后相同呈现了”同款“问题。
虽然能复现,但由于是瞬间的高并发,经过ONCPU火焰图
或者Dubbo
线程池的ThreadDump
都无法识别某个瞬间的问题。
终究从成果来看,
ONCPU火焰图
原本应该是能发现问题的,可是或许由于ByteBuddy
做字节码改写后,注入的代码并没有在火焰图记载的仓库中。
所以我把思路调整了一下,既然99线有尖刺,那就先从99线高的接口调用链路开端查。
链路剖析
先从99线高的链路能够追寻,找到对应的慢调用,能够找到这样的一些链路信息:
接口逻辑十分简略,仅仅去DB查一下数据,可是接口耗时到达6s。
其间,耗时比较长的时刻在getConneciton
,莫非是衔接池不够用?
为什么有两个?和咱们自定义注入函数和
APM SPAN
逻辑有关,实际是对一次获取衔接操作记载了两次。
从上图衔接池的衔接运用及行列等候监控来看,衔接数还很足够。
那么为什么会getConnection
很久呢?
参与这个进程的耗时,除了获取衔接行为自身,还有一个简单被忽略的行为,APM Span
的收集,有没有或许是这个原因呢。
真的是APM Agent的问题吗?
想到这儿其实自己也不太确认,究竟APM Agent
这些年一向在用,也并没有发现什么问题。
抱着掐指算命试一试的情绪,暂时把APM Agent
关掉,做了一轮压测比照。
不试不知道,一试就有点蚌埠住了。
移除APM Agent:99线根本稳定在100ms内,Load相对平稳。
带着APM Agent:99线根本在500~750ms,而且Load呈现瞬间飙升的状况。
这就根本上能够确实是APM Agent
导致的问题了。
根因究竟是什么?
查到这儿还不够,还要继续剖析根本原因是什么。
为了查到根因,去翻了一下Elastic APM
和咱们自己APM扩展插件的代码,乍看没什么太大问题,可是既然有99线较高的状况,直接trace
一把耗时不久知道了。
首要我找到了APM
创立Span
的办法,经过Arthas
追寻该办法耗时,过滤出大于500ms的链路,终究追寻到了下面这个办法。
其间,spanPool
经过工厂类一致创立,是一个Span
目标池,每次需要创立Span
时,直接从池中获取,用完偿还,防止了目标的频频创立。
感觉离本相越来越近了,既然用到池化办法进行办理,那么很或许需要处理并发获取和偿还的问题,那么这池子是怎样的呢?
而该池子的参数经过ConcurrentQueueSpec
指定,是巨细限制为capacity
,producers
和consumers
设置为0则意味着该目标池有多个生产者和多个顾客。
而在AtomicQueueFactory
工厂类中,经过newQueue
创立了一个JCTools
的MpmcAtomicArrayQueue
。
经过上图能够看到,SpanPool
目标池终究是一个MpmcAtomicArrayQueue
,那么进一步验证猜想,看看目标获取和偿还的逻辑。
发现获取和偿还都是时,都是经过index
来判别运用位置,并经过CAS
来处理并发问题。
CAS
是一种达观的无锁化设计,适合在抵触并不高的场景下运用,而在高并发的场景下,不断抵触带来不停的自旋,很或许导致瞬间的高负载,有没有或许是这个问题呢?
进一步验证一把,trace
一下MpmcAtomicArrayQueue
中的办法调用,来判别该CAS
是否会带来问题。
能够看到,在极点状况下,该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
并发行列的组件不在少数,Netty
、RxJava
等均有运用。
根因验证
综上所述,根本能够确认MpmcAtomicArrayQueue
的CAS
会带来性能问题,可是怎样验证咱们发现的现象便是该问题导致的呢?
有办法~,由于呈现自旋比较严重的场景是在取Span
的poll()
办法上,那么能够暂时切换成MpscLinkedAtomicQueue
。
MpscLinkedAtomicQueue
是一个有界的多生产者、单顾客行列,所以在poll()
的时候并不会进行并发操控,经过添加JVM参数-Delastic.apm.max_queue_size=-1
能够进行切换。
能够看到换成MpscLinkedAtomicQueue
后,服务99线也恢复正常,证明该问题是由MpmcAtomicArrayQueue
的CAS
锁导致。
该办法仅可用于验证问题,实际上Consumer为多个,不能运用MPSC。
处理计划
从根因来看,是高并发场景下频频请求Span
,导致从目标池获取Span
时,CAS
不断抵触引发负载过高。
但这儿假如不运用无锁化设计,选用锁进行并发操控,会大大下降程序并发度,那有什么更好的办法呢?
细细想一想,有没有相相似的场景呢?
答案当然是有的,
(1)JVM为了处理所有线程在堆上分配空间的并发性能问题,运用了TLAB(Thread Local Allocation Buffer)
,每个线程独享一块空间,用于目标分配;
(2)在资源分配的场景中,为了处理资源频频请求的问题,会选用批量预取+缓存的方式,减少请求次数。
终究咱们选用了计划一的完成,每个线程都有一块有限巨细的ThreadLocal
独享空间,优先从其间获取Span
,防止了并发场景下的资源竞赛。
选用新计划后的作用如下:
其时也猜想这个计划或许会带来
Agent
内存运用上涨,由于每个线程具有一块独享Span
空间。从终究测试的成果上来看,咱们有设置ThreadLocal
目标池的巨细,内存运用上涨不大。
总结
Elastic APM
经过运用JCTools
的MpmcAtomicArrayQueue
进行Span
目标池的办理,本意是想经过无锁化的行列来防止锁堵塞及竞赛带来的额外开销,用CAS
来处理并发操控问题。
可是,这仍然没有从根本上处理高并发场景下的资源抢夺问题,终究咱们经过相似JVM中TLAB
的思维进行优化,处理了这个问题。
而Elastic APM
自身也有测验处理这个问题,大佬张师傅在其源码中发现了ObjectPool
的ThreadLocal
版,类名为ThreadLocalObjectPool
,在2018
年就现已存在,但从其注释上看,现在仅会用于压测模块中,并未在生产环境运用。