关于618大促压测时,自研中间件暴露出问题的那点破事儿.

why技术

共 16636字,需浏览 34分钟

 ·

2023-06-15 12:26

你好呀,我是歪歪。

这个周末又是一年一度的 618 了,价格确实是比较合理的,所以又心动的产品,又在自己的能力范围之内的,该下手时就下手了。

我前两天在网上看到一篇京东的程序员写的关于 618 大促之前的全链路压测时自研中间件暴露出的问题总结文章。

文章是 2020 年的文章了,在日新月异的技术圈属于“老文章”了。但是文章中提到的自研中间件,是京东的 hotkey,这个框架还是很不错的,有很多值得学习、研究的地方。

分享给大家,希望能对你有所帮助。

原文链接:https://blog.csdn.net/tianyaleixiaowu/article/details/106092060
作者:天涯泪小武

前天618大促演练进行了全链路压测,在此之前刚好我的热key探测框架也已经上线灰度一周了,小范围上线了几千台服务器,每秒大概接收几千个key探测,每天大概几亿左右,因为量很小,所以框架表现稳定。

借着这次压测,刚好可以检验一下热key框架在大流量时的表现。毕竟作为一个新的中间件,里面很多东西还是第一次用,免不得会出一些问题。

压测期,我没有去扩容热key的worker集群,还是平时用的3个16C+1个4C8G的组合,3个16核是是主力,4核的是看上限能到什么样。

由于之前那一周的平稳表现,导致我有点大意了,没再去好好检查代码。导致实际压测期间表现有点惨淡。

框架的架构如下:

ec511f8a91ca21393bde5532ac3cf04d.webp

https://gitee.com/jd-platform-opensource/hotkey

大概0点多压测开始,初始量比较小,从10w/s开始压,当然都是压的APP的后台,我的框架只是被动的接收后台发来的热key探测请求而已。我主要检测的就是worker集群,也就是那4台机器的情况。

从压测开始的一瞬间,那台4核8G的机器就CPU100%,16核的CPU在90%以上,4核的100%即便在压测暂停的间隙也没有恢复,一直都是100%,无论是10w/s,还是后期到大几十万/s。

16核的在20w/s以上时也开始CPU100%,整体卡到不行了已经,连10秒一次的定时任务都卡的不走了,导致定时注册自己到etcd的任务都停了,再导致etcd里把自己注册信息过期删除,大量和Client断连。

然后Dashboard控制台监听etcd热key信息的监听器也出了大问题,热key产生非常密集,导致Dashboard将热key入库卡顿,甚至于入库时,都已经过期1分钟多了,导致插入数据库的时间全部是错的。

虽然Worker问题蛮多,也蛮严重,但好在etcd集群稳如老狗,除了1分钟一次的热key密集过期导致CPU有个小尖峰,别的都非常稳定,接收、推送都很稳,Client端表现也可以,没有什么异常表现。

其中etcd真的很不错,比想象中的更好,有图为证:

9fe75eaa1943942bf3b9b76e7ab6cfae.webp

Worker呢就是这样子

9555cb7cbc8d85b623ec8d64f607f6c4.webp

后来经过一系列操作,我还乐观的修改上线了一版,然后没什么用,在100%上稳的一匹。

后来经过我一天的研究分析,发现当时没找到关键点,改的效果不明显。当然后来我自我感觉找到问题点了,又修改了一些,有待下次压测检验。

这一篇就是针对各个发现的问题进行总结,包括压测期间的和之前灰度期间发现的一些。总的来说,无论书上写的、博客写的,各路这个说的那个说的虽然在本地跑的时候各种正常,但真正在大流量面前,未必能对。还有一些知名框架,参数配不好,效果未必达到预期。

平时发现的问题列表

先说压测前小流量时的问题。

1、在Worker端,会密集收到Client发来的请求。其中有代码逻辑为先后取系统时间戳,居然有后取的时间戳小于前面的时间戳的情况(罕见、不能复现),猜测为Docker时间对齐问题。造成时间戳相减为负值,代码数组越界,CPU瞬间达到100%。注意,这可是单线程的!

解决:问题虽然很奇葩,但很好解决,为负时,按0处理。

2、使用网上找的Netty自定义协议,在本地测试以及线上灰度测试时,表现稳健。但在全量上线后,2千台client情况下,出现过单Worker关闭一段时间并重启后,瞬间收到高达数GB的流量包,直接打爆内存,导致Worker停机,后续无法启动的情况。

解决:书上及网上均未找到相关解决方案,类似场景别人极难遇到。后通过使用Netty官方自带协议StringDecoder加分隔符后,未复现突传大包的情况,目前线上表现稳定。

