今天在测试服务器上压测时发现并发数不在正常范围,使用netstat查看tcp连接存在很多连接的状态为time_wait状态,比established多很多,这是个不正常的现象,time_wait是什么鬼?
netstat -n | awk '/^tcp/ {++state[$NF]} END {for(key in state) print key,"\t",state[key]}'
三次握手
这里还得从tcp的四次分手说起,在这之前,稍微带上三次握手的状态转变:
三次握手不是这篇笔记的重点,这里只扯一个问题:
为什么需要3次握手而不是2次就够?
以下引用谢希仁版《计算机网络》中的话:
“已失效的连接请求报文段”的产生在这样一种情况下:client发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达server.本来这是一个早已失效的报文段.但server收到此失效的连接请求报文段后,就误认为是client再次发出的一个新的连接请求.于是就向client发出确认报文段,同意建立连接.假设不采用“三次握手”,那么只要server发出确认,新的连接就建立了.由于现在client并没有发出建立连接的请求,因此不会理睬server的确认,也不会向server发送数据.但server却以为新的运输连接已经建立,并一直等待client发来数据.这样,server的很多资源就白白浪费掉了.采用“三次握手”的办法可以防止上述现象发生.例如刚才那种情况,client不会向server的确认发出确认.server由于收不到确认,就知道client并没有要求建立连接.”
总结一句话就是:
防止已经失效的请求再次到达服务端导致服务器连接资源浪费.
注:失效的连接请求:若客户端向服务端发送的连接请求丢失,客户端等待应答超时后就会再次发送连接请求,此时,上一个连接请求就是『失效的』
四次分手
tcp的四次分手过程如下:
这里说一下,在实际环境下,FIN-WAIT1的状态其实很难看见,这是因为当有一端请求close时,会很快得到对方ACK,所以FIN-WAIT1有状态很快的会变成FIN-WAIT2,我们这里重点关注Time-Wait状态的2MSL(Maximum Segment Lifetime )
TIME-WAIT
每个具体TCP实现必须选择一个报文段最大生存时间MSL(Maximum Segment Lifetime),它是任何报文段被丢弃前在网络内的最长时间,我们知道这个时间是有限的,因为TCP报文段以IP数据报在网络内传输,而IP数据报则有限制其生存时间的TTL(time to live)字段,TTL为8位,所以最大值为255,我们来看下ip数据报头的格式:
我们姑且认为ip数据报在网络中每经过一个跳数TTL字段-1,最后减到0时则会丢弃这个数据报,(或者这样理解:一个IP数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减1,当此值为0则数据报将被丢弃,同时发送ICMP报文通知源主机)一般情况下,具有最大跳数(255)的分组在网络中的存活时间不可能超过MSL,所以这里设置为2MSL就可以确保TCP的可靠性.
必要性
- 我们没有任何机制保证最后的一个ACK能够正常传输
- 网络上仍然有可能有残余的数据包(wandering duplicates),我们也必须能够正常处理TCP是建立在不可靠网络上的可靠的协议.
1.假设最后一个ACK丢失了,被动关闭一方会重发它的FIN。主动关闭一方必须维持一个有效状态信息(TIMEWAIT状态下维持),以便能够重发ACK。如果主动关闭的socket不维持这种状态而进入CLOSED状态,那么主动关闭的socket在处于CLOSED状态时,接收到FIN后将会响应一个RST。被动关闭一方接收到RST后会认为出错了。如果TCP协议想要正常完成必要的操作而终止双方的数据流传输,就必须完全正确的传输四次握手的四个节,不能有任何的丢失。这就是为什么socket在关闭后,仍然处于TIME_WAIT状态的第一个原因,因为他要等待以便重发ACK。
2.假设目前连接的通信双方都已经调用了close(),双方同时进入CLOSED的终结状态,而没有走TIME_WAIT状态。会出现如下问题,现在有一个新的连接被建立起来,使用的IP地址与端口与先前的完全相同,后建立的连接是原先连接的一个完全复用。还假定原先的连接中有数据报残存于网络之中,这样新的连接收到的数据报中有可能是先前连接的数据报。为了防止这一点,TCP不允许新连接复用TIME_WAIT状态下的socket。处于TIME_WAIT状态的socket在等待两倍的MSL时间以后(之所以是两倍的MSL,是由于MSL是一个数据报在网络中单向发出到认定丢失的时间,一个数据报有可能在发送途中或是其响应过程中成为残余数据报,确认一个数据报及其响应的丢弃的需要两倍的MSL),将会转变为CLOSED状态。这就意味着,一个成功建立的连接,必然使得先前网络中残余的数据报都丢失了.
负作用
处于Time-Wait状态的连接必须等待2MSL的时间才能变成CLOSED,RFC 793指出MSL时间为2分钟,2MSL就为4分钟,在这个时间内,这个连接处于不可用状态,也就是不能被复用,只能干等着这个时间结束后再新建新的连接,这对应用程序来说是个巨大的浪费,特别是对于短连接类的应用来说,更是性能的一大损耗,那有什么办法来缩短这个时间或者复用这个连接呢?
当然是有的,linux下可通过修改内核的配置来做到快回收重复用time-wait:
1 | vim /etc/sysctl.conf |
内核参数说明:
- net.ipv4.tcp_syncookies = 1 表示开启SYN Cookies.当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭,打开这个syncookies的目的实际上是:“在服务器资源(并非单指端口资源,拒绝服务有很多种资源不足的情况)不足的情况下,尽量不要拒绝TCP的syn(连接)请求,尽量把syn请求缓存起来,留着过会儿有能力的时候处理这些TCP的连接请求。
- net.ipv4.tcp_tw_reuse = 1 表示开启重用.允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭
- net.ipv4.tcp_tw_recycle = 1 表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭.
- net.ipv4.tcp_fin_timeout = 30 表示如果套接字由本端要求关闭,这个参数决定了它保持在FIN-WAIT-2状态的时间.
- net.ipv4.tcp_keepalive_time = 1800 表示当keepalive起用的时候,TCP发送keepalive消息的频度.缺省是2小时,改为20分钟.
- net.ipv4.ip_local_port_range = 1024 65000 表示用于向外连接的端口范围.缺省情况下很小:32768到61000,改为1024到65000.
- net.ipv4.tcp_max_syn_backlog = 8192 表示SYN队列的长度,默认为1024,加大队列长度为8192,可以容纳更多等待连接的网络连接数.
- net.ipv4.tcp_max_tw_buckets = 5000 表示系统同时保持TIME_WAIT套接字的最大数量,如果超过这个数字,TIME_WAIT套接字将立刻被清除并打印警告信息.默认为180000,改为5000.对于Apache、Nginx等服务器,上几行的参数可以很好地减少TIME_WAIT套接字数量
- net.ipv4.route.gc_timeout = 100 路由缓存刷新频率,当一个路由失败后多长时间跳到另一个,默认是300
- net.ipv4.tcp_syn_retries = 1 对于一个新建连接,内核要发送多少个 SYN 连接请求才决定放弃.不应该大于255,默认值是5,对应于180秒左右.
这样一来,状态为time-wait的连接数就刷刷的下来了.