千万并发不是梦:TCPBurn并发测试

摘要:TCPCopy 和 Cetus 开源主要作者
知识星球 ID: 47406575,提供 TCP 经典案例分析课程
微信公众号:得一技术
“黑云压城城欲摧,甲光向日金鳞开”,唐朝诗人李贺字面上描绘了黑云压城的自然景象,但实际描述的是敌军攻占城池的人马众多,来势汹汹,但是守城士兵依旧严阵以待,斗志昂扬。这种攻守的战争场面,和服务器的高并发压测十分相似。待压测的服务器应用类似于待攻占的城池,而测试

千万并发不是梦:TCPBurn并发测试

TCPCopy 和 Cetus 开源主要作者
知识星球 ID: 47406575,提供 TCP 经典案例分析课程
微信公众号:得一技术
“黑云压城城欲摧,甲光向日金鳞开”,唐朝诗人李贺字面上描绘了黑云压城的自然景象,但实际描述的是敌军攻占城池的人马众多,来势汹汹,但是守城士兵依旧严阵以待,斗志昂扬。这种攻守的战争场面,和服务器的高并发压测十分相似。待压测的服务器应用类似于待攻占的城池,而测试软件需要做的就是构造出成千上百万的士兵,来攻占服务器的应用。
如何构造出成千上百万的攻城士兵,是高并发测试的关键。而传统压力测试工具设计的时候并不是针对高并发测试设计的。针对高并发场景,传统压力测试工具往往自身是性能瓶颈。为适应高并发趋势,我们设计了 TCPBurn,用于无状态协议的高并发压力测试,瞬间可以创造出任意多的攻城的精兵猛将。
我们以消息推送服务为例,来模拟海量用户并发场景。千万并发连接测试相关的公开资料很少,据说要达到 C10M(千万连接)并发,需要从根本上解决内核自身的问题。我们的实验希望验证 linux 服务器环境下 Nginx 能否承受千万连接的考验。

1、软硬件配置

为进行千万并发连接测试,采用的软硬件如下:

服务器 IP 地址采用 192.168.25.89。
TCPBurn 采用 IP 欺骗的方式来模拟客户端,需要为客户端选择 IP 地址网段,这里我们选择内网 IP 地址来欺骗 Nginx。
客户端虚拟机 IP 地址和 TCPBurn 客户端所采用的 IP 地址关系如下:

虚拟机和服务器具体关系图如下(图中省略 192.168 前缀):
图片
图中的城池便是我们待测试的 Nginx 服务;我们在城池的周围建立了 7 个营地,每个营地其实是客户端的虚拟机。接下来,我们需要在每个虚拟机上,通过 TCPBurn 来构建我们的精兵猛将。每个 TCPBurn 实例为客户端配置了 254 个 IP 地址,每个可用的 IP 地址类似于营地中的兵团,而 TCPBurn 可利用的端口区间为 32768~65535,也就是每个兵团的士兵可达到 32768 个。这么计算下来,我们使用 TCPBurn,在每个营地,轻而易举的便能够构造出 254×32768=8323072 个士兵,即 800 多万并发连接。在我们测试过程中,每一个营地会利用 300 万个地址空间来构造 300 万个客户端并发连接,需要大概 500 多 M 内存,而 2000 万并发连接,需要累计消耗 3G 多空间。
服务器操作系统内核版本:
Linux 3.10.0-957.el7.x86_64
服务器 CPU 采样配置如下图:
图片
服务器内存配置采样图:
图片
从上图可以看出服务器可用内存大概是 200G 左右(为性能考虑,海量并发测试不考虑 swap 空间),用来支撑 2000 万并发连接应用。在这个配置下,理论上每一个连接消耗资源不超过 10k 字节才可以做到(包括内核 + 应用)。
虚拟机 CPU 采样配置如下图:
图片
虚拟机内存情况如下图:
图片
虚拟机的硬件条件足够支持单个 TCPBurn 模拟 300 万并发连接。

2、服务器端系统参数配置