3、Netty client是可以反复连接同一个server的,造成单个Client对单个Server产生多个长连接的情况,使得Server的TCP连接数远远大于Client的总数量。此前书上、网络教程等各个地方均未提及该情况。使得误认为,Client对Server仅会保持一个长连接。

解决:对Client的连接进行排重、加锁,避免Client反复连接同一个server。

4、Netty server在推送信息到大量client时,会造成CPU瞬间飙升至60-100%,推送完毕后CPU下降至正常值.

解决:在推送时,避免做循环体内Json序列化操作,应在循环体外进行.

5、在复用Netty创建出来的ByteBuf对象时,反复的使用它,会出现大量的报错。原因是对ByteBuf对象了解不深,该对象和普通的Java对象不一样,Java对象是可以传递下去反复使用的,不存在使用后销毁的情况,而ByteBuf在被Netty传出去后,就销毁了,里面携带的字节组就没了。

解决:每次都创建新的ByteBuf对象,不要复用它。

6、2千台Client在监听到Worker发生变化后,会同时瞬间去连接它,和平时上线时,每次几百台缓慢连接Server的场景不同,突发瞬间数千连接时,可能发生Server丢失一部分连接,导致部分Client连接失败。

解决:不再采用监听的方式,而采用定时轮训的方式,错开连接的时机,对连不上的Worker进行本地保存,后加一个定时任务,定时去连接那些没连上的Server。

7、Worker机器占用的内存持续增长,超过给Docker分配的内存后,被系统杀死进程。

解决:Worker全部是部署在Docker里的,刚开始我是没有给它配JVM参数的,譬如那个4核8G的,我只是将它部署上去,就没有管它了。随后,它的内存在持续稳定上涨,从未下降。直到内存爆满。后来经进入到容器内部,执行查看内存命令,发现虽然Docker是4核8G的,但是宿主机是250G的。JVM采用默认的内存分配策略,初始分配1/64的内存,最大分配1/4的内存。但是是按250G进行分配的,导致JVM不断扩容再扩容,直到1/4 * 250G,在到达Docker分配的8G时就被杀死了。后来给容器配置了JVM参数后,内存平稳。这块带来的经验教训就是,一定要给自己的程序配JVM,不然JVM按默认的执行,后果就不可控了。

压测发现的问题列表

前面发现的多是代码逻辑和配置问题,压测期间主要是CPU100%的问题,也列一下。

1 、Netty线程数巨多、disruptor线程数也巨多,导致CPU100%

问题描述:Worker部署的JDK版本是1.8.20,注意,这个版本是在1.8里算比较老的版本。

Worker里面作为Netty Server启动,我是没有给它配线程池的(如图,之前Boss和Worker我都没有指定线程数量),所以它走的就是默认Runtime.getRuntime().availableProcessors()* 2。

这个是系统获取核数的代码,在JDK1.8.31之前,Docker容器内的这段代码获取到的是宿主机的核数,而非给容器分配的核数!!!

譬如我的程序取到的就是32核,而非分配的4核。再乘以2后,变成了64个线程。导致Netty boss和worker线程数高达64,另外我还用了Disruptor,Disruptor的Consumer数量也是64!

导致压测一开始,瞬间CPU切换及其繁忙,大量的空转。大家都知道,CPU密集型的应用,线程数最好比较小,等于核数是比较合适的,而我的程序线程数高达180,CPU全部用于轮转了。

之后我增加了判断JDK版本的逻辑,JDK1.8.31后的获取到的AvailableProcessors就是对的了,并且我限制了BossGroup的线程数为1.再次上线后,CPU明显有下降!

带来的经验教训是,用Docker时,需要注意JDK版本,尤其是有获取系统核数的代码作为逻辑时。CPU密集型的,切勿搞很多线程。

4dec7b750f75187a735a81c788c8e880.webp

2、CPU持续100%,导致定时任务都不执行了

和第一个问题是连锁的,因为worker接收到的请求非常密集,每秒达10万以上,而CPU已经全部用于N个线程的轮转了,真正工作的都没了,我的一个很轻的定时任务5s上传一次Worker自己的IP信息到配置中心etcd,连这个定时任务都工作不ok了,通过Jstack查看,一直处于Wait状态。

之后导致etcd里该Worker信息过期被删除,再导致2千多个Client从etcd没取到该Worker注册信息,就把它给删掉了,发生了大量client没有和Worker进行连接。

可见,CPU满时,什么都不靠谱了,核心功能都会阻塞。

3、Caffeine密集扩容,耗费CPU大

因为Worker里是用Caffeine来存储各Client发来的Key信息的,之后读取Caffeine进行存取。

Caffeine底层是用ConcurrentHashMap来进行的数据存储,大家都知道HashMap扩容的事,扩容2倍,就要进行一次Copy,里面动辄几十万个Key,扩容Resize时,CPU会占用比较大。尤其是CPU本身负荷很重时,这一步也会卡住。

