Netty WebSocket Server 压测

背景

阿里小蜜机器人作为一款面向阿里经济体消费者的智能助理产品,以用户和机器人之间进行对话的形式作为智能服务的载体,在技术实现上则是通过http的请求响应模型来完成交互流程。但是使用http请求带来的痛点问题也不少,比如机器人无法主动触达到用户去推送一些个性化的内容,因为每次http请求都只能是由用户主动发起,而机器人总是被动的那一方; 再比如这种请求同步等待的机制也会给后端一些比较复杂的算法模型、服务带来时间上的挑战,限制了更多样化的能力输出。

基于上述问题,我们基于Netty4实现了一个Websocket服务端应用,用于监听用户的进线请求,建立 & 管理用户和后端服务之间的长连接通道,具体ws协议的优势就不在此过多介绍了。那么代码写好了,系统性能到底怎么样,能不能承担足够的连接数,大家心里都是没有底的,这也就是为什么有了这次压测以及踩坑和爬坑经历的原因了。


压测准备

本次压测只涉及跟连接数相关的内容,其他指标(如上下行消息处理吞吐量)优化不在本文中介绍。受压机配置: 4C-8G-60G alios7u2。

  • 在 上上传自定义压测脚本,20台施压机,每台机器50个线程,并且在每个线程里会跟后端一台受压机建立100个长连接。如果全部建联成功预计共有10W模拟用户连接,并且会随机模拟用户跟后端发送上行消息并预期下行回复。线程数以及每个线程创建多少连接都可以在压测启动脚本中修改,方便随时调整连接压力。
  • 应用压测改造,压测标传递,DB、Tair、MQ压测流量隔离。


压测目标

单机10W连接


压测过程


Round 1

先试试压测脚本功能是否正常吧,单机压个1k连接练练手。

undefined

没啥问题,符合预期,继续调高压力。


Round 2

继续施压,模拟建立1W连接,但这次发现总是不能正常到达预期,峰值连接数在8~9k左右徘徊.后端应用的日志没有看到任何错误,而施压机上的客户端日志则频繁提示websocket建联失败。

undefined

没办法,先执行下dmesg看看能不能发现什么蛛丝马迹好了,结果。。。

undefined

这很明显是TCP建连的时候连接队列溢出了,并且按日志来看该队列的最大阈值是128。为了证实这个结论,再次压测了一次,这次使用netstat -s来看一下压测过程中数据的变化。

netstat -s | egrep "listen|LISTEN" 

Copy

image.png

上面看到的 64614 times、64927 times等 ,表示队列溢出的次数,隔几秒执行下发现这个数值一直在增加,这样的话问题原因基本可以确定了。那128这个阈值是哪里设置的呢,等等。。这个数字怎么感觉有点熟悉啊,之前好像有在代码里设置过,一查果然是SO_BACKLOG这个参数值。

image.png

那么这个参数到底代表了什么含义呢?为了讲清楚它让我们先祭出TCP三次握手神图

image.png

图中有两个队列,syns queue(半连接队列)和accept queue(全连接队列)。当server端收到client发送的syn包,进入SYN_RCVD状态后后会把该请求放到半连接队列中。当server端收到client的ack包后进入establish状态,就会放到全连接队列中去。

accept queue

其队列大小通过/proc/sys/net/core/somaxconn指定,在使用listen函数时,内核会根据传入的backlog参数与系统参数somaxconn,取二者的较小值。Netty的*ChannelOption.SO_BACKLOG*这个参数就是用于底层方法int listen(int sockfd, int backlog)

那其实到这一步解决办法已经很明显了,只要同时调大应用里的SO_BACKLOG和虚拟机内核的/proc/sys/net/core/somaxconn值应该就可以了。

  • SO_BACKLOG参数很好修改,我这边直接代码里调大到4096
  • /proc/sys/net/core/somaxconn这个参数咨询了Pouch的同学,拿到了修改的具体命令,只需要加到应用端的启动脚本里就可以了。记住,不要直接在机器上执行,否则下次重启又会不生效了。  sudo mount -o remount,rw -t proc /proc/sys /proc/sys && echo 4096 | sudo tee /proc/sys/net/core/somaxconn 

对了,这些参数修改完以后如果想知道有没有实际生效,可以通过以下命令查看,其中的Send-Q这一列表示应用在7001端口上监听的全连接队列的最大值。调完了这些再重启应用进行压测,发现连接数已经可以稳稳的到达1W了,并且不再有刚才的TCP listen overflow的异常情况了。

image.png

undefined


Round 3

上面的一步折腾了好久终于搞定了,心想终于可以让我安安心心继续压测了吧。Ok,继续模拟建立4W连接,然后眼巴巴的盯着我的监控大盘,盯盘,盯。。。我勒个去,为啥只到了28231啊!!!

image.png

这肯定是个意外,一定的。。。继续多压几次,更吊诡的事情发生了,每次的压测结果都是只能到28231,这到底是个什么鬼限制啊,那一瞬间的感觉啊。。。不知道大家有看过三体没,三体人送到地球的智子彻底封锁了地球基础物理学研究,每次都会去干扰地球物理实验的真实结果,让一群物理学家彻底崩溃。所以会不会也是智子在搞我啊。。just kidding me?

