问题现象:
客户端接受用户咨询期间部分用户连接无故断开重连,导致分配到一个新的客服
问题排查
一、发现异常点
- 看到问题发生,第一时间想到去看服务器状态和监控的各项指标
经排查,系统各项基础指标如内存,CPU使用率等都在正常范围内 - 因为是连接断开的问题,所以在基础指标正常之后就想到去看TCP的连接状态信息,发现在ESTABLISHED只有不到200的情况下,TIME_WAIT的数量达到了6W左右的量,网上查了下发现这个比例是很不正常的,所以分析方向指向TCP连接的问题
二、分析为什么
- 分析TIME_WAIT状态产生的原因
TIME_WAIT产生于tcp连接的4次挥手阶段,并且产生在主动断开连接的一方
这里假设是客户端首先发起断开连接的请求,过程如下
a、客户端先发送FIN,进入FIN_WAIT1状态
b、服务端收到FIN,发送ACK,进入CLOSE_WAIT状态,客户端收到这个ACK,进入FIN_WAIT2状态
c、服务端发送FIN,进入LAST_ACK状态
d、客户端收到FIN,发送ACK,进入TIME_WAIT状态,服务端收到ACK,进入CLOSE状态
客户端TIME_WAIT持续2倍MSL时长,在linux体系中大概是60s,转换成CLOSE状态
注:MSL 是Maximum Segment Lifetime英文的缩写,中文可以译为“报文最大生存时间”,MSL指明TCP报文在Internet上最长生存时间,每个具体的TCP实现都必须选择一个确定的MSL值.RFC 1122建议是2分钟,但BSD传统实现采用了30秒.TIME_WAIT 状态最大保持时间是2 * MSL,也就是1-4分钟.在redhat和centos系统中MSL也是采用的30秒
借网上一张简单的图来说明下
-
TIME_WAIT过多的影响
在socket的TIME_WAIT状态结束之前,该socket所占用的本地端口号将一直无法释放。高TCP并发并且采用短连接方式进行通讯的通讯系统在高并发高负载下运行一段时间后,就常常会出现做为客户端的程序无法向服务端建立新的socket连接的情况。此时用“netstat -tanlp”命令查看系统将会发现机器上存在大量处于TIME_WAIT状态的socket连接,并且占用大量的本地端口号。最后,当该机器上的可用本地端口号被占完(或者达到用户可使用的文件句柄上限),而旧的大量处于TIME_WAIT状态的socket尚未被系统回收时,就会出现无法向服务端创建新的socket连接的情况。此时系统几乎停转,空有再好的性能也发挥不出来。
注:这里涉及到一个服务器最多可以达到多少连接上限的问题,由于TCP的要素--四元组(源地址,源端口,目的地址,目的端口)中有一个不同就可以连接起新的连接,所以理论上一个服务器能接受的TCP连接数量貌似是无上限的(不考虑存储连接信息的表资源被用光和其他资源耗尽的情况)————这里后面再深挖一下 -
TIME_WAIT状态是否可以跳过直接关闭连接?
为什么主动关闭的一方不直接进入CLOSED状态,而是进入TIME_WAIT状态,并且停留两倍的MSL时长呢?这是因为TCP是建立在不可靠网络上的可靠的协议。
例子:被动关闭的一方收到主动关闭的一方发出的FIN包后,回应ACK包,同时进入TIME_WAIT状态,但是因为网络原因,主动关闭的一方发送的这个ACK包很可能延迟,从而触发被动连接一方重传FIN包。极端情况下,这一去一回,就是两倍的MSL时长。如果主动关闭的一方跳过TIME_WAIT直接进入CLOSED,或者在TIME_WAIT停留的时长不足两倍的MSL,那么当被动关闭的一方早先发出的延迟包到达后,就可能出现类似下面的问题:
旧的TCP连接已经不存在了,系统此时只能返回RST包
新的TCP连接被建立起来了,延迟包可能干扰新的连接
不管是哪种情况都会让TCP不再可靠,所以TIME_WAIT状态有存在的必要性。那么从这个解释来说,2MSL的时长设定是可以理解的,MSL是报文最大生存时间,如果重新发送,一个FIN+一个ACK,再加上不定期的延迟时间,大致是在2MSL的范围。 -
怎么解决问题
了解了TIME_WAIT产生的原因,发现是正常会产生的,且是不能跳过的一个必要的过程,那么怎么解决也就大致有了方向。
a、修改TIME_WAIT连接状态的上限值
b、启动快速回收机制
c、开启复用机制
d、修改短连接为长连接方式
e、 由客户端来主动断开连接
a方案:修改内核参数
修改/etc/sysctl.conf,net.ipv4.tcp_max_tw_buckets对应的是系统同时保持的TIME_WAIT的最大数量,超过此数量时,系统会立即清理出多余的TIME_WAIT连接,系统日志中会出现TCP: time wait bucket table overflow的警告信息,最终该状态连接不会超出设置的值,net.ipv4.tcp_max_tw_buckets可设置的最大值为262144 (硬件限制)
这种方法虽然可以很快把TIME_WAIT状态数量降低至设定值以下,使用短连接连接方式的高并发状态下,TIME_WAIT产生速度非常快,当TIME_WAIT连接数达到设置值之后系统会以其产生速度相同的速度去销毁正常的TIME_WAIT连接,这时就可能出现前面说过的跳过TIME_WAIT连接状态可能会出现的结果,部分连接异常或者新的连接建立失败。
net.ipv4.tcp_max_tw_buckets的值应该依据官方建议,不宜设置过小
b方案:启用快速回收
什么是快速回收机制?既然前面说了TIME_WAIT等待2*MSL时长是有必要的,怎么又可以快速回收了?
快速回收机制是系统对tcp连接通过一种方式进行快速回收的机制,对应内核参数中的net.ipv4.tcp_tw_recycle,要搞清楚这个参数,就不得不提一下另一个内核参数:net.ipv4.tcp_timestamps
net.ipv4.tcp_timestamps是在RFC 1323中定义的一个TCP选项。
tcp_timestamps的本质是记录数据包的发送时间
TCP作为可靠的传输协议,一个重要的机制就是超时重传。因此如何计算一个准确(合适)的RTO对于TCP性能有着重要的影响。而tcp_timestamp选项正是*主要*为此而设计的。
当timestamp和tw_recycle两个选项同时开启的情况下,开启per-host的PAWS机制。从而能快速回收处于TIME-WAIT状态的TCP流。
PAWS — Protect Againest Wrapped Sequence numbers
目的是解决在高带宽下,TCP序号可能被重复使用而带来的问题。
PAWS同样依赖于timestamp,并且假设在一个TCP流中,按序收到的所有TCP包的timestamp值都是线性递增的。而在正常情况下,每条TCP流按序发送的数据包所带的timestamp值也确实是线性增加的。
如果针对per-host的使用PAWS中的机制,则会解决TIME-WAIT中考虑的上一个流的数据包在下一条流中被当做有效数据包的情况,这样就没有必要等待2*MSL来结束TIME-WAIT了。只要等待足够的RTO,解决好需要重传最后一个ACK的情况就可以了。因此Linux就实现了这样一种机制:
当timestamp和tw_recycle两个选项同时开启的情况下,开启per-host的PAWS机制。从而能快速回收处于TIME-WAIT状态的TCP流。
快速回收有多快?
据百度谷歌得到的数据是700ms即可回收
使用快速回收在某些情况下可以很大程度上缓解TIME_WAIT状态过多的问题,毕竟回收时间由原先的1min变为不到1s。但是PAWS机制开启的情况下有一个无法解决的问题:NAT网络下会出现部分主机无法正常建立连接
NAT网络和PAWS机制的死结
a. 同时开启tcp_timestamp和tcp_tw_recycle会启用TCP/IP协议栈的per-host的PAWS机制
b. 经过同一NAT转换后的来自不同真实client的数据流,在服务端看来是于同一host打交道
c. 虽然经过同一NAT转化,但由于不同真实client会携带各自的timestamp值,因而无法保证整过NAT转化后的数据包携带的timestamp值严格递增
d. 当服务器的per-host PAWS机制被触发后,会丢弃timestamp值不符合递增条件的数据包
造成的直接结果就是连接成功率降低
为什么有人推荐同时开启tcp_timestamp和tcp_tw_recycle呢?
如果去百度TIME_WAIT过多的问题,很多人的博客写的解决方法是开启tcp_tw_recycle,因为同时开启后,能够更快的回收TIME-WAIT状态的socket <== 这也正是PAWS从per-conn在配置后扩展到per-host的目的 ,只可惜虽然逻辑是对的,但是也许是没有考虑到公网广泛存在的NAT机制可能带来的问题。
但是既然有人写了开启tcp_tw_recycle可以解决问题,那么我们不妨继续探究一下
在Linux系统中tcp_timestamp是默认开启的,加上用户手动开启tcp_tw_recycle,就启动了PAWS机制,也就开启了快速回收,这时就会发现TIME_WAIT数量确实降下来了,但是为什么没发现我们上面提到的NAT网络和PAWS机制造成的连接问题呢?--仔细看上面NAT网络和PAWS机制的死结中的B经过同一NAT转换后的来自不同真实client的数据流,在服务端看来是于同一host打交道就可以明白真相,触发该情况需要满足两个条件:
a、是发起请求的主机位于NAT网络中
b、请求主机和被请求的服务端tcp_timestamp都是开启的,且服务端开启了tcp_tw_recycle
所以虽然这个坑很大,但是作为服务端确实不太容易发现,但是有些时候还是发现的,因为当服务端程序向其他服务发起请求,比如数据库连接请求此时的服务端在这个连接中就充当了客户端的角色。
如何避免客户端连接我们的服务器时出现NAT和PAWS冲突而出现的连接失败率高的问题呢?
由于触发该情况的条件是客户端开启tcp_timestamp并且我们服务端同时开启tcp_timestamp和tcp_tw_recycle,所以要避免这种情况就要从这些点出发,所以有两种方式:
a、关闭tcp_tw_recycle,等于是放弃了快速回收TIME_WAIT状态的方案。
b、关闭tcp_timestamp,虽然很多情况下也不会出现问题,但是考虑到tcp_timestamp参数设定的初衷,建议还是使用默认的开启状态(如果作为客户端要连接其他的服务器出现了连接失败,不妨试下关闭tcp_timestamp,毕竟这个时候服务端在别人家,我们没办法改参数)
c方案:开启重用机制
开启重用用机制net.ipv4.tcp_tw_reuse,允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭
要开启重用机制需要依赖tcp_timestamps的功能
重用TIME_WAIT的条件是收到最后一个包后超过1s
官网对这个参数有这样的描述:这个参数从协议角度讲是安全的,而对快速回收的描述是如无特殊需求不建议开启,从这点可以看出开启重用相比开启快速回收要更安全一点
官网手册有一段警告:It should not be changed without advice/request of technical experts.
重用机制的优缺点
优点:配合tcp_timestamps可以在协议上安全的前提下对TIME_WAIT连接用于新的TCP连接,1s相比默认的60s时间还是极大的缩短了
缺点:该机制只对“客户端”有效,即主动发起连接的一方。比如一台web服务器,客户端发来请求时,web服务器时服务端,但web服务器又需要去连接后台数据库,这时候,web服务器又同时作为了客户端,只有主动发起连接一方主动断开所产生的TIME_WAIT该参数才生效
d方案:将连接方式由短连接或者长连接
短连接和长连接工作方式的区别:
短连接
连接->传输数据->关闭连接
HTTP是无状态的,浏览器和服务器每进行一次HTTP操作,就建立一次连接,但任务结束就中断连接。
也可以这样说:短连接是指SOCKET连接后发送后接收完数据后马上断开连接。
长连接
连接->传输数据->保持连接 -> 传输数据-> 。。。 ->关闭连接。
长连接指建立SOCKET连接后不管是否使用都保持连接,但安全性较差。
从区别上可以看出,长连接比短连接从根本上减少了关闭连接的次数,减少了TIME_WAIT状态的产生数量
短连接和长连接使用场景的区别:
长连接多用于操作频繁,点对点的通讯,而且连接数不能太多情况。
每个TCP连接都需要三步握手,这需要时间,
如果每个操作都是先连接,再操作的话那么处理速度会降低很多,
所以每个操作完后都不断开,次处理时直接发送数据包就OK了,
不用建立TCP连接。
而像WEB网站的http服务一般都用短链接,
因为长连接对于服务端来说会耗费一定的资源,
而像WEB网站这么频繁的成千上万甚至上亿客户端的连接用短连接会更省一些资源,
如果用长连接,而且同时有成千上万的用户,
如果每个用户都占用一个连接的话,那可想而知吧。
所以并发量大,但每个用户无需频繁操作情况下需用短连好。
目前Nginx只支持反向代理到upstream下配置的server,不支持直接由proxy_pass指令配置的server,更不支持proxy_pass参数中包含变量的情况。此外,为支持长连接,需要配置使用HTTP1.1协议(虽然HTTP 1.0可通过设置Connection请求头为“keep-alive”来实现长连接,但这并不推荐)。
此外,由于HTTPPROXY模块默认会将反向代理请求的connection头部设置成Close,因此这里也需要清除connection头部(清除头部即不发送该头部,在HTTP 1.1中默认为长连接)。
关于长连接的内容另起一篇:长连接
e方案:由客户端来断开连接(假想,未研究方法)
这样客户端作为发起断开连接的一方,TIME_WAIT就会留在客户端。
对TCP连接谁先断开的一点研究
作者:进击的胖达
链接:https://www.jianshu.com/p/a2938fc35573
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。