概述
背景
一个项目对接第三方接口数据。对方是TCP接口,发送数据频率很高。平均2毫秒发送三四千个字节。由于TCP协议的粘包拆包问题,我这里接收到的数据需要对粘包拆包按照对方数据的格式进行处理。对接了一段时间后发现,TCP连接会自动断开。由于我这里做了断开重连的逻辑。所以最终的现象就是一直在断开,重连,再断开,再重连。。。
向数据提供方咨询,数据提供方给出的反馈是数据消费不过来,造成数据积压后,他们的程序就会主动断开TCP连接。通过日志发现,我这里确实在断开前,消费数据出现了延迟。通过日志观察发现,接收数据的时候,比发送数据的时间慢了几秒,由于数据量很大,所以造成了积压,数据提供方就断开了连接。
问题分析
于是,问题的焦点就到了为何我的程序消费不过来数据呢?首先想到的就是我写的程序性能有问题,导致无法消费平均2毫秒产生的三四千个字节这个数据频率。由于粘包拆包程序是我自己自定义处理的,于是,我怀疑是自己的处理逻辑性能差。
通过研究发现,netty框架提供了针对于TCP粘包拆包的解析类。于是,我引入了netty框架,使用netty框架提供的LengthFieldBasedFrameDecoder解析器对接收到的数据进行处理。发现还是会出现延迟,消费不过来的现象发生。
由于netty接收数据后,对数据进行处理,默认是使用单线程来完成的。即接收TCP数据和处理粘包拆包是在一个线程中完成的。这是不是影响了消费的速率呢?于是,我写了两个线程,一个用于接收数据,然后把接收到的数据存入一个集合容器中。紧接着继续拉取下一批数据。另一个线程用于处理集合容器中的数据。这种解决方案依旧不行,还是延迟消费数据。
难道是接收了数据,往集合容器中存放这个操作,也影响了性能,使其消费不过来了?于是,我把程序改成了只接收数据,然后打印一个接收字节数的日志,其余的操作再也没有了。尝试这种操作是否可以不自动断开TCP连接。因为不自动断开TCP连接证明数据消费没有延迟。
令人费解的事情发生了,纯接收数据,就打印个接收字节数的日志,还是会自动断开TCP连接。这就表明,无论我怎么优化代码,它都会延迟消费。这就不是我代码的问题而引起的消费延迟了。因为我肯定要去接收TCP的数据。而现在纯接收数据,不做任何处理,就发生了延迟了。这说明已经不是我程序代码的问题了。
起初怀疑的对象是操作系统是不是消费不过来了?一定是操作系统先从网络中拉取数据,我的应用程序再从操纵系统中获取数据的。如果操作系统这个层面就消费不过来了,那么我的程序肯定也消费不过来。因为我用的服务器是Window Server系统,所以,将程序部署到了一台linux服务器上进行纯接收数据,不做任何处理的测试。最后发现,依旧会自动断开TCP连接。
服务器这个猜测也失败了。因为数据提供方也会消费这些数据,而他们的系统没有出现过这种自动断开的情况。说明不是操作系统消费不过来了。
于是,我把目光转到了接收字节数量的日志上。发现大多数日志输出的都是一次拉取1460个字节。还发现会有一次拉取几万个字节的情况出现。而最大的拉取量是65536个字节,不会比这个字节再大了。即使我在程序里定义的读取数据的byte数组的长度是10万,程序最多也是拉取65536个字节。
为了研究这些数字代表的含义,特意研究了TCP协议。并将所学知识记录下来:
tcp协议知识详解
这里解释一下上述日志中几个数字的含义。首先,1460是以太网的MTU是1500,去掉40个字节的TCP头和IP头,业务数据的长度就是1460个字节。即一个包最长的业务数据就是1460。而程序每次大部分都读取1460个字节。证明滑动窗口里的数据是没有积压的。也就是说当程序读1460个字节的时候,说明是消费的过来的。因为如果数据积压了,那么必定在滑动窗口里有很多个字节,甚至把滑动窗口填满。那么程序单次拉取字节数,就不可能是1460个,而是比1460个要多。所以当程序每次拉取也是1460的时候,说明发一次数据,就可以消费一次数据,是不存在延迟现象的。
那么日志中小于1460个数据拉取又是如何发生的呢?加入TCP的发送方一次发送了2000个字节的业务数据,而在物理层的以太网中,只能发送1460个字节。那么就会对数据进行分片。前1460个字节为一个包,发送获取了,剩余的540个字节是另一个包发送过去。这就造成了少于1460个字节的拉取情况出现。其本质也是发送了多少数据就拉取了多少数据,不存在延迟现象。
那么延迟到底是怎么发生的呢?这就要分析那些一次拉取上万个字节的数据的情况是如何发生的了。通过观察发现,每次拉取上万个字节的数据,日志都会卡顿几十毫秒甚至更长才会拉取一次数据。由于停顿的这几十毫秒一直有数据发送过来,所以接收滑动窗口的数据就会一直增加。当停顿结束后再次拉取数据时,就从滑动窗口里拉取了更多的数据回来。所以就有上万个字节的数据了。那程序为何会停顿,不拉取数据了呢?
通过wireshark抓包工具发现,当TCP出现丢包时,程序就不拉取数据了。因为当TCP丢包后,由于滑动窗口的存在,在滑动窗口范围内,还会继续接收发送过来的数据。但是因为丢包了,所以应用程序就不会再消费数据了。此时,我这里的操作系统会给发送方反馈丢包信息(TCP dup ack),默认情况下是发送三次TCP dup ack后,发送方就会立马重传丢包的数据。但是观察wireshark发现,丢包后,重传50多次TCP dup ack后,发送方才会返回丢包的数据。这就说明,TCP dup ack反馈消息也在一直丢失,直到发送了50多次后,才收到3条TCP dup ack信息。当然,如果一直收不到TCP dup ack信息,那么只有等到超时时间后,发送方主动再次发送丢包数据了。可见,这里的网络环境是有多差。
那这就分析出了消费延迟的根本问题所在。因为TCP发生了丢包,导致了应用程序的停顿,无法读取TCP丢包后的数据。而丢包的反馈TCP dup ack又无法第一时间被发送方接收到,所以接收方应用程序卡顿的时间就会变长。而TCP产生数据的频率又是很高的,所以在停顿的这个期间,就产生了很多的数据。当丢包的数据被发送方发送过来后,应用程序从丢包位置读取数据,而此时丢包的位置后面已经产生了大量的数据,所以造成了消费的延迟。
分析到这里,问题的本质似乎找到了。但是还有一个现象不可忽略。那就是每次TCP主动断开的位置,都不是程序停顿的位置,也不是一次拉取几万个字节的位置。而是一次拉取1460个字节的位置。而上面我们又分析道,一次拉取1460个字节,说明消费是能跟的上的,没有积压数据。那为何还会消费延迟呢?
这就涉及到了TCP网络拥堵的处理机制。只要发生了丢包,TCP就认为当前的网络环境不佳,TCP发送方会根据自己的机制,主动减少发送量,避免对网络造成更大的压力。当发生丢包后,应用程序停顿了几秒。但是停顿结束后,会有几次拉取几万个字节数据的情况,这几次拉取,就会赶上积压的数据的消费速率。也就是这里会有延迟,但是拉取几次大数据量后,消费就赶上来了。而TCP发送方认为当前的网络环境不佳,所以发送方主动减少了发送的吞吐量。这就造成了发送方产生了大量的数据,但是发送的数据量很小。这就造成了发送方数据的积压。当积压到一定程度后,发送方的应用程序就断开了连接。
总结
综上所述,发生消费延迟问题,是因为TCP频繁丢包,触发了TCP的拥堵处理机制,导致发送方发送量减少,数据产生了积压,造成了消费的延迟。
上面还有一点提到数据积压后,最大的拉取量是65536,这是因为操作系统所能承受的单次数据拉取量,就是65535个,如果频繁的接收大于65535字节的数据,会使操作系统崩溃。所以这个值是操作系统进行的限制。而我上面又说到,数据提供方也在消费这个数据,但是他们没有出现过延迟。是因为他们在本地进行的数据消费。即数据发送和接收都是一个服务器。这种情况是不会走物理层的,也不会经过网卡,所以单次传输就没有1460这个限制了,就升级到了65535这个限制。而且本地传输也不会出现丢包现象。所以他们消费没出现延迟。
感悟
TCP为了保证数据的安全性,发生丢包后做出的一系列处理会影响性能。而在大数量的情况下,会把性能的影响放大。所以,在选择协议时,要综合分析,选择最合适的协议。例如本次的业务场景,完全不适合用TCP协议,而适合用UDP协议。因为接收到数据后,也会根据逻辑过滤掉大多数的数据,而是保留一部分数据。所以对数据的安全性要求不是那么高,使用UDP协议,允许丢失一部分数据,就不会出现这种消费延迟的问题了。
抓包记录
记录一下使用wireshark抓包发现问题的过程。
首先,选择要监听的网卡,然后输入过滤器,过滤ip(host xxx),只查看发送数据的ip的TCP协议。
然后,进入网络监控页面,在顶部的过滤器中,输入tcp,表示只监控tcp协议。
第二列的TIme字段,默认不是yyyy-MM-dd HH:mm:ss格式的,需要手动调成此格式,方便查看数据发送的时间:
然后,开始分析接收到的具体数据:
如上图所示,TCP Previous segment not captured就代表接收方收到了后面的数据,但是前面的数据还没收到。即前面的数据发生了丢包。这个提示是wireshark自己分析给出的提示,而且还标黑了,说明接收数据发生了丢包。
发生丢包后,接收方就给发送方发送TCP Dup Ack信息,告诉接收方丢包了。默认情况是发送三次,接收方就会把丢包数据返回,但是如上图所示,发送了11次,还没收到丢包数据。
直到发送了58次以后,才收到发送方返回的 TCP Fast Retransmission信息,这表明是收到了丢包的数据。
此外,在统计–>专家信息中,wireshark也统计了各种情况发生的次数,如下图:
第一行就是丢包的次数,可见,发生了85次丢包。丢包现象很严重。
最后
以上就是冷酷烧鹅为你收集整理的记一次TCP丢包带来的重大性能问题的全部内容,希望文章能够帮你解决记一次TCP丢包带来的重大性能问题所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复