为规避高并发带来的系统参数问题,需要在如下方面进行配置:

1)规避 IP conntrack 坑

参考第 21 讲(IP CONNTRACK 大坑,你跳不跳)内容,我们在服务器端配置了 iptables 命令来规避 IP conntrack 坑(两大坑,性能 + 连接无法建立):
具体命令:
iptables -t raw -A PREROUTING -p tcp –dport 8080 -j NOTRACK
iptables -t raw -A OUTPUT -p tcp –sport 8080 -j NOTRACK
这里 8080 是 Nginx 监听端口,意思是对出入 8080 端口的数据包不进行跟踪。设置上述命令可以降低内存和 cpu 资源消耗,并且也不会干扰测试的进行(21 讲会详细讲述)。
配置结果可以通过 iptables 来查看,见下图:
图片

2)规避文件句柄坑

ulimit -HSn 1000000
需要注意上述命令只对本终端有效,建议修改 /etc/security/limits.conf。
上述设置确保单个进程能够打开的句柄数量为 100 万。
由于 Nginx 是多进程程序,所以可以配置多个进程的方式来支持 2000 万连接,理论上至少需要 20 个进程才能达到。

3)关闭 rp filter 设置

由于 TCPBurn 采用了 IP 欺骗(原理类似流量复制工具 TCPCopy,可参考课程 TCPCopy 相关部分),系统如果设置 rp filter 则会干扰测试的进行。
由于被测试的服务器配置了 rp filter 过滤,为简单起见,在服务器端关闭所有 rp_filter 设置。

4)路由设置

由于 TCPBurn 采用 IP 欺骗的方式来模拟大量客户端连接,需要在服务器端配置路由,使其响应能够回到发送请求的客户端虚拟机上。
客户端虚拟机利用了 7 台虚拟机,所在的 IP 地址跟欺骗的 IP 网段地址对应关系如下:
.25.121   <——-> 192.168.100.0
.25.122   <——-> 192.168.101.0
.25.123   <——-> 192.168.102.0
.25.124   <——-> 192.168.103.0
.25.125   <——-> 192.168.104.0
.25.126   <——-> 192.168.105.0
.25.127   <——-> 192.168.106.0
在 服务端进行路由设置:
route add -net 192.168.100.0 netmask 255.255.255.0 gw 192.168.25.121
route add -net 192.168.101.0 netmask 255.255.255.0 gw 192.168.25.122
route add -net 192.168.102.0 netmask 255.255.255.0 gw 192.168.25.123
route add -net 192.168.103.0 netmask 255.255.255.0 gw 192.168.25.124
route add -net 192.168.104.0 netmask 255.255.255.0 gw 192.168.25.125
route add -net 192.168.105.0 netmask 255.255.255.0 gw 192.168.25.126
route add -net 192.168.106.0 netmask 255.255.255.0 gw 192.168.25.127
上述路由设置的作用是请求从哪一台虚拟机过来,其响应就回到哪一台虚拟机。

3、服务器端 Nginx 部署安装

我们需要部署一个消息推送 Nginx 服务,用来支持 2000 万模拟用户的消息推送服务 。
具体安装请参考官网:https://github.com/wandenberg/nginx-push-stream-module
图片
Nginx 配置参考下图:
图片
上图中配置了 20 个进程,每一个进程支持 1048576 个连接,理论上可以支持 2000 万并发连接(1048576×20 > 20000000)。
Nginx 其它配置如下:
图片
监听端口采用 8080 端口。
消息推送相关配置参数见下面两张图:
图片
图片
完成上述步骤后,消息推送服务已经部署好,直接启动 Nginx:
./nginx
Nginx 就可以等待用户发送请求过来。

4、客户端 TCPBurn 部署安装

tcpburn(TCPBurn 是由 tcpburn 和 intercept 组成)运行,首先需要 intercept 的配合(具体工作原理图可以参考 TCPCopy 部分课程)。
intercept 工具的主要功能是截获 Nginx 消息推送服务的响应数据包,配合 tcpcopy 和 tcpburn 工具来完成用户会话回放。没有 intercept 的配合,tcpburn 就无法工作。

