文章目录

前言

长连接放到K8S之后,做性能测试就发现连接量上不去,在7~8W连接左右的时候,ELB的“波动队列”就开始暴增至1024,然后开始丢包。我一直有其他的工作要处理,这个排查又比较费时间,所以一直把这个事情搁置了。在迭代新版本的时候,发现连接量降至单个容器6W左右,反向优化最为致命…

我的同事开始排查这个问题,最终找到是ELB的问题,记录一下他的思考过程,积累经验。

一、问题描述

在AWS的ELB(CLB)中,有一项监控指标叫做波动队列(Surge Queue),长度最大为1024。波动队列的作用是,当后端服务器没有响应的时候,它会将请求包缓存起来,等服务器有响应了之后再把这些包慢慢消化掉,如果流量过大超过1024,则直接将包丢弃掉,避免在服务器处理能力有问题的时候遇到突增流量导致崩溃。

在性能测试(PET)过程中,测试同事发现总是到7~8W左右连接量开始出现报错。我们排查发现,ELB的波动队列在出错的时刻上涨到1024,然后将包丢弃了。所以需要排查的就是为什么波动队列会上涨。

二、排查过程

2.1 观察现象

最开始怀疑是我们此次为了解决CVE漏洞,升级了Netty的版本,才导致7~8W降至6W。于是请测试部同事对比测了很多次,发现每次旧版本固定在65.5K,新版本固定在62.8K,就一定会出现错误。这里只差了3000左右,其实还好,但是很多次都固定在这个数值,这个现象不太正常,一定是有参数限制在了这个范围。另外,这两个值很像是65535这个经典的数值。65535让人可以想到一些常见的限制,比如TCP客户端的端口数量65535。

2.2 排查GC问题

首先我们要解决波动队列问题而不是65535连接量,波动队列一定是因为后端服务器没有响应导致,有两点可以怀疑,一个是服务器性能有瓶颈,一个是FullGC的stopworld。但仔细观察了prometheus的监控数据,CPU、内存、磁盘、网络I/O、网络带宽、堆内存、GC情况、请求队列排队数量等等都很正常,看起来完全没有内存泄漏、或者长时间GC的情况。

(1)减缓TPS,仍然出现波动队列问题

资源足够的情况下,我们将TPS降低为20,仍然出现波动队列问题,所以不是服务器性能瓶颈。

(2)抓到长时间GC现象

在确认资源的确都OK之后,我们把重点放在GC上,仍然怀疑GC有问题。还好我们现在上了Skywalking,于是用skywalking排查,发现了和prometheus不一样的结果:

首先来解释一下为什么Skywalking采集到了一个尖峰,而Prometheus抹平了。很有可能是prometheus间隔30秒才采样springboot的acuator数据,精度较低导致数据丢失。为了验证这个结论,同事直接登录pod,每秒打印一次GC情况:

jstat -gcutil {pid} 1000

发现的确是有长时间GC的情况出现,说明结论正确,skywalkiing采集的数据正确。接下来分析2.5秒的GC带来的影响。

FullGC有一次长达2.5秒的GC,此时连接量大约为300~400个,按400计算,光是心跳包400*2.5=1000,FullGC服务器没有响应,包的数量又差不多达到了ELB的波动队列最大长度,造成了波动队列丢包,这样的猜测非常合理,这也解释了为什么有的时候在测试前期就偶尔会出现波动队列满了的错误,而不是等到65K。

出现GC之后同时观察skywalking,发现连接节点请求依赖服务的一个接口长达2.8秒,且原因是自己在等待资源。

开始同事开始怀疑我写得有问题,比如线程池设置不合理等等,我检查了一下是合理的…后面考虑到GC的2.5秒,说明GC过长的确是对服务器产生了性能影响。

(3)找到长时间FullGC原因

我们再来观察图像,唯一比较异常的是GC的图像中,committed呈现锯齿状上涨和下降,而used却正常直线上升下降,或者也有跟随锯齿状上升下降。我怀疑GC是不是不是我们之前一直以为的默认G1。同时检查了一下GC的信息:

