这篇文章的内容写于 2016 年左右,最近在整理材料时翻出来,还是能感觉到当时对于性能测试的热爱,现在都好久没做性能测试了。来看看当年的个人是如何定位问题的吧,也许对于现在做性能测试的同学能带来一些启示。
当时接手了一个内部 IM 系统的性能测试,以 Netty 为底层框架,做二次开发,研发属于自己的私有协议,用于内部通信系统的使用。当时处于系统架构验证阶段,还没有业务实现(谁说性能一定要在业务稳定后再测试的?)。在测试一个推送协议时,发现压力机上传输出的数据假如有 10W 条,那么最终入库的时候只有 6W 条左右的数据。中间过程没有发生任何错误。由于这是一个不需要返回的过程(只负责推送,不确认服务器是否收到,不要问为什么,问就是开发这么设计的),所以无法做检查点。那么这些丢失的数据去哪了?
既然是服务端的数据少了。那么看下服务端收到了多少条数据吧,通过统计服务器日志中的信息,确认服务端确实只接收到了 6W 多的数据:
这里有 12W,是因为这条信息会打印两次,所以实现收的只有 6W 多。既然系统收到的是这么多,是不是系统程序弄丢的?但是不能直接和开发说你们的程序有问题吧,Netty 的框架还是比较成熟的,不太可能会有这样的问题。首先先确认下压力机是否真实发送了这么多的数据,对压力机的网卡做了如下监控:
可以看出,通过压力机网卡的包数量与 LR(当年 Jmeter 还不流行)上显示的数据基本一致(有误差是因为两边启动不同步,且还有其它因素的干扰,但可以忽略)。也就证明了发送端是没有问题的。
和开发沟通,他们还是感觉不是程序的问题,还是那句话,Netty 的框架还是比较成熟的,百度了一下,网上也没有相关的 BUG 说明。那么就继续找问题吧。
数据会不会在网络传输上丢失了?因为是在局域网内,好像也不会存在这么严重的丢包率。为了验证问题,我们监控服务器的网卡,看看是否达到服务器的数据是否准确:
可以看出,到达服务器网卡的数量是对的。数据仿佛真的是应用程序弄丢的。经过反复的排察,发现数据总在接近 7W 的时候开始出现这种情况,之前都是正常的。是哪里有这个限制呢?
从最开始的脚本开发入手。一个一个环节的分析。首先是性能测试脚本的排查。使用的是 LR 的 Socket 协议,使用长连接,来发送报文,完成后关闭连接。这个看起来也是没错的。在测试其它协议的时候也是正常的(比用户认证,点对点消息推送等,没出现过类似的问题。虽然由于性能问题,TPS 不是很乐观,但是数据是不会丢的)。是 Netty 框架的问题么?好像也不是,否则网上应该能找到相关的资料,总不可能我们是第一次遇到的吧?
于是把这个推送的功能单独拿出来分析。经过一翻讨论和对比,发现推送这个功能最特殊的地方就是它不需要返回报文来确认结果,其它功能都需要返回一个 ACK 包来确认其结果,会不会是这个地方有我们没注意的东西呢?
于是我们开始各种找资料,学习 TCP/IP 协议以及与之相关的网卡的工作原理。因为都是较为底层的东西 。最终确认了问题的所在。TCP 对于流量的控制机制导致的问题。相关资料如下:
TCP 中采用滑动窗口来进行传输控制,滑动窗口的大小意味着接收方还有多大的缓冲区可以用于接收数据。发送方可以通过滑动窗口的大小来确定应该发送多少字节的数据。
TCP 连接阶段,双方协商窗口尺寸,同时接收方预留数据缓存区;发送方根据协商的结果,发送符合窗口尺寸的数据字节流,并等待对方的确认;发送方根据确认信息,改变窗口的尺寸,增加或者减少发送未得到确认的字节流中的字节数。调整过程包括:如果出现发送拥塞,发送窗口缩小为原来的一半,同时将超时重传的时间间隔扩大一倍。
为什么要有滑动窗口?在英特网中,可能同时存在着数百万条 TCP 连接。如果这些连接同时无节制的发送数据包,那么整个网络都会被堵死,没有数据包能到达目的地。
因此 TCP 需要根据网络状况,每次发送若干数据包。
简单来说,网卡在控制流量的时候,会有一个缓存机制,当接收的量大于传送给应用程的量时,网卡会把数据缓冲在一个类似于缓冲区的地方,利用滑动窗口机制来控制传输。当连接断开后,由于物理链路的丢失,这部分 “缓冲” 数据也会跟着消失。在上层应用其实这种方法很常见,比如各类中间件的队列,本质上是一样的。
明白了网卡的工作原理后,就可以解释上面的现象了。由于我们大量发送数据,服务器的网卡在接收这些数据后,往应用层传送数据时,启动了滑动窗口机制,对一部分数据做了 “缓冲”。这些数据是全部达到网卡这一层的,所以我们的监控也没有问题,因为数据确实是到达了网卡。但是由于这个功能不需要确认机制,客户端发送完数据后,就关闭了连接。导致网卡 “缓冲区” 里的数据不知道往哪送了。所以数据就丢了,因为数据也没有到达应用层,所以应用层日志时也没有错误。
在修改过内核参数后(调整滑动窗口的大小),经验证,数据丢失的量会随着参数的变化而变化。证明了我们的猜想是正确的。
解决办法:客户端发送完消息后,脚本不马上结束,保持一段时间的链接,让服务端网卡里缓冲区的数据 “知道” 往哪走,问题就消失了。同时告诉开发这个机制,在后续的业务开发中,需要特别注意这种场景。
在这个问题的处理上可以看出,性能测试涉及到方方面面,不但要懂软件,还要懂硬件。在解决了这个问题之后,我们就能更清楚的知道网络是怎么处理等待的,为什么带宽满了后响应时间会变长。对这些问题有了更深的了解。
另:为什么其它功能不会出现此问题。是因为其它的情况下,我们需要一个 ACK 的返回包来确认我们的结果。在没有得到结果之前,连接是会一直保持着的。所以在测试 Socket 之上的协议时,基本不会出现此问题,但是越底层的协议越需要注意这些细节。同时这个问题也可以解释为什么在同样的压力下,网络的好坏也会影响响应时间(网络较差时,数据在 “缓冲区” 时的时间会越长,容易超时,导致数连断开,客户端产生超时的现象)。
PS:性能测试其实是个很锻炼个人思维的测试活动,需要有足够的知识量和分析问题的能力,很庆幸能在工作前期就有机会锻炼自己的逻辑思维,感谢天胜和 Mike。
做性能,如果你只关心如何使用工具,很大概率会跑偏了。