1)intercept 安装运行

intercept 安装参考官网 github 地址:https://github.com/session-replay-tools/tcpburn
图片
如果编译的时候遇到问题,一般是因为没有安装相应的 libpcap 开发库,安装上即可(centos 上可以使用命令:yum install libpcap-devel.x86_64)
运行命令如下:
图片
需要注意的是,7 台虚拟机运行的 intercept 命令都一样,具体命令如下:
./intercept -i eth1 -F ‘tcp and src port 8080’ -d
intercept 命令参数说明如下:
-i 参数设置的是网卡设备名称,因环境不同会有差异。
例如,下图 192.168.25.121 对应的网卡设备是 eth1,而 Nginx 返回给 192.168.100.0 网段的 IP 地址响应包都通过路由走向 192.168.25.121(充当网关),经过的网卡就是 eth1,所以 - i 参数选择 eth1:
图片
-F 参数设置过滤条件,需要加引号,引号里的内容类似 tcpdump 的过滤条件
-d 参数,设置 - d 代表以 daemon 方式运行

2)tcpburn 安装

安装 tcpburn 参考官网 github 地址:https://github.com/session-replay-tools/tcpburn
图片

3)准备测试数据

tcpburn 不伪造原始数据,需要依赖外部抓包文件。
在服务器端,我们采用如下命令来开启抓包:
tcpdump -i any tcp and port 8080 -s 1500 -w 8080.pcap -v
然后我们在另外一个终端下面访问 Nginx 服务:
图片
这些访问就会被 tcpdump 所捕获。
我们累计开启 5 个终端,分别访问如下:
curl -s -v –no-buffer ‘http://192.168.25.89:8080/sub/my\_channel\_1'
curl -s -v –no-buffer ‘http://192.168.25.89:8080/sub/my\_channel\_2'
curl -s -v –no-buffer ‘http://192.168.25.89:8080/sub/my\_channel\_3'
curl -s -v –no-buffer ‘http://192.168.25.89:8080/sub/my\_channel\_4'
curl -s -v –no-buffer ‘http://192.168.25.89:8080/sub/my\_channel\_5'
然后 ctrl+c 关闭服务器 tcpdump 抓包,5 个请求都被捕获。利用 wireshark 来查看这些抓包数据,从下图中我们可以看出抓包文件捕获了 5 个请求作为 tcpburn 回放的请求。
图片
我们把抓包文件 8080.pcap 放到每一个虚拟机 tcpburn 运行目录下面。

4)关闭客户端虚拟机 ip forward 功能

客户端虚拟机必须关掉 ip forward 功能,否则那些回到虚拟机的响应数据包,会路由给真正的客户端机器(具体参考 TCPCopy 欺骗部分内容)。
查看客户端 ip forward 是否开启的方法如下:
sysctl -a|grep ip_forward
如果 net.ipv4.ip_forward = 1,那就必须在客户端 /etc/sysctl.conf 文件里面增加下面一行:
net.ipv4.ip_forward = 0
并执行 sysct -p 使其生效

五、第一次冲击 2000 万并发失败

