你好,我是胜辉。

在上一讲里,我们排查了一个跟操作系统紧密相关的性能问题。我们结合top和strace这两个工具,抓住了关键点,从而解决了问题。性能问题,确实也是我们日常技术工作中的一个重要话题。在出现性能问题以后,我们要有能力搞定它;而在出现性能问题之前,最好能提前预见到它。而要“预见”性能瓶颈,最好的方法就是做压力测试

但是,我们在做压力测试的过程中也时常出现预料不到的情况。比如在离预期的瓶颈值还很远的时候,系统就出现了各种意外,影响到压力测试的继续进行。

那么在这节课里,我们会回顾几个典型的压力测试场景中的网络问题,一起来学习其中的关键要点。同时,我们还会学习一系列跟网络性能相关的压测工具和检测工具,这样以后你遇到类似的问题时,就有所准备了。

压测要做什么?

压力测试的诉求实际上多种多样,不过大体上可以分为这几种。

应用的承受能力:这主要在第七层应用层,比如发起了压测,把服务端的CPU打到95%甚至100%,观察这时候的请求的TPS、请求耗时、并发量等等。而这些对于不同的业务场景,又会有不同的侧重点。比如:

LB的连接处理能力:这主要在第四层TCP,看LB能最大支持的TCP并发连接数。这时候,发起压测的客户端一般会指定比较大的并发数,这样就可以发起尽量多的TCP连接。

网络的承受能力:这可能主要在第三层IP层了,比如测试上行和下行带宽能否跑满、是否有丢包和额外的延迟,等等。特别是对于一些流量比较大的场景,很可能服务端计算能力都还在,但带宽已经不够用了,所以我们要提前发现这些隐患。

案例1:压测TPS上不去

我们有个客户是传统企业转型做电商,有一次准备搞大促。为了确保大促顺利,他们要提前对网站进行压测。

客户用的压测工具比较简单,是Apache ab。ab是Apache Benchmark的缩写,它的用途就是对HTTP服务端发起测试,以获得性能指标(Benchmark)。ab本身不是独立安装的,而是在apache2-utils工具包里,所以你可以这样来安装它:

apt install apache2-utils

ab是一个轻量级的工具,因为相对其他重量级的工具比如LoadRunner或者JMeter来说,ab只要一行命令就可以发起压测了,是不是很省事?比如你可以这样:

ab -c 100 -n 10000 目标URL

通过上面的命令,你就用-c 100这个参数,让ab发起了100个并发的请求,而-n 10000指定了总共发送的请求量。

补充:这里有一个小的注意点。如果目标URL只是站点名本身,还是需要在结尾处加上“>/”,要不然ab会报这个错误:ab: invalid URL

比如我用下面这条ab命令,对一个著名网站发起“压测”,当然我的参数选择的很小,只有10个并发,一共100次请求,尽量避免打扰到这个网站。我们可以看一下输出:

$ ab -c 10 -n 100 https://www.baidu.com/abc
This is ApacheBench, Version 2.3 <$Revision: 1843412 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking www.baidu.com (be patient).....done


Server Software:        Apache
Server Hostname:        www.baidu.com
Server Port:            443
SSL/TLS Protocol:       TLSv1.2,ECDHE-RSA-AES128-GCM-SHA256,2048,128
Server Temp Key:        ECDH P-256 256 bits
TLS Server Name:        www.baidu.com

Document Path:          /abc
Document Length:        201 bytes

