原文:
https://zhuanlan.zhihu.com/p/65791330
https://zhuanlan.zhihu.com/p/65790128
待压测的服务器应用:城池;
测试软件:攻城军队;
测试软件需要做的就是构造出成千上百万的士兵,来攻占服务器的应用。
TCPBurn是一个关注并发的重放工具。(所谓重放就是 在服务端用过tcpdump 抓取客户端的请求消息,记录在pcap文件中,然后将pcap文件给TCPBurn读出里面的请求记录,将这些请求重新发往服务器)
如下图所示,TCPBurn由两个部分组成:
1:tcpburn 在测试主机(test server)上运行 tcpburn并从pcap文件获取要发送的记录“重放”发送数据。(pcap是提前在服务器端用tcpdum ”录“下的请求数据文件)
2:intercept 拦截运行助手服务器(Assistant server)并执行一些助手工作,如传递响应信息到 tcpburn。
ibm - - tcpburn 从pcap文件读取 [请求记录],并执行必要的处理( 包括TCP交互仿真。网络延时控制和通用上层交互仿真),默认情况下使用原始套接字输出技术将数据包发送到目标服务器服务器。
针对TCPBurn目标服务器所需的唯一操作是设置适当的路由(因为TCPburn或产生虚拟IP来发送数据到服务端,设置服务端的路由,让服务端收到虚拟IP发来的数据,就返回数据到 测试主机的IP,而不是往虚拟IP回复)。
intercept 负责将响应头传递给 tcpburn。 通过捕获响应包,intercept 将提取响应头信息,并使用特殊的信道( 紫色箭头) 将响应头发送到 tcpburn。 当收到响应头时,它利用头信息修改pcap包的属性,并继续发送另一个包。 应该注意到来自目标服务器的响应被路由到辅助服务器,该服务器应该充当一个黑洞。
为进行千万并发连接测试,采用的软硬件如下:
服务器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多空间。
(每一条链接两个socket<客户端和服务端在同一机器上时要特别注意这点>,每个socket 4KB左右,《每个 TCP 连接最少占用多少内存》https://blog.csdn.net/sinat_41832255/article/details/80048580)
服务器操作系统内核版本:
Linux 3.10.0-957.el7.x86_64
服务器CPU采样配置如下图:
服务器内存配置采样图:
从上图可以看出服务器可用内存大概是200G左右(为性能考虑,海量并发测试不考虑swap空间),用来支撑2000万并发连接应用。在这个配置下,理论上每一个连接消耗资源不超过10k字节才可以做到(包括内核+应用)。
虚拟机CPU采样配置如下图:
虚拟机内存情况如下图:
虚拟机的硬件条件足够支持单个TCPBurn模拟300万并发连接。
为规避高并发带来的系统参数问题,需要在如下方面进行配置:
参考第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来查看,见下图:
ulimit -HSn 1000000
需要注意上述命令只对本终端有效,建议修改/etc/security/limits.conf。
上述设置确保单个进程能够打开的句柄数量为100万。
由于Nginx是多进程程序,所以可以配置多个进程的方式来支持2000万连接,理论上至少需要20个进程才能达到。
由于TCPBurn采用了IP欺骗(原理类似流量复制工具TCPCopy,可参考课程TCPCopy相关部分),系统如果设置rp filter则会干扰测试的进行。
由于被测试的服务器配置了rp filter过滤,为简单起见,在服务器端关闭所有rp_filter设置。
由于TCPBurn采用IP欺骗的方式来模拟大量客户端连接,需要在服务器端配置路由,使其响应能够回到发送请求的客户端虚拟机上。
客户端虚拟机利用了7台虚拟机,所在的IP地址跟欺骗的IP网段地址对应关系如下:
192.168.25.121 <-------> 192.168.100.0
192.168.25.122 <-------> 192.168.101.0
192.168.25.123 <-------> 192.168.102.0
192.168.25.124 <-------> 192.168.103.0
192.168.25.125 <-------> 192.168.104.0
192.168.25.126 <-------> 192.168.105.0
192.168.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
上述路由设置的作用是请求从哪一台虚拟机过来,其响应就回到哪一台虚拟机。
我们需要部署一个消息推送Nginx服务(添加推送模块)用来支持2000万模拟用户的消息推送服务 。
安装Nginx的同时添加 nginx-push-stream-module 模块,nginx-push-stream-module 请参考官网:https://github.com/wandenberg/nginx-push-stream-module :
(中文:https://www.kancloud.cn/hfpp2012/websocket/467133)
安装:
# clone the project
git clone https://github.com/wandenberg/nginx-push-stream-module.git
NGINX_PUSH_STREAM_MODULE_PATH=$PWD/nginx-push-stream-module# get desired nginx version (works with 1.2.0+)
wget http://nginx.org/download/nginx-1.2.0.tar.gz# unpack, configure and build
tar xzvf nginx-1.2.0.tar.gz
cd nginx-1.2.0
./configure --add-module=../nginx-push-stream-module
make# install and finish
sudo make install
(如果make的时候报错: error: this statement may fall through [-Werror=implicit-fallthrough=]
h ^= data[2] << 16;,请将打开 nginx的安装目录/objs/Makefile,去掉CFLAGS中的-Werror,再重新make,参考:https://www.cnblogs.com/hxlinux/p/12900458.html)
检查:
# check
sudo /usr/local/nginx/sbin/nginx -vnginx version: nginx/1.2.0
# test configuration
sudo /usr/local/nginx/sbin/nginx -c $NGINX_PUSH_STREAM_MODULE_PATH/misc/nginx.conf -t
the configuration file $NGINX_PUSH_STREAM_MODULE_PATH/misc/nginx.conf syntax is ok
configuration file $NGINX_PUSH_STREAM_MODULE_PATH/misc/nginx.conf test is successful
# run
sudo /usr/local/nginx/sbin/nginx -c $NGINX_PUSH_STREAM_MODULE_PATH/misc/nginx.conf
遇到下面的错误:
nginx: [emerg] invalid event type "poll" in /root/nginx-push-stream-module/misc/nginx.conf:19
nginx: configuration file /root/nginx-push-stream-module/misc/nginx.conf test failed
则:注释掉/root/nginx-push-stream-module/misc/nginx.conf:19 的 use poll 这一行
Nginx配置参考下图:(vim /usr/local/nginx/conf/nginx.conf ?)
上图中配置了20个进程,每一个进程支持1048576个连接,理论上可以支持2000万并发连接(1048576×20 > 20000000)。
Nginx其它配置如下:
监听端口采用8080端口。
消息推送相关配置参数见下面两张图:
# add the push_stream_shared_memory_size to your http context
http {
push_stream_shared_memory_size 32M;
# define publisher and subscriber endpoints in your server context
server {
location /channels-stats {
# activate channels statistics mode for this location
push_stream_channels_statistics;
# query string based channel id
push_stream_channels_path $arg_id;
}
location /pub {
# activate publisher (admin) mode for this location
push_stream_publisher admin;
# query string based channel id
push_stream_channels_path $arg_id;
}
location ~ /sub/(.*) {
# activate subscriber (streaming) mode for this location
push_stream_subscriber;
# positional channel path
push_stream_channels_path $1;
}
}
}
完成上述步骤后,消息推送服务已经部署好,直接启动Nginx:
./nginx
Nginx就可以等待用户发送请求过来。
"Starting nginx... nginx: [emerg] bind() to 0.0.0.0:888 failed (98: Address already in use)"这样的报错问题。
Starting nginx... nginx: [emerg] bind() to 0.0.0.0:888 failed (98: Address already in use)
nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
nginx: [emerg] bind() to 0.0.0.0:888 failed (98: Address already in use)
nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
nginx: [emerg] bind() to 0.0.0.0:888 failed (98: Address already in use)
nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
nginx: [emerg] bind() to 0.0.0.0:888 failed (98: Address already in use)
nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
nginx: [emerg] bind() to 0.0.0.0:888 failed (98: Address already in use)
nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
nginx: [emerg] still could not bind()
failed
估摸着是在操作的时候忘记关闭Nginx导致启动冲突,这里要禁止掉端。
sudo fuser -k 80/tcp
最后我们再重启Nginx后显示正常:
/etc/init.d/nginx restart
tcpburn(TCPBurn是由tcpburn和 intercept组成)运行,首先需要intercept的配合(具体工作原理图可以参考TCPCopy部分课程)。
intercept工具的主要功能是截获Nginx消息推送服务的响应数据包,配合tcpcopy和tcpburn工具来完成用户会话回放。没有intercept的配合,tcpburn就无法工作。
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方式运行
安装tcpburn参考官网github地址:https://github.com/session-replay-tools/tcpburn
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运行目录下面。
客户端虚拟机必须关掉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使其生效
1、第一次冲击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!'
5个消息推送完以后,每一个客户端连接都会收到一个推送消息。
根据下图,可以看出可用内存还有40多G空间,而整个消息推送过程,额外消耗了不少的内存。
通过这次测试,我们发现Nginx如下的问题:
因为最终内存还有40多G,说明还是有潜力的,我们需要找出瓶颈所在。
2、第二次冲击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千万并发连接的目标顺利达到。
3、结束语
通过这次测试,暴露了Nginx消息推送在极端海量并发情况下的问题,而且为了支持2000万并发连接,还需要修改系统参数fs.nr_open,以支持ulimit -HSn更大数量的设置。
从这次测试过程中,可以证明Linux系统支持千万并发是可行的,而且也无需很多配置。实践是检验真理的标准,这句话还是挺有道理的。
用户如果感兴趣,可以去尝试利用TCPBurn进行更加激进的性能测试,去探索海量连接场景下未知的Linux内核世界。下面二维码是“TCP相关问题经典案例分析” 知识星球,有兴趣可以加入,一起探索TCP和应用的相关问题。