性能常识 数据为什么会走丢了呢?

CKL的思考 · 2022年03月14日 · 最后由 huangshengnan 回复于 2022年03月16日 · 3277 次阅读

这篇文章的内容写于 2016 年左右,最近在整理材料时翻出来,还是能感觉到当时对于性能测试的热爱,现在都好久没做性能测试了。来看看当年的个人是如何定位问题的吧,也许对于现在做性能测试的同学能带来一些启示。

01 背景介绍

当时接手了一个内部 IM 系统的性能测试,以 Netty 为底层框架,做二次开发,研发属于自己的私有协议,用于内部通信系统的使用。当时处于系统架构验证阶段,还没有业务实现(谁说性能一定要在业务稳定后再测试的?)。在测试一个推送协议时,发现压力机上传输出的数据假如有 10W 条,那么最终入库的时候只有 6W 条左右的数据。中间过程没有发生任何错误。由于这是一个不需要返回的过程(只负责推送,不确认服务器是否收到,不要问为什么,问就是开发这么设计的),所以无法做检查点。那么这些丢失的数据去哪了?

02 排察过程

既然是服务端的数据少了。那么看下服务端收到了多少条数据吧,通过统计服务器日志中的信息,确认服务端确实只接收到了 6W 多的数据:

这里有 12W,是因为这条信息会打印两次,所以实现收的只有 6W 多。既然系统收到的是这么多,是不是系统程序弄丢的?但是不能直接和开发说你们的程序有问题吧,Netty 的框架还是比较成熟的,不太可能会有这样的问题。首先先确认下压力机是否真实发送了这么多的数据,对压力机的网卡做了如下监控:

可以看出,通过压力机网卡的包数量与 LR(当年 Jmeter 还不流行)上显示的数据基本一致(有误差是因为两边启动不同步,且还有其它因素的干扰,但可以忽略)。也就证明了发送端是没有问题的。

和开发沟通,他们还是感觉不是程序的问题,还是那句话,Netty 的框架还是比较成熟的,百度了一下,网上也没有相关的 BUG 说明。那么就继续找问题吧。
数据会不会在网络传输上丢失了?因为是在局域网内,好像也不会存在这么严重的丢包率。为了验证问题,我们监控服务器的网卡,看看是否达到服务器的数据是否准确:

可以看出,到达服务器网卡的数量是对的。数据仿佛真的是应用程序弄丢的。经过反复的排察,发现数据总在接近 7W 的时候开始出现这种情况,之前都是正常的。是哪里有这个限制呢?

03 深入思考

从最开始的脚本开发入手。一个一个环节的分析。首先是性能测试脚本的排查。使用的是 LR 的 Socket 协议,使用长连接,来发送报文,完成后关闭连接。这个看起来也是没错的。在测试其它协议的时候也是正常的(比用户认证,点对点消息推送等,没出现过类似的问题。虽然由于性能问题,TPS 不是很乐观,但是数据是不会丢的)。是 Netty 框架的问题么?好像也不是,否则网上应该能找到相关的资料,总不可能我们是第一次遇到的吧?

于是把这个推送的功能单独拿出来分析。经过一翻讨论和对比,发现推送这个功能最特殊的地方就是它不需要返回报文来确认结果,其它功能都需要返回一个 ACK 包来确认其结果,会不会是这个地方有我们没注意的东西呢?

04 寻找资料

于是我们开始各种找资料,学习 TCP/IP 协议以及与之相关的网卡的工作原理。因为都是较为底层的东西 。最终确认了问题的所在。TCP 对于流量的控制机制导致的问题。相关资料如下:

TCP 中采用滑动窗口来进行传输控制,滑动窗口的大小意味着接收方还有多大的缓冲区可以用于接收数据。发送方可以通过滑动窗口的大小来确定应该发送多少字节的数据。

TCP 连接阶段,双方协商窗口尺寸,同时接收方预留数据缓存区;发送方根据协商的结果,发送符合窗口尺寸的数据字节流,并等待对方的确认;发送方根据确认信息,改变窗口的尺寸,增加或者减少发送未得到确认的字节流中的字节数。调整过程包括:如果出现发送拥塞,发送窗口缩小为原来的一半,同时将超时重传的时间间隔扩大一倍。

为什么要有滑动窗口?在英特网中,可能同时存在着数百万条 TCP 连接。如果这些连接同时无节制的发送数据包,那么整个网络都会被堵死,没有数据包能到达目的地。

因此 TCP 需要根据网络状况,每次发送若干数据包。

简单来说,网卡在控制流量的时候,会有一个缓存机制,当接收的量大于传送给应用程的量时,网卡会把数据缓冲在一个类似于缓冲区的地方,利用滑动窗口机制来控制传输。当连接断开后,由于物理链路的丢失,这部分 “缓冲” 数据也会跟着消失。在上层应用其实这种方法很常见,比如各类中间件的队列,本质上是一样的。

05 验证并解决