在客户端虚拟机上利用 tcpburn 命令发起连接请求。
在第一台虚拟机(192.168.25.121)tcpburn 命令如下:
./tcpburn -x 8080-192.168.25.89:8080 -f /xxx/tcpburn/sbin/8080.pcap -s 192.168.25.121 -u 3000000 -c 192.168.100.x
参数解释如下:
-x  8080-192.168.25.89:8080 代表复制抓包文件的 8080 端口请求到 192.168.25.89 机器的 8080 端口应用
-f 参数指定抓包文件的路径
-s 参数指定运行 intercept 所在机器的 IP 地址(tcpburn 与 intercept 一一对应)
-u 参数指定模拟用户的数量,-u 3000000 代表一个 tcpburn 实例模拟 300 万用户
-c 参数代表 300 万用户采用的 IP 地址列表是从 192.168.100.0 网段去获取
由于客户端虚拟机和服务器都在同一个网段 192.168.25.0,可能你会问客户端 IP 地址为什么不采用这个网段的地址呢?首先我们不想干扰本网段的应用,其次单台虚拟机采用的端口数是有限的。对服务器 Nginx 应用,由于客户端虚拟机 TCP 层的端口限制,最多模拟几万个连接,而 TCPburn 由于绕开了 TCP 端口限制,采用 IP 欺骗的方式,可以采用任意其它网段的 IP 地址,这样就可以利用海量的地址空间,从而为海量用户模拟打下基础。这里我们采用 192.168.100.0 网段,理论上可以利用 254×32768=8323072(tcpburn 采用的端口从 32768 开始)个连接,而我们这里只用了 300 万个连接。
我们逐个运行 TCPBurn。每一台虚拟机运行一个 TCPBurn 实例,包括 tcpburn 和 intercept 实例。intercept 命令相同,而 tcpburn 命令在不同虚拟机的参数不同,需要修改 - s 参数和 - c 参数。下图是运行了第 6 个 tcpburn 实例时的情况,连接数量已经达到 1600 多万。
图片
此时的内存使用情况呢?参考下图,服务器还有 60 多 G 内存空间可以利用。
图片
此时的 Nginx,情况如何?Nginx 开始大量报 too many open files 错误,每秒大概输出这样的日志数量高达几十万,而我们的连接数量只是以每秒几千的速度缓慢增加。Nginx 高频繁输出这样的错误日志,显然是不合理的,而且这样很快就会打爆磁盘空间。
图片
此时,tcpburn 运行过的命令如下图:
图片
第 6 个 tcpburn 实例并没有运行完毕。理论上,第 6 个 tcpburn 运行完,连接数量可达到 1800 万。
继续观察一段时间,服务器连接数量不再增加,最终运行结果如下图:
图片
服务器连接数量达到 1672 万后,就很难上升了,但从下图的内存来看,还有 50 多 G 空闲内存,这说明 Nginx 遇到 accept 连接瓶颈了。
图片
我们继续试验,让这 1600 多万客户端连接都接收一个消息推送,以便查看 Nginx 运行和内存情况。
下图我们在服务器端机器对 my_channel_1 发送了 Goodbye 消息,根据 Nginx 返回结果,会有 3344639 个客户端连接去接收这个消息。
图片
同时我们查看 Nginx 运行情况,我们发现下图中的 Nginx 异常繁忙,因为有几百万的消息推送需要处理。
图片
对 5 个 channel 进行消息推送(服务器端执行),命令如下图:
curl -s -v -X POST ‘http://localhost:8080/pub?id=my\_channel\_1' -d ‘Goodbye!’
curl -s -v -X POST ‘http://localhost:8080/pub?id=my\_channel\_2' -d ‘Goodbye!’
curl -s -v -X POST ‘http://localhost:8080/pub?id=my\_channel\_3' -d ‘Goodbye!’
curl -s -v -X POST ‘http://localhost:8080/pub?id=my\_channel\_4' -d ‘Goodbye!’
curl -s -v -X POST ‘http://localhost:8080/pub?id=my\_channel\_5' -d ‘Goodbye!’
个消息推送完以后,每一个客户端连接都会收到一个推送消息。
根据下图,可以看出可用内存还有 40 多 G 空间,而整个消息推送过程,额外消耗了不少的内存。
图片
通过这次测试,我们发现 Nginx 如下的问题:
无缘无故地记录一些 access log,而报错之前没有记录
频繁刷日志,容易导致磁盘空间耗满
负载不均衡,到 1600 多万连接就很难接收新的连接了
千万并发连接测试过程中,通过 pkill nginx 有时候关不掉
因为最终内存还有 40 多 G,说明还是有潜力的,我们需要找出瓶颈所在。

六、第二次冲击 2000 万并发成功

