前段时间接到某项目中关于虚拟机所在宿主机上最大支撑连接数的 需求。应用场景类似于在物理机上运行着多个虚拟机,这些虚拟机对外提供服务,来自于任何地方的客户端都可能向这些应用服务发起连接和请求。也许单个虚拟机并发的连接数十分有限,但对提供虚拟机服务的物理机或宿主机来说连接数就可能达到十万、几十万甚至百万。在这样的情况下,宿主机是否能够稳定运行呢?同时项目方也提出了明确的测试目标,支撑300万连接,这就需要我们着手进行测试验证了。
首先,我们先做一个假设,即这300万连接里同时只有少数活动连接,测试场景可以简化为保持300万长连接的测试。明确了这一点,我们继续分析测试可能出现的瓶颈点,是CPU、内存还是网络?借鉴以往长连接或者消息推送类服务的测试经验,由于保持长连接并不需要消耗过多CPU和网络资源,只是占有系统的内存,我们决定把关注点主要集中在内存使用上,测试过程中也确实出现了一些内存相关的问题,这是后话了。
我们首先需要准备一个测试环境,一个可以支撑足够连接的应用服务,大量的客户端。很幸运我们可以借用杭研前台技术中心基于node.js开发的开源游戏引擎pomelo作为服务端程序。使用 网络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_range |
可见单个客户端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中添加如下两行搞定
至此我们终于完成了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和日志中出现的异常信息定位问题,进而调整系统相应参数,这些参数大多与网络、句柄数及内存有关。不仅仅是测试阶段,日常运维这类系统也应该时刻关注这些。
最新内容请见作者的GitHub页:http://qaseven.github.io/