明白了网卡的工作原理后,就可以解释上面的现象了。由于我们大量发送数据,服务器的网卡在接收这些数据后,往应用层传送数据时,启动了滑动窗口机制,对一部分数据做了 “缓冲”。这些数据是全部达到网卡这一层的,所以我们的监控也没有问题,因为数据确实是到达了网卡。但是由于这个功能不需要确认机制,客户端发送完数据后,就关闭了连接。导致网卡 “缓冲区” 里的数据不知道往哪送了。所以数据就丢了,因为数据也没有到达应用层,所以应用层日志时也没有错误。

在修改过内核参数后(调整滑动窗口的大小),经验证,数据丢失的量会随着参数的变化而变化。证明了我们的猜想是正确的。

解决办法:客户端发送完消息后,脚本不马上结束,保持一段时间的链接,让服务端网卡里缓冲区的数据 “知道” 往哪走,问题就消失了。同时告诉开发这个机制,在后续的业务开发中,需要特别注意这种场景。

06 小结

在这个问题的处理上可以看出,性能测试涉及到方方面面,不但要懂软件,还要懂硬件。在解决了这个问题之后,我们就能更清楚的知道网络是怎么处理等待的,为什么带宽满了后响应时间会变长。对这些问题有了更深的了解。

另:为什么其它功能不会出现此问题。是因为其它的情况下,我们需要一个 ACK 的返回包来确认我们的结果。在没有得到结果之前,连接是会一直保持着的。所以在测试 Socket 之上的协议时,基本不会出现此问题,但是越底层的协议越需要注意这些细节。同时这个问题也可以解释为什么在同样的压力下,网络的好坏也会影响响应时间(网络较差时,数据在 “缓冲区” 时的时间会越长,容易超时,导致数连断开,客户端产生超时的现象)。

PS:性能测试其实是个很锻炼个人思维的测试活动,需要有足够的知识量和分析问题的能力,很庆幸能在工作前期就有机会锻炼自己的逻辑思维,感谢天胜和 Mike。
做性能,如果你只关心如何使用工具,很大概率会跑偏了。

原文链接:https://mp.weixin.qq.com/s/Aosgl0IY79dyMGJboYTA2A

共收到 12 条回复 时间 点赞

当时处于系统架构验证阶段,还没有业务实现(谁说性能一定要在业务稳定后再测试的?)

确认这个阶段可以做性能测试吗?

测试对应的性能测试感觉和开发对应的后端开发一样, 都是俩四字的名字. 都代表了较低的门槛和无限高的上限. 名字表达的意思太范围笼统了.

Thirty-Thirty 回复

当然啦,对于一些技术选型,基础底层框架的性能验证,和业务无关。

很不错的文章,先增加一些场景细化复现场景,然后查资料了解黑盒子背后的原理,提出假设 + 验证确认,点赞!

话说,这种为了高性能服务,并且发了后不用管的,用 UDP 应该比 TCP 更合适,当时开发为啥不考虑直接用 UDP ,这个有和开发探讨过么?

CKL的思考 回复

业务实现后,是不是还需要做整体性能测试?两次测试结果会一致吗?应该以哪次结果为准?

找到了两篇相关文章:

没想到协议栈底层原来会这样处理数据,send函数只是将数据写到了内核缓冲区,所以send函数的返回值不代表对方已经收到多少数据,挺有意思。

陈恒捷 回复

😅 当时没想到过,还是懂的少

Thirty-Thirty 回复

还是需要的啊,随着业务代码的加入,性能肯定会有一些损耗,以最终业务场景下的性能测试为准。框架级的性能测试主要是验证框架本身的性能够用,否则到业务代码引入后,发现是框架的性能问题,改起来就很麻烦了。

1、性能测试中常见的 IO 问题。性能测试过程中,除了常见的 CPU、Memory 资源占用,如果加入 IO、带宽的观察,这类问题的发现就相对容易得多。
2、很多初学者以为性能测试就是拿工具发送压力(跟一部分人理解的 “API 测试就是发送请求” 一样),其实事前有很多知识需要了解和准备, 例如端口范围、文件/TCP 链接最大限制、TCP 超时时间、存活间隔... 当然这也是系统/网络调优的一部分
3、性能测试什么时候开始做,跟测试的对象和目的有关。可以只对单个协议进行测试,可以只对单个中间件进行测试,可以只对某段代码进行测试,当然也可以对业务场景进行测试

CKL的思考 回复

多谢释疑, 😃

厉害了,无论如何我都不会想到问题出在这。😀

学习了!
当发生 TCP 堵塞时,数据会先存在接收方的接收缓存区中,这时如果 TCP 连接断开竟然会导致缓存区数据丢失。
至于数据丢失的原因,应该是 TCP 连接断开时,接收方的 socket 套接字(存放 TCP4 元组、程序信息)也会被注销,导致缓存数据不知道该被发往哪个程序。

CKL的思考 构建性能测试知识体系 中提及了此贴 05月05日 14:49
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册