在最近的工作中需要对同一个域名下的源站同时发起多次请求,有时甚至达到了6000次,发生了很严重的性能问题,追查了下原因是被浏览器(Chrome)stalled
了,因为浏览器只支持对同一个域名下保持6个连接,拥有更多连接时,就只能被挂起,直到上一个连接完成被复用。所以同时发起6000次请求,100ms的耗时也将会导致100秒的处理时间,在实际过程中复用连接将会导致更高的耗时。最终的解决方案当然是要求对方接口人改批量接口啦,皆大欢喜。
在追查的过程中也发现了前人关于请求被挂起导致加载缓慢的分析,最终追查到chrome日志,其中关于网络连接引用到了TCP Reset
的文章,对自己有一些启发,这里翻译并总结下。
在网络服务中,经常性会遇到reset导致的连接断开问题,每一次追查问题可能都会让人大为恼火,但是首先我们需要了解reset是怎么工作的。reset事实上是一件好事情,它关闭那些已经没有必要继续存在的连接,举个例子,我们的程序建立了许多的TCP短连接,这些连接在断开时将会保持一段时间的Time Wait State
状态,默认4分钟,用于保证连接能够被正确的断开,但是当我并不想这么做,想降低资源的消耗快速重用这些端口和资源时,就需要使用reset来重置这些连接了。从这里我们也能看出,重置实质上就是强制断开连接。
三次握手
首先来说一下具体的TCP连接,老生常谈的三次握手,它适用于基于IP协议的稳定网络传输,保证信息是准确无误的。当一个节点A想要跟另外一个网络节点B通信时,A节点会先发送一个同步包Syn包
,这个包将会包含建联需要的基本信息,如源IP和端口以及目的IP和端口,以及序列值等信息。
如上面的包可以看到TCP:Flags=......S
字段,这标识着这是一个Syn包,里面同时包含着Source IP, SrcPort, Destination IP, DstPort
信息用于建联,还有内容长度以及序列值等信息。这里的445端口是常用的SMB直连端口,为了本次能够正确建联服务端应该监听这个端口并捕获这个Syn包。
第二个包是Ack, Syn包
,代表着服务端接收到了第一个Syn包
并发送了自己的Syn包
,这两个动作发生在同一个包里,此时的包里可以看到源站IP和端口已经与目的IP和端口的位置互换了。
最后一个包是客户端确认接收到了服务端的Syn包
并完成了本次建联。
上面就是大家常说的三次握手的实际传输文本,现在两个节点已经建联并能够进行稳定的数据传输了。这个稳定是对应于只基于IP协议的传输无法确认收到的是否是正确的数据。
Time Wait State
之前提到了Time Wait State
,它的存在有什么意义呢?当三次握手建立TCP连接后,我们还需要四次分手来断开连接,其中的包与上面类似,过程为:
- 客户端A发送完了所有数据,向服务端B发送
FIN包
来请求断开,A进入FIN_WAIT_1
状态。 - 服务端B发送
Ack包
给客户端A,表示收到了断开请求,A进入FIN_WAIT_2
状态。 - 当服务端B也发送完了自己的所有数据,向客户端A发送
FIN包
请求断开,B进入LAST_ACK
状态。 - 客户端A收到了服务端B的请求,返回
Ack包
给B,然后进入TIME_WAIT
状态。 - 服务端B收到后,就断开连接,不再确认。当经过2MSL
(Maximum Segment Lifetime) 最大分段寿命
后A没有收到B的任何重传信息,就代表连接被正常断开了,A此时也断开连接。
其中的MSL最大段寿命是一个TCP分段可以存在于互联网系统中的最大时间,它通常被定义为两分钟长。保持2MSL的Time Wait State保证了这个连接的杂散数据能够被正确传递到并且连接相关的资源被正确释放。比如步骤4中A的Ack包并没有被接收到,就会被B的重传机制要求重传,重传成功后才可以正确断开连接。
Reset
Reset是立即断开TCP连接,释放连接占用的资源并使得系统可以再次使用这些资源,如客户端的端口号和服务端的fd(文件描述符)。
下面讲几个常见的Reset的场景,用于了解具体的操作。
SMB Reset
从Windows 2000开始,操作系统更希望监听445端口而不是139端口来提供SMB服务,为了向下兼容,用户需要同时发起两个端口的Syn
包,如果服务端也同时监听了两个端口,那么将会针对这两个请求返回两个Ack + Syn
包。此时客户端拿到两个响应,将会给更偏爱的端口返回最后一次Syn
包,同时对另一个端口进行Reset操作,如下图所示:
Ack Reset
对于一个Syn
包响应Ack Reset
包,是在服务端接收到了客户端的建联请求,但是无法在请求的端口进行建联的场景操作,原因可能是:
- 请求建联的服务端并没有监听这个端口。
- 一些原因导致服务端无法在这个端口建联成功,如资源被耗尽,从而无法建立起新的连接。
在一些设备上如果没有监听该端口,请求将会被默认丢弃,而不是返回Ack + Reset
包,这是出于安全性的考虑,比如防火墙。
无响应导致的Reset
在三次握手建联之后,当一个网络传输包传输失败(没有接收到该包的ack包超时)时,会进行重传,并尝试等待一段时间ack包,当重传五次依然失败,就会reset该连接。这里的重传次数可以设置,默认为5。reset的原因是我们认为此时在两个网络节点间或希望发送ack包的节点上发生了问题,这也意味着本次连接变得不再有效。这里需要注意几点:
- 重传指的是同一个包重传5次。
- 重传的包之前的包传输不受影响。
- 迟到的ack不会导致reset,迟到的包会在后续超时重传时成功被确认。
对用于TCP建联的Syn
包重传的次数限制可以通过TCP-MaxConnectRetransmission
字段设置,默认为2。
应用程序Reset
许多应用程序也会Reset连接,这些很难发现,如果我们排查了网络问题和TCP本身没有重置该连接,以及上面几种情况的话,我们基本可以断定是应用程序重置了该连接。一个很常见的场景,我们会在应用上建立很多的短连接,这样的应用由于保持了太多的Time Wait State
会导致端口资源被耗尽,所以这些等待的连接会被应用程序主动重置。但此时开发者应该明白Time Wait State
存在的意义。
我们可以通过查看应用中关于socket的代码或查看socket日志来确认是否是应用重置了连接。如果三次握手的连接正常关闭,应该是采用FIN
包的四次握手。
还有一些更为罕见的场景是端口被抢占,常出现在系统重启时不同应用抢占同一个端口来进行监听。
网络管理员关注的Reset
来自于网络的reset,听起来并不是那么靠谱,但事实上确实会发生。在建联的两个网络节点之间的网络设备会主动重置该连接,这种问题很难追查,最好的方法是使用traceroot来查看节点之间的连接,并同时对接收方和发送方的数据同时进行捕获。在这种场景下,我们会发现发起reset请求的源ip并不是建联的两个网络节点的任何一个,发生这种场景的原因有很多,比如交换机重启,网络设备故障等。有时候我们甚至会发现中间的设备对双方都发起了reset请求。
重用端口
如果一个端口已经处于Time Wait State
,这时希望复用源端口和目的端口发送Syn
包,根据RFC 1122协议,是被允许的,但需要注意该状态的含义(用于上个连接最后的FIN包被正确接收),此时的包seq
值需要大于上次连接的最后一个包,否则将会被reset。
总结
TCP重置是一个好的事情,如果没有reset,我们将会遇到各种各样的TCP网络连接问题。需要注意的是导致reset的原因是多种多样的,不仅仅是两端的节点还有可能是应用程序,追查问题时最重要的是查看包的状态以及其重传。
参考文献
- Reset来自哪里:https://blogs.technet.microso...
- TCP的三次握手和四次分手:https://segmentfault.com/a/11...
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。