jinfo {pid} | grep GC

发现打印出来竟然是:

-XX:+UseParallelGC

说明GC真的没有用G1!我之前一直有个错误概念,就是jdk8里面的某个版本开始,G1被用作默认垃圾收集器。实际上,jdk9才被用作默认垃圾收集器,jkd8一直都是ParallelGC。这里再回顾一下GC:

JVM参数 新生代(别名) 老年代 
 -XX:+UseSerialGC

Serial (DefNew)

 Serial Old(PSOldGen)  
 -XX:+UseParallelGC

Parallel Scavenge (PSYoungGen)

 Serial Old(PSOldGen)
 -XX:+UseParallelOldGC

Parallel Scavenge (PSYoungGen)

 Parallel Old (ParOldGen)
 -XX:-UseParNewGC

ParNew (ParNew)

 Serial Old(PSOldGen)
 -XX:+UseConcMarkSweepGC ParNew (ParNew) CMS+Serial Old(PSOldGen)
 -XX:+UseG1GC G1 G1

我之前一直误以为用的是G1,因为连接节点的最低要求就是不能发生长时间停顿,否则会导致大量连接掉线。因此G1虽然收集得慢,但是停顿少吞吐量高,且我们也没有什么大对象要分配,因此非常适合。只是最开始的默认垃圾收集器的概念记错了,这是很严重的问题。

更换G1收集器之后:

-XX:+UseG1GC -XX:MaxGCPauseMillis=200

测试期间没有再发生过长时间GC,注意G1之后垃圾收集器的概念有所变化,监控的指标也需要相应更新。

(4)其他问题

我们把TPS调高到150,发现依赖的服务出现了CPU瓶颈,将其资源提升之后,TPS即使调到200,GC的情况也非常好。

2.3 排查连接量问题

虽然GC问题解决了,但是连接量还是上不去,在65.5K左右必然出现波动队列上涨(且不是GC引起)。

(1)排查Netty版本问题

前面提到,Netty只影响了3000(且最后发现并不是Netty版本导致的,而是ELB导致的)。

(2)排查业务更新了组件导致

在Netty版本相同的情况下,回滚到旧版本进行对比测试,表现一致,说明不是业务的问题。

(3)排查ELB问题

另一位同事开始怀疑是ELB的问题,于是对比测试了去掉ELB和有ELB的情况,发现去掉ELB之后,连接量至少可以达到150K,情况瞬间明朗了。开始研究为什么会出现这样的现象,同时给AWS提工单。经过交流,发现ELB(CLB)底层是EC2机器实现,只预热了2台机器,每台机器长连接限制是32768,所以最大连接量只有65536左右,和前面测试结果一致。

单一一组CLB Node 与Signle Taget的配对,最高仅支持32k的连线容量,这个限制同样也适用于TCP的监听端口。在正常的情境下,CLB记录到一段时间内连接数压力较高时,会自动扩展。

问题就在于这个自动扩展比较慢,需要“一段时间内连接数”的指标检测,而压力测试是瞬间上去的,因此CLB来不及扩展直接波动队列上涨导致丢包。

三、解决方案

官方的解决方案是:

由于CLB/ALB本身会因为服务压力Auto Scaling,节点可能会随着时间或服务压力而发生替换,节点替换时可能会给长连接带来影响。如果您对长连接是预期永不断线,或者超长连接(例如:超过24小时),我们会建议您改用NLB。

这也解释了之前经常发生的稳定性问题。这个建议当然只能拒绝掉,因为我们用不起昂贵的NLB。

考虑的解决方案是,在预知流量即将迎来一个波峰之前,提前和云服务提供商交流,预热几台CLB的EC2,保证流量平稳过渡。这也算是比较无奈的解决方案。


转载请注明出处http://www.bewindoweb.com/303.html | 三颗豆子
分享许可方式知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议
重大发现:转载注明原文网址的同学刚买了彩票就中奖,刚写完代码就跑通,刚转身就遇到了真爱。
你可能还会喜欢
具体问题具体杠