前段时间接到某项目中关于虚拟机所在宿主机上最大支撑连接数的测试需求。
应用场景类似于在物理机上运行着多个虚拟机,这些虚拟机对外提供服务,来自于任何地方的客户端都可能向这些应用服务发起连接和请求。也许单个虚拟机并发的连接数十分有限,但对提供虚拟机服务的物理机或宿主机来说连接数就可能达到十万、几十万甚至百万。在这样的情况下,宿主机是否能够稳定运行呢?同时项目方也提出了明确的测试目标,支撑300万连接,这就需要我们着手进行测试验证了。
首先,我们先做一个假设,即这300万连接里同时只有少数活动连接,测试场景可以简化为保持300万长连接的测试。明确了这一点,我们继续分析测试可能出现的瓶颈点,是CPU、内存还是网络?借鉴以往长连接或者消息推送类服务的测试经验,由于保持长连接并不需要消耗过多CPU和网络资源,只是占有系统的内存,我们决定把关注点主要集中在内存使用上,测试过程中也确实出现了一些内存相关的问题,这是后话了。
我们首先需要准备一个测试环境,一个可以支撑足够连接的应用服务,大量的客户端。很幸运我们可以借用杭研前台技术中心基于node.js开发的开源游戏引擎pomelo作为服务端程序。使用java网络API开发了一个简单的客户端程序。客户端向服务器发起连接请求,成功后把连接对象保存在内存中。至此万事具备,只欠执行测试了。
不急,我们先建立100万连接试试。没过多久第一个拦路虎就出现了,在客户端日志里出现了java.net.NoRouteToHostException异常,将它作为关键字google一把,原来是/proc/sys/net/ipv4/ip_local_port_range配置的区间太小,端口耗尽导致,配置修改如下
pomelo@debian :~/ socktest$ cat / proc / sys / net / ipv4 / ip_local_port_range2048 65500
可见单个客户端ip只能建立6万多连接,所以我们需要大约50个独立ip发起300万的连接,为简单起见我们使用50台虚拟机运行客户端,而没有采用单机多ip的方案。
继续测试,再次被异常打断,java.net.SocketException: Too many open files。这个有经验的同学都应该了解,该修改ulimit了。改完继续,可奇怪的是运行了一段时间后,又被同样的异常中断了,不是修改得很大了吗?怎么又出了呢?确认修改有没有生效,/etc/security/limits.conf文件是否保存,不会是这么狗血的问题吧。确认已经保存,检查了客户端进程的limit,/proc/pid/limits,发现open files竟然只有4096。百思不得其解。最后还是SA发现了问题的原因,原来是open files默认最大值1024*1024,我们修改时设置过大导致其溢出了,最后在/etc/security/limits.conf中添加如下两行搞定
* hard nofile 1048576* soft nofile 1048576
至此我们终于完成了100万连接的阶段性胜利,不能松懈, 一鼓作气拿下300万大关。等等,有人可能会问,服务端的open files也是配置1048576,就100万多点,肯定不能支撑300万连接吧。是的,open files是针对单一进程的限制,但我们跑的服务是多进程程序,所以不用担心。另外,open files的最大值也能通过配置/proc/sys/fs/nr_open参数修改,这样就能摆脱1048576的上限了;而系统中所有进程打开的文件数确实是需要配置的,通过fs.file-max修改。
接着我们遇到了这次测试中最可疑的问题之一。当建立完200万连接以后,我们kill掉了一个服务进程,没过多久,就发现部分运行客户端的虚拟机不能ssh登陆了。通过vnc连接上后发现虚拟机CPU几乎跑满了,dmesg中存在Out of socket memory这样的错误信息。此处省略一千字某SA大神的问题定位过程,本质上是由于服务端和客户端之间存在4万连接,服务进程挂掉后客户端机器收到大量FIN包导致网络相关内存溢出,而客户端机器上存在一个使用curl的定时任务,网络内存溢出引发其某个bug,进而引发CPU跑满。因此调大net.ipv4.tcp_mem配置,注意该参数的单位是页,而不是字节。
眼看着300万连接的目标已经近在咫尺,可问题再次不请自来了。在漫长的等待后,我们用ss –s确认最终建立的连接数,但这个数值却始终停留在280万附近,照例的打开dmesg查看,发现了一大堆错误信息,如下为开始的一段
[ 531503.634391 ] TCP : too many of orphaned sockets[ 531503.634412 ] TCP : too many of orphaned sockets[ 531503.634432 ] TCP : too many of orphaned sockets[ 531503.634451 ] TCP : too many of orphaned sockets[ 531508.704084 ] net_ratelimit : 255864 callbacks suppressed[ 531508.704088 ] Out of socket memory[ 531508.704233 ] Out of socket memory[ 531508.704245 ] Out of socket memory
简单的调大net.ipv4.tcp_max_orphans参数,问题依旧。
查看服务端日志,发现报错
FATAL ERROR: CALL_AND_RETRY_2 Allocation failed - process out of memory
查看服务进程数,确认有部分进程已不存在了,结合以上两种日志不难猜到问题的根源,是由于服务进程挂掉,瞬间出现大量孤儿连接,进而导致网络内存溢出引起。但服务进程为什么会莫名其妙地挂掉呢?咨询了相关开发人员,由于pomelo是基于node项目的,需要通过编译选项调整内存上限,修改重新编译后测试,进程还是出现了内存溢出的情况,但明白了问题的本质,我们通过增加node进程数量,以减小单个进程的内存占用,可以绕过该问题。而node进程的内存限制问题还需要后续的确认。
到此为止,终于完成了300万连接的目标,可喜可贺。简单总结一下,连接数测试不同于常规的性能测试,不太关注TPS和响应时间等指标,主要是通过dmesg和日志中出现的异常信息定位问题,进而调整系统相应参数,这些参数大多与网络、句柄数及内存有关。不仅仅是测试阶段,日常运维这类系统也应该时刻关注这些。