通过如下命令尝试继续增加最大打开文件句柄数量
ulimit -HSn 1048580
-bash: ulimit: open files: cannot modify limit: Operation not permitted
ulimit -HSn 1048575
ulimit -HSn 1048576
ulimit -HSn 1048577
-bash: ulimit: open files: cannot modify limit: Operation not permitted
从中找出进程的最大文件句柄数量为 1048576,然后利用如下命令:
sysctl -a|grep 1048576
看看哪些参数设置了这个值,结果发现如下:
fs.nr_open = 1048576
fs.pipe-max-size = 1048576
其中 pipe-max-size 是 pipe 相关的,跟文件句柄最大支持数量没有关系,而 nr_open 是真正能够改变最大文件支持数量的参数。
直接在 /etc/sysctl.conf 文件里面增加:
fs.nr_open = 2097152
这里数量是原先的 2 倍大小。
执行:
sysctl -p
参数生效。
需要注意的是,只有内核 2.6.25 及之后可以修改此参数。
然后我们继续设置如下命令:
ulimit -HSn 2000000
系统不再报错,这样就能支持单个进程 200 万并发连接了。
修改了系统参数后,需要继续修改 Nginx 配置文件:
图片
上图理论上可支持 3000 万连接,离 2000 万有一段距离,应该不会再报 too many open files 错误。
重新启动 nginx 和 tcpburn,继续新的一轮测试:
执行 tcpburn 的实例如下图:
图片
累计会有 2050 万的并发连接请求发送到服务器。
最终服务器端的连接数量突破了 2000 万连接,如下图:
图片
这个时候服务器空闲内存只有 19G 了,还能不能应对 2000 万并发消息处理呢?
我们继续在服务器端发送下述请求到 Nginx 服务:
curl -s -v -X POST ‘http://localhost:8080/pub?id=my\_channel\_1' -d ‘Goodbye!’
curl -s -v -X POST ‘http://localhost:8080/pub?id=my\_channel\_2' -d ‘Goodbye!’
curl -s -v -X POST ‘http://localhost:8080/pub?id=my\_channel\_3' -d ‘Goodbye!’
curl -s -v -X POST ‘http://localhost:8080/pub?id=my\_channel\_4' -d ‘Goodbye!’
curl -s -v -X POST ‘http://localhost:8080/pub?id=my\_channel\_5' -d ‘Goodbye!’
我们发现 Nginx 非常繁忙,如下图:
图片
利用 free –m 来查看内存情况,结果如下图:
图片
为什么内存越来越多呢,我们查看 Nginx 日志,内容如下:
图片
进程 48887 的 Nginx 进程退出了,而且还是被杀退出的(signal 9)。
我们看看服务器连接数量有没有下降,利用 ss –s 查看,如下图:
图片
连接数量也下降了,原因是 Nginx 进程 48887 被杀了。
我们查看系统 dmesg 日志(直接运行 dmesg),从下图我们看出进程 48887(nginx)被 OOM 了,也即因为内存吃紧被操作系统选择性杀掉了,可用内存增加也就不足为奇了。
图片
整个测试到此为止,测试 2000 千万并发连接的目标顺利达到。

七、结束语

通过这次测试,暴露了 Nginx 消息推送在极端海量并发情况下的问题,而且为了支持 2000 万并发连接,还需要修改系统参数 fs.nr_open,以支持 ulimit -HSn 更大数量的设置。
从这次测试过程中,可以证明 Linux 系统支持千万并发是可行的,而且也无需很多配置。实践是检验真理的标准,这句话还是挺有道理的。
用户如果感兴趣,可以去尝试利用 TCPBurn 进行更加激进的性能测试,去探索海量连接场景下未知的 Linux 内核世界。btw,下面二维码是 “TCP 相关问题经典案例分析” 知识星球,有兴趣可以加入,一起探索 TCP 和应用的相关问题;也可以通过 qq 群:1013880537 咨询 TCPBurn 相关问题。


来源网址:https://my.oschina.net/u/3803982/blog/3049638