Concurrency Level:      10
Time taken for tests:   9.091 seconds
Complete requests:      100
Failed requests:        0
Non-2xx responses:      100
Total transferred:      34600 bytes
HTML transferred:       20100 bytes
Requests per second:    11.00 [#/sec] (mean)
Time per request:       909.051 [ms] (mean)
Time per request:       90.905 [ms] (mean, across all concurrent requests)
Transfer rate:          3.72 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:      141  799 429.9    738    4645
Processing:    19   67 102.0     23     416
Waiting:       17   67 101.8     23     416
Total:        162  866 439.7    796    4666

Percentage of the requests served within a certain time (ms)
  50%    796
  66%    877
  75%    944
  80%   1035
  90%   1093
  95%   1339
  98%   1530
  99%   4666
 100%   4666 (longest request)

结尾部分的多个Percentage,确切地说就是Percentile,也就是百分位。比如50% 796这一行的意思是:第50个请求(因为总数是100个)的耗时小于等于796毫秒,另外50个请求大于796毫秒。那么我们也可以知道,耗时最长的那个就是排最后一名的4666,它的耗时是4666毫秒。

这次,客户用ab时指定的具体参数是这样的:

ab -k -c 500 -n 100000 http://site.name.com/path

也就是并发数为500,总次数为10万,而-k参数是启用长连接。然后就是查看以下两个主要指标:

压测过程中,客户发现测试端的带宽用到400Mbps后,TPS就上不去了,无论把并发量或者总量的数值进行怎样的调整,TPS都会维持在一个稳定的数值。

Transfer rate:          52087.82 [Kbytes/sec] received

那究竟是不是购买的服务端主机的性能不够的原因呢?要知道,客户端和服务端都是1Gbps的网卡,客户是想压满100%的带宽,现在只用到了40%的带宽,TPS就上不去了。

为什么会这样呢?

我们就跟客户配合,一边做ab压测,一边做了4秒钟的tcpdump抓包,这对采样分析来说也够用了。然后打开抓包文件,查看专家信息(Expert Information),如下:

补充:抓包示例文件已经上传至Gitee,建议结合抓包文件和文稿一起学习。

图片

这次我就不做标记了,你自己先找找看,能否找到一些问题?

你有没有发现,GET有1603次(倒数第三行),而RST有1982次(第三行),比GET这种HTTP请求的次数还更多。也就是说,平摊的话,每次GET请求对应了一次以上的RST。这个现象会不会跟TPS上不去的问题有关系呢?

我们选一个RST来看一下情况。比如14031号报文:

图片

然后Follow -> TCP Stream,就来到了这条TCP流:

图片

在这里,你有没有发现两个重传报文?一个是6504号报文,一个是7340号报文。我们再进一步看一下这两个的重传的原因是什么。一般来说,判断一个报文是否是重传,最方便的就是借助Wireshark本身提供的提示,Wireshark说是Retransmission,那就是了。

那么假设世界上还没有Wireshark,你又如何判断一个报文是否是重传呢?其实可以根据两个关键信息:序列号、载荷长度

我在之前的课程里提到过,序列号本身反映的就是字节位置,载荷的长度就表示这段报文的实际字节长度,这两个信息就确定了信息的起点和终点,也就是决定了一个报文是否是之前报文的重传。

在上面的截图里,我们根据序列号(Sequence Number列)和载荷长度(TCP Seglen列),判断出下面两次重传:

可见,7340是3155报文的重传,因为它们的序列号都是9593,载荷长度都是1061。同样的道理,6504和3156也是一对重传关系。

当然,我们也可以直接在Wireshark里选中7340号报文,它的基础报文也就是3155的前面会出现一个小圆点,就像下图这样:

图片

总之,我们确认TCP传输中是有丢包和重传现象的,我们可以回头从专家信息那里得到证实,这里显示有184个Suprious重传和2274个(超时)重传。

图片

补充:而出现RST的原因,是客户端在已经没有了这个TCP连接的情况下,收到了服务端的ACK报文。从现象来看,客户端应该是做了TIME_WAIT优化的设置,我们后面的实验里也会带到。

考虑到这是在压测场景中出现的丢包现象,我们根据经验,想到了网卡包量的问题。

一般说到网络性能,我们会讨论的就是带宽、时延、网速等等这些指标。实际上,另外一个性能指标常常在达到带宽极限之前就已经触顶了,它就是包量。

包量是对PPS(Packet Per Second)的简称,一般用来衡量一台主机的网络处理能力。那么,这次是不是触及了服务端主机的包量上限了呢?我们又如何检查包量指标呢?

我们需要用的工具是sar。

性能工具sar

sar是sysstat工具集的一部分。这个工具集包含了一些很有用的工具,除了sar,还有mpstat、iostat等等。如果你平时经常做操作系统维护方面的工作,应该对它们比较熟悉了。这些工具还有一个共同的特点,运行的时候一般都是加上“间隔 次数”这样的参数的。也就是下面这样:

sar -n DEV 1 10   #查看网卡性能
iostat 1 10       #查看IO性能
mpstat 2 5        #查看CPU性能

这些工具会按指定的时间间隔,运行指定的次数后再退出。这样我们就可以观察到这段时间内的多次输出了,很适合通过这种多次的观察,得到比较准确的性能数据趋势。

这次我们也在被压测的服务端云主机上运行了 sar -n DEV,得到了以下的性能数据:

图片

前面说的包量,就是体现在两个pck/s指标中,一个是 rxpck/s,就是接收方向的包量;一个是 txpck/s,就是发送方向的包量。显然,图中的数值已经在5万左右,这个正是当时我们云主机性能的上限。难怪服务端已经无法提供更高的TPS了,因为网络包的处理都来不及做了。

那你可能会问了:“网络包也有大有小,这个包量指标说的是大包还是小包呢?”

其实,一般的包量测试,不是随便什么大小的报文都可以测试,而是普遍使用64字节长度的IP报文。另外,我们也要认识到,包的大小对包量性能的影响也不是很大。这是因为,对于网络处理来说,主要的开销在包的头部的处理上,而载荷本身的处理是很快的。

那么,对于客户遇到的这种包量达到上限的情况,我们可以选择的应对办法是这样的:

案例2:LoadRunner压测发现部分失败

这是另外一个客户,他们没有用ab这样的简单工具,而是用了LoadRunner这样一个企业级的测试软件。LoadRunner的厉害之处在于,不仅可以发起巨大的请求量,而且可以模拟用户的复杂行为,比如登录、浏览、加入购物车等等。这一系列事务有前后状态关系,这就不是简单的ab可以做到的了。

但是测试结果中的小几十个Failed和Error引起了客户的疑虑。他们怀疑:是不是我们公有云的机器或者网络质量不行,所以才导致了这些失败呢?

我们就尝试复现这个压测场景,同时也对测试机上的TCP资源情况做检查,其中最主要的就是源端口了。

为什么会想到这个呢?因为压测发起的请求,都是依托于TCP连接的。我们在第1讲里就提到过:TCP连接是基于五元组的。那么对于客户端来说,源IP、目的IP、目的端口、协议,这四个元素都不会变化,唯一会变的就是自己的源端口了。那么我们来看看,当时测试机的源端口情况。

这是一台Window机器,跟Linux类似,我们执行这条命令:

netstat -ant

输出如下图:

原来,确实有大量的TCP连接都在TIME_WAIT状态,尤其是源端口已经用到了65534。所以可以肯定,这次压测中出现失败的原因,就在于源端口耗尽。

那要如何处理呢?显然我们也变不出更多的端口来。这个时候,我们可以调整压测软件的设置,比如从短连接改成长连接。这样就可以避免源端口耗尽的情况,因为所有的请求是在长连接里完成的,只要连接池本身设置合理,源端口就不会被用完。

案例3:压测报cannot assign requested address错误

我们内部有一个团队在做业务压力测试,结果遇到了一个奇怪的报错:cannot assign requested address。这次的场景是这样的:这个团队从多台客户端机器向LB上的一个VIP,发送大量的请求,而LB的后面就是很多的服务器。

但是,还没等到这些服务器的负载跑起来,客户端那边提前报错了。这是一个Go语言的程序,具体的报错信息如下:

https://test.vip/a": dial tcp 10.123.123.12:443: connect: cannot assign requested address
http post Post \"https://test.vip/b": dial tcp 10.123.123.12:443: connect: cannot assign requested address retry 1 times

于是他们就怀疑:既然报错是连接出错,那是不是LB有什么问题呢?比如,是否LB本身处理能力不够,不能服务这么大量的连接请求?否则为什么客户端会报connect的错误呢?要知道,用来发起TCP连接的系统调用(syscall)就是connect。

确实,这个报错信息“dial tcp 10.123.123.12:443: connect: cannot assign requested address”说得不太清楚。表面上看,就是客户端往10.123.123.12:443这个VIP发起连接请求(用connect)然后遇到了报错,也难怪测试团队会找到我们来查看LB的问题。

我们检查了LB,发现一切正常。于是把排查方向换到客户端。在这次测试中,有一个参数是关于HTTP Keep-alive的。如果你对课程前面的内容还有印象的话,应该还记得我们在第7讲“保活机制”里,深入讨论过这个问题。

简单来说,这次的测试配置里,HTTP Keep-alive没有打开,导致这些TCP连接被视作短连接来处理了,也就是一次HTTP请求和响应完成后,这条连接就关闭了。由于发起关闭的是客户端自己,于是这条连接也就进入了TIME_WAIT状态。

而要查看TIME_WAIT状态的连接数量,我们可以用 netstat命令,配合管道和awk来完成统计。比如,这次我在一台客户端机器上执行了下面的命令:

$ netstat -ant |awk '{++a[$6]} END{for (i in a) print i, a[i]}'
TIME_WAIT 28231

或者用下面的 ss命令会更快:

ss -ant | awk '{++s[$1]}END{for(k in s) print k,s[k]}'
TIME-WAIT 28231

可见,处于TIME_WAIT状态的连接数接近3万个,差不多就是Linux的本地动态端口的范围了。我们随后检查这台Linux机器的本地源端口范围,执行了下面的命令:

$ cat /proc/sys/net/ipv4/ip_local_port_range
32768	60999

于是发现它的下限是32768,上限是60999,范围正好就是28231,跟TIME_WAIT的数量一致,显然也是一次源端口耗尽导致的压测问题。

当然,用sysctl也一样可以查看这个范围:

sysctl net.ipv4.ip_local_port_range

不过你可能会问:“难道压测中的网络排查,除了包量和源端口,就没有别的问题了吗?”

我们来看一个不一样的案例。

案例4:压测遇到connection reset by peer

这次的场景是一个应用团队用netty http client去调用一个VIP做压测。具体来说是9台客户端机器,向同一个LB VIP发起大量的请求。

结果遇到了connection reset by peer。蹊跷的是,这个报错是零零星星出现的。而像之前的例子,如果真的源端口用尽了,那么接下来一段时间内,这些请求都会因为本地没有源端口可用而宣告失败,也就是报错会是大面积出现,而不会零星出现。

图片

从图上看,报错数量不超过50个,主要集中在11点20分到11点35分这个时间段,这正是压测的时段。因为报错只有50来个,这相比于这次压测发起的成千上万的请求来说,是很小的比例了。

补充:Y轴就是报错的个数,最高值是10,我们把几个柱体的高度加起来,就是报错的总数量了。

我们了解到,压测期间每个客户端的请求频率是700TPS,所以9台客户端一共会发起6300TPS的请求量。这个问题诡异的地方就是,大部分的请求都能得到正确及时的回复,但是隔了两三分钟,就会出现几次这种connection reset by peer的问题。

那我们需要理解一下,为什么会reset

我们在做网络排查的时候,如果在Wireshark里看到TCP RST,往往会觉得它不是一个好的征兆。确实,有时候是RST引起了故障,有时候又是网络故障迫使TCP用RST来结束连接。无论RST是因还是果,它总是跟问题本身逃不脱关系。

那干脆在TCP里取消RST,是不是很多问题就会被解决呢?当然不是,RST在TCP里面是一个非常必要的组成部分。没有RST,其实就没有“坏”的结束,也就没有“好”的开始。

大体上,TCP RST的原因可以分为这么几个大类:

而这次案例里面的情况,就符合第一种。这是因为:

你有没有发现问题?这两个idle timeout值一边大一边小,配合不当,就会出现下面这样的问题:

某条TCP连接中完成一次HTTP请求和响应之后,连接没有被关闭。过了180秒,LB这一侧的连接被回收了,但客户端那边还没到360秒,所以还认为这条连接是活着的,于是在180秒之后发起了一次请求。报文到达LB,后者在连接表里没有查到这条连接,于是回复了RST。

根因清楚了,解决起来异常简单:把客户端的Idle timeout参数,从原先的360秒,改成比LB的180秒更低的值就好了。这次改到了120秒,RST就完全消失了。

你看,虽然在案例2里,我们知道了压测最好使用长连接,这样可以避免源端口耗尽的问题。但是不等于你设置了长连接就一劳永逸了,还需要考虑idle timeout的问题。这次压测的关键在于:要确保长连接本身是真正有效的,你需要确保客户端的idle timeout小于服务端的idle timeout,这样才能避免连接失效导致的RST

补充:这个库是Ractor-Netty,idle timeout对应的是evictInBackground + maxLifeTime

实验

回顾完这些压测案例,你可能会发现其实难度并不太大,是不是也跃跃欲试了呢?接下来,我们就用ab来做个小实验,这次还捎带一个小任务:控制TIME_WAIT状态的连接的数量

在案例2和3里面,源端口耗尽的问题,本质是TIME_WAIT需要停留2MSL的时长。要解决TIME_WAIT连接过多的问题,方法有很多。这次我们只实验其中的一种方法,就是修改 net.ipv4.tcp_max_tw_buckets 这个内核参数。

它的默认值为16384,改为更小的值后,超过这个数值的TIME_WAIT连接会被清除,这样,TIME_WAIT连接数就被控制住了。

这个实验在本地就可以完成,比如在你的笔记本上安装VirtualBox,然后在这个虚拟机里面完成实验。

步骤一:安装Nginx和Apache ab。

apt install nginx
apt install apache2-utils

步骤二:启动ab测试,执行下面的命令。

ab -c 500 -n 10000 http://localhost/

步骤三:观察TCP连接的各种状态的统计数。

ss -ant | awk '{++s[$1]}END{for(k in s) print k,s[k]}'

此时你应该会看到1万多个TIME_WAIT的连接,然后等待2分钟后继续步骤四。

步骤四:修改内核参数 tcp_max_tw_buckets 为100。

sysctl net.ipv4.tcp_max_tw_buckets=100

步骤五:再次ab测试。

ab -c 500 -n 10000 http://localhost/

步骤六:再次观察TCP连接的各种状态的统计数。

ss -ant | awk '{++s[$1]}END{for(k in s) print k,s[k]}'

此时TIME_WAIT应该只有100了。

当然,TIME_WAIT本身被设计出来是有原因的,建议你在改动它之前,把自己的场景想清楚,然后再改不迟。

小结

这节课,我们回顾了几个典型的压力测试场景中的网络问题,你需要重点掌握以下这些知识点。

关于压测指标和工具。轻量级的工具是ab,它十分方便,而且也可以发起大量的请求,在简单的压测场景下很有用。比如下面的命令:

ab -k -c 100 -n 10000 目标URL

重量级工具是LoadRunner和JMeter,它们可以实现复杂的交互行为,也提供更加详细的报告。

在网络性能领域,包量是对PPS的简称,一般用来衡量一台主机的网络处理能力。而评估包量的工具是sar,我们可以运行下面的命令获取到实时的包量值:

sar -n DEV 1 10

关于TCP的知识。这节课里我们也再次温习了TCP相关的知识,包括:

ss -ant | awk '{++s[$1]}END{for(k in s) print k,s[k]}'

或者:

netstat -ant |awk '{++a[$6]} END{for (i in a) print i, a[i]}'
sysctl net.ipv4.ip_local_port_range

思考题

最后还是再给你留两道思考题:

欢迎在留言区分享你的答案,也欢迎你把今天的内容分享给更多的朋友。

附录

抓包示例文件:https://gitee.com/steelvictor/network-analysis/tree/master/22

评论