image.png

言归正传,难道是受限于系统文件句柄数(fd) ?可是执行了下ulimit -n 发现进程可以打开的最大文件句柄数是655350,远远大于我要压测的目标连接数,原因排除。。。因为后端还是没有任何错误日志,只有压测机客户端报连接失败,于是又去tengine里查看了下连接的错误日志,结果居然发现有一堆错误

[crit] 3036#0: *517253 connect() to 127.0.0.1:7001 failed (99: Cannot assign requested address) while connecting to upstream


带着这个错误找到了相关同学,他很有经验的指出了这应该是端口占用满了导致的,并建议我绕过tengine直接在施压机上指定压测机的IP和7001端口来验证一下,结果还真的可以单机到4W了。那这就很奇怪了啊,tengine作为反向代理服务器不是挺高效的么,咋还能限制住了后端服务的连接数呢。

因为系统中有大量的处于 time_wait 状态的连接而首先耗尽了系统本地端口的案例。我的压测场景下虽然没有TIME_WAIT,但是有很多ESTABLISHED啊,是不是突然有了一种柳暗花明的感觉!

Google一下,你就知道 What is the cost of many TIME_WAIT/ESTABLISHED on the server side?

image.png

对于TCP协议来说,四元组(源IP,源端口,目标IP,目标端口)就可以唯一确定一条连接,而nginx在作为反向代理的情况下,它是会随机分配一个临时端口来与后端服务器建立连接,举个栗子请看下面的执行结果,7001是后端应用的监听端口,而52670就是随机分配的临时端口。在同台机器上,既然源IP、目标IP、目标端口(假设只有7001的情况)都是固定的,那也就是说唯一的变量只有临时分配的源端口了!

$netstat -an
tcp        0      0 127.0.0.1:52670         127.0.0.1:7001          ESTABLISHED

Copy

但是临时端口的选择范围其实系统是有限制的,在网上查了下可以通过以下命令来查询分配范围,我这边的执行结果是

cat /proc/sys/net/ipv4/ip_local_port_range

Copy

image.png

我屮艸芔茻,是不是巨想摔个鼠标庆祝下的,60999 - 32768 = 23821啊!!!原来这个鬼限制是这么计算出来的啊,不是智子操纵的啊,好失望!!!

找到原因就好办了,既然现在的变量只有源端口,那就再加个变量,增大几个目标端口呗,Netty办这些事情还是很方便的。好,那就一个字,干!(当然也可以通过修改端口分配范围来提高一些)

image.png

哐哐哐一顿撸完,让Netty应用把7001~7005全给监听了,另外还需要稍微改造下nginx的配置文件。那这样一来理论上单机最大连接数能到28231* 5 = 141155,妥妥够了。实际压了一把,终于突破了28231,也确实能很轻松到4W预期目标了。

image.png

image.png

image.png


Round 4

继续朝着最终目标前进,单机压测10W连接,刚才的预计都能到14W了,现在10W应该问题不大吧。事实证明,Flag真的不能立太早,真的想不到啊想不到啊,这次只到了40960。。。我改了那么多,怎么就只能到40960 ?

不过吧,现在来看感觉这个坑还挺浅的,翻了一下nginx-proxy.conf的配置,就看到了worker_connection这个参数配置,猜也能猜到跟40960有关。机器上一共有4个nginx worker进程,每个进程最大连接数20480,加上因为反向代理的原因除以2,那差不多就是40960的最大限制了。好吧,调大这个值然后继续Round 5吧 。PS:nginx为了防止惊群效应,在epoll唤醒的时候会首先去获取一把锁,之后获取锁的进程去accept所有的已经激活(完成三次握手)的连接,nginx被压得时候,大量连接同一时刻到达,会导致大量连接被一个worker获取。

image.png


Round 5

因为当时不在线上发布窗口期,所以直接在预发环境调大了上述的nginx worker_connections配置,心想这下总能给我老老实实到10W了吧?现实:不能,觉悟吧,愚蠢的人类

image.png

image.png

56522。。。这又双叒叕是个啥限制,关键是这次真的再也找不到任何错误日志能分析的了,因为确实没有任何报错了。我也不知道这是啥,但是还好我敢问呐,所以又拉上了Tengine的韩述同学一起查,抓压测的网络包分析,升级tengine到最新版本,到最后还是没发现瓶颈点在哪边


image.png

65535 - 9000 = 56535

image.png

所以我只是换了个位置,然后又跳到了一个跟Round 3一模一样的坑里了是吗,哎。。。不过我得到了线上环境不会存在这一情况的答复,那就期待下次发布后的压测吧。

Round 6

终于等到发布窗口期了,测试完毕后的所有修改一起上线,这次目标压线上单机10W。

image.png

15W两台,稳!

image.png

啦啦啦啦啦,完结撒花~~~~

评论区
京ICP备19006603号-1 Rick ©2018