我的Worker给Caffeine分配的最大500万容量,虽然不是很大,但卡顿时,Resize这一步执行很慢。不过这个不是什么大问题,也没有什么好修复的,就保持这样就行。

4、Caffeine在密集失效时,老版本JDK下,Caffeine默认的ForkJoinPool有BUG

Caffeine我是设置的写入后一分钟过期,因为是密集写入,自然也会密集失效。Caffeine采用线程池进行过期删除,不指定线程池时采用默认的ForkJoinPool。问题是什么呢,大家自己也能试出来。搞个死循环往Caffeine里写值,然后看它的失效。在JDK1.8.20之前,这个ForkJoinPool存在不提交任务的BUG,导致Key过期后未被删除。进而Caffeine容量爆满超过阈值,发生内存溢出。架构师针对该问题给Caffeine官方提了Issue,对方回复,请勿过于密集写入Caffeine,写入过快时,删除跟不上。还需要升级JDK,至少要超过1.8.20.不然ForkJoinPool也有问题。

5、Disruptor消费慢

大名鼎鼎的Disruptor实际表现并不如名气那么好,很多文章都是在讲Disruptor怎么怎么牛x,一秒几百万。

在Worker里的用法是这样的,Netty的Worker线程池接收到请求后,统一全部发到Disruptor里,然后我搞CPU核数个线程来消费这些请求,做计算热Key数量的操作。

而压测期间,CPU100%时,几乎所有的线程都卡在了Disruptor生产上。即N个线程在这个生产者获取Next序列号时卡住,原因很简单,就是没消费完,生产者阻塞。

我设置的Disruptor的队列长度为100万,实际应该写不满这个队列,但不知道为什么还是大量卡在了这个地方。该问题有待下次压测时检验。

92eb5cdea0ed2981096e8f60c239a77c.webp

6、有个定时任务里面有耗大量CPU的方法

之前为了统计Caffeine的容量和占用的内存,我搞了个定时任务10秒一次上传Caffeine的内存占用。就是被注释掉的那行,上线后坑到我了,那一句特别耗CPU。赶紧删掉,避免这种测试性质的代码误上线,占用大量资源。

0310f65870ff1a041683883406f08b17.webp

7、数据库写入速度跟不上热Key产生的速度

我是有个地方在监听etcd里热Key的,每当有新Key产生时,就会往数据库里插值。结果由于Key瞬间来了好几千个,数据库处理不过来,导致大量的阻塞,等轮到这条Key信息插入时,早就已经过期了,造成数据库里的数据全是错的。

这个问题比较好解决,可以做批量入库,加个队列缓冲就好了。

初步总结

其实里面有很多本地永远无法出现的问题,譬如时间戳的那个,还有一些问题是JDK版本的,还有是Docker的。但最终都可以归纳为,代码不够严谨,没有充分考虑到这些可能会带来问题的地方,譬如不配JVM参数。

但是不上线又怎么都测试不出来这些问题,或者上线了量级不够时也发现不了。这就说明一个稳定健壮的中间件,是需要打磨的,不是说书上抄了一段Netty的代码,上线了它就能正常运行了。

当然进步的过程其实就是踩坑的过程,有了相应的场景,实实在在的并发量,踩过足够的坑,才能打磨好一个框架。

也希望有相关应用场景的同学,关注京东热Key探测框架。

好了,本文的技术部分就到这里啦。

下面这个环节叫做[荒腔走板],技术文章后面我偶尔会记录、分享点生活相关的事情,和技术毫无关系。我知道看起来很突兀,但是我喜欢,因为这是一个普通博主的生活气息。

荒腔走板

0408a5ddf6d9467c7971d6bc0f10525c.webp

这是上周五下班回家的路上拍的一张照片。

我当时非常开心,因为最近工作上的事情比较多,白天时间比较分散,晚上才有整块的时间集中做事情,所以我晚上走的比较晚,已经有近一个月没有在天黑之前下过班了。

上周五算是阶段性的完成一个小任务,在天黑之前走人回家。

下班的时候,天还亮着,是大多数互联网程序员的奢侈品。

所以当我路过这个路口的时候,看到晚霞这么灿烂的时候,我非常的开心,感觉这就是一份礼物。

想着一会路过小区楼下的水果店的时候再买一个西瓜回去放在冰箱里面,晚上看电影的时候时候吃。

这感觉,

他妈的,

太爽了,

我艹。

· · · · · · · · · · · · · ·     E N D     · · · · · · · · · · · · · ·

8e36954404cf77db2bf4761e65423361.webp

推荐👍 68行代码实现Bean的异步初始化,粘过去就能用。

👍 @ E v e n t L i s t e n e r

👍

推荐👍 这是一行牛逼的源码。

👍

线 t i t l e

浏览 67
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报