计算机网络 TIME_WAIT TCP关闭

在这里插入图片描述

  1. time-wait开始的时间为tcp四次挥手中主动关闭连接方发送完最后一次挥手,也就是ACK=1的信号结束后,主动关闭连接方所处的状态。

然后time-wait的的持续时间为2MSL. MSL是Maximum Segment Lifetime,译为“报文最大生存时间”,可为30s,1min或2min。2msl就是2倍的这个时间。工程上为2min,2msl就是4min。但一般根据实际的网络情况进行确定。

作用1:为了保证客户端发送的最后一个ack报文段能够到达服务器。因为这最后一个ack确认包可能会丢失,然后服务器就会超时重传第三次挥手的fin信息报,然后客户端再重传一次第四次挥手的ack报文。如果没有这2msl,客户端发送完最后一个ack数据报后直接关闭连接,那么就接收不到服务器超时重传的fin信息报(此处应该是客户端收到一个非法的报文段,而返回一个RST的数据报,表明拒绝此次通信,然后双方就产生异常,而不是收不到。),那么服务器就不能按正常步骤进入close状态。那么就会耗费服务器的资源。

作用2: 在第四次挥手后,经过2msl的时间足以让本次连接产生的所有报文段都从网络中消失,这样下一次新的连接中就肯定不会出现旧连接的报文段了。 没有timewait。这次连接中有个迷失在网络中的syn包,然后下次连接又马上开始,下个连接发送syn包,迷失的syn包忽然又到达了对面,所以对面可能同时收到或者不同时间收到请求连接的syn包,然后就出现问题了 。

弊端: 因为客户端使用随机端口来访问服务器,当它主动断开的时候会出现这个状态,比如第一次系统给它分配了一个51000的随机端口访问服务器,然后客户端主动断开了,在2MSL期间,该端口就处于TIME_WAIT状态,如果它再次访问相同的服务器,那么系统会为它再次分配一个随机端口,如果51000端口还处于TIME_WAIT状态,那么这个随机端口就肯定不是51000,如果51000端口不处于TIME_WAIT状态,那么这个随机端口就有可能是51000。所以这个状态在一定期间内对于客户端角色来讲会影响并发量,大量这个TIME_WAIT就导致可用随机端口不断减少。

如何解决

简单来说,就是打开系统的TIMEWAIT重用和快速回收。

发现系统存在大量TIME_WAIT状态的连接,通过调整内核参数解决,

vi /etc/sysctl.conf

编辑文件,加入以下内容:
net.ipv4.tcp_syncookies = 1net.ipv4.tcp_tw_reuse = 1net.ipv4.tcp_tw_recycle = 1net.ipv4.tcp_fin_timeout = 30

然后执行 /sbin/sysctl -p 让参数生效。

net.ipv4.tcp_syncookies = 1 表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭;

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 修改系統默认的 TIMEOUT 时间

网上很多人给出的答案是调整内核参数比如下面的参数,但是这些答案有很多误区,在不同场景下并不一定适用,所以这里先对参数做一下澄清:

net.ipv4.tcp_tw_reuse = 1

表示开启重用。允许将一个处于TIME-WAIT状态的端口重新用于新的TCP连接,默认为0,表示关闭,其防止重复报文的原理也是时间戳,具体看后面。

net.ipv4.tcp_tw_recycle = 1

表示开启TCP连接中TIME-WAIT sockets的快速回收,意思就是系统会保存最近一次该socket连接上的传输报文(包括数据或者仅仅是ACK报文)的时间戳,当相同四元组socket过来的报文的时间戳小于缓存下来的时间戳则丢弃该数据包,并回收这个socket,默认为0,表示关闭。开启这个功能风险有点大,NAT环境可能导致DROP掉SYN包(回复RST),在NAT场景下不要使用。需要注意在Linux内核4.10版本以后该参数就已经被移除了。

net.ipv4.tcp_fin_timeout = 60

这个时间不是修改2MSL的时长,主动关闭连接的一方接收到ACK之后会进入,FIN_WAIT-2状态,然后等待被动关闭一方发送FIN,这个时间是设置主动关闭的一方等待对方发送FIN的最长时长,默认是60秒。在这个状态下端口是不可能被重用的,文件描述符和内存也不会被释放,因为这个阶段被动关闭的一方有可能还有数据要发送,因为对端处于CLOSE_WAIT状态,也就是等待上层应用程序。关于这个的真实含义我希望大家清楚,而且不要调整的太小当然太大也不行,至少在3.10内核版本上这个参数不是调整的TIME_WAIT时长。我的资料查询3.10内核变量定义RedHat官方解释。至于到底如何修改TIME_WAIT的时长,目前没找到可以通过命令或者配置的形式去修改的方式。

net.ipv4.ip_local_port_range = 32768 60999

表示用于外连使用的随机高位端口范围,也就是作为客户端连接其他服务的时候系统从这个范围随机取出一个端口来作为源端口使用来去连接对端服务器,这个范围也就决定了最多主动能同时建立多少个外连。

net.ipv4.tcp_max_tw_buckets = 6000

同时保持TIME_WAIT套接字的最大个数,超过这个数字那么该TIME_WAIT套接字将立刻被释放并在/var/log/message日志中打印警告信息(TCP: time wait bucket table overflow)。这个过多主要是消耗内存,单个TIME_WAIT占用内存非常小,但是多了就不好了,这个主要看内存以及你的服务器是否直接对外。

使用net.ipv4.tcp_tw_reusenet.ipv4.tcp_tw_recycle 的前提是开启时间戳net.ipv4.tcp_timestamps = 1不过这一项默认是开启的。

作为7层的代理的Nginx

在这种场景下首先要搞清楚哪一侧产生TIME_WAIT最多。为什么要看这个,我们知道TIME_WAIT是主动关闭一方具有的状态,但是Nginx作为7层代理对外它是服务器而对内它是客户端(例如,相对于后端的其他Web应用比如Tomcat)。

对外侧(被动连接)

img

ss -nat | grep "TIME-WAIT" | awk '{print $4}' | egrep -w "192.168.71.101:80|192.168.71.101:443" | wc -l

img

作为代理服务器对外提供的端口就是80和443,所以我们针对"TIME-WAIT"状态来进行过滤本地地址和端口而且通过-w进行严格匹配2个条件就是"192.168.71.101:80|192.168.71.101:443",这样就统计出Nginx作为服务器一方的外连TIME-WAIT的数量。

看一下抓包情况(下图是测试环境的包,上图是生产环境的统计)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zMa7YvHS-1612454403601)(https://img2018.cnblogs.com/blog/1448094/201907/1448094-20190706161553039-1238929808.png)]

由于在作为Web代理角色运行的时候为了提高HTTP性能所以Nginx通常会开启Keep-alive来让客户端对TCP连接进行复用,如果客户端在Keep-alive超时内没有进行通信那么当触发超时时服务器就会主动断开连接,也就是上图红色箭头的地方,另外一个情况就是Nginx设置对Keep-alive最大请求数量,意思是改链接在复用的时候可以发送多少次请求,如果到达这个最大请求次数也会断开连接,但无论怎么说这种情况是服务器主动断开所以TIME_WAIT则会出现在服务器上。

对于这种情况的TIME_WAIT通过修改net.ipv4.tcp_tw_reuse无法优化,因为服务器工作在80或者443端口,不存在重复使用或者快速回收的前提。开启net.ipv4.tcp_tw_recycle这个功能倒是还有点意义。

对内侧(主动连接)
ss -nat | grep "TIME-WAIT" | awk '{print $4}' | egrep -w -v "192.168.71.101:80|192.168.71.101:443" | wc -l

img

我们增加一个-v参数来取反,这样就获取了本地地址中不是80和443端口的TIME-WAIT状态数量,那么这个数量就是Nginx作为客户端进行内连后端服务器所产生的。

很明显对内侧的TIME_WAIT明显比对外侧要高,这就是因为Nginx反向代理到后端使用随机端口来主动连接后端服务的固定端口,在短连接的情况下(通常是短连接),Nginx作为主动发起连接的一方会主动断开,所以在业务繁忙的Nginx代理服务器上会看到大量的对内侧的TIME_WAIT。

基于这种情况可以采用net.ipv4.tcp_tw_reusenet.ipv4.tcp_tw_recycle优化方式,因为高位随机端口具备复用的可能。当然至于旧IP分组影响新连接的情况在前面已经说过了其依靠时间戳来做丢弃。具体机制请看后面,现在你只需要知道是依靠时间戳来规避这个问题。

另外net.ipv4.ip_local_port_range参数可以设置一个更大的范围,比如net.ipv4.ip_local_port_range = 2048 65000这就意味着你的可用随机端口多了,端口少我们更多关注与端口复用,端口多其实是不是复用的意义就不是那么大,当然这还得取决于并发量,当然这里也不要死磕,如果你的并发量是100万,你怎么可能指望1台Nginx来抗住流量呢,显然需要构建Nginx集群。

再有net.ipv4.tcp_max_tw_buckets这个参数当主机对外的时候需要调整,如果完全是内网提供服务那么这个值无需关心,它根据系统内存动态生成的,当然你可以修改。在对外的时候主要是简单防止DoS攻击。

net.ipv4.tcp_fin_timeout这个值保持默认60秒或者调整成30秒都可以,主要避免对端上层应用死掉了无法进行正常发送fin,进而长期处在CLOSE_WAIT阶段,这样你自己这段的服务器就被拖住了。

总结

对于TIME_WAIT不要死磕,存在即合理,明明是一个很正常的且保证可靠通信的机制你非要抑制它的产生或者让它快速消失。任何的调整都是双刃剑,就像2台Nginx组成的集群去抗100万并发的流量,你非要去优化TIME_WAIT,你为什么不想想会不会是你Nginx集群规模太小了呢?

作为不会主动进行外连的服务器来说对于TIME_WAIT除了消耗一点内存和CPU资源之外你不必过多关心这个状态。

针对Nginx做反代的场景使用reuse优化一下,另外调大一下高位端口范围,fin_timeout可以设置小一点,至于net.ipv4.tcp_max_tw_buckets保存默认就可以,另外对于net.ipv4.tcp_tw_recycle则放弃使用吧,比较从Linux 4.10以后这个参数也被弃用了参见kernel.org

2MSL和resue或者recycle会不会有冲突

这个问题在TCP上有一个术语缩写是PAWS,全名为PROTECT AGAINST WRAPPED SEQUENCE NUMBERS,也就是防止TCP的Seq序列号反转的机制。

我们上面介绍了2MSL的作用以及减少TIME_WAIT常用措施,但是你想过没有重用TIME_WAIT状态的端口以及快速回收会不会引发收到该相同4元组之前的重复IP报文呢?很显然是有可能的,那么这里就谈谈如何规避。通常2种办法:

  • TCP序列号,也就是Seq位置的数字
  • 时间戳,所以这也是为什么在开启resue和recycle的时候要求开启时间戳功能。

TCP头中的序列号位有长度限制(32位),其最大值为2的32次方个,这就意味着它是循环使用的,也很容易在短时间内完成一个循环(序列号反转),在1Gbps的网络里17秒就可以完成一个循环,所以单纯的通过检查序列号不能完全实现阻挡老IP分组的数据,因为高速网络中这个循环完成的太快,而一个IP分组的最长TTL是2MSL,通常是1分钟,所以最主要还是靠时间戳。

前面我们也几次提到时间戳,比如在reuse和recycle的时候提到会对比时间戳,如果收到的报文时间戳小于最近连接的时间戳就会被丢弃,那么我们如何获取这个时间戳呢?我们先看看它长什么样子:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RUwhvtEw-1612454403607)(https://img2018.cnblogs.com/blog/1448094/201907/1448094-20190706161619284-1188491039.png)]

TSval:发送端时间戳

TSecr:对端回显时间戳

我们看第三行,如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aduBhAIX-1612454403608)(https://img2018.cnblogs.com/blog/1448094/201907/1448094-20190706161628750-1901642841.png)]

这一行是客户端回复ACK给服务器完成三次握手的最后一个阶段,TSval就是客户端的时间戳这个和第一行一样这是因为速度快还没有走完一个时间周期,这一行的TSecr是434971890,这个就是第二行服务器回复SYN时候给客户端发来的服务器的时间戳,这个就叫做回显时间戳。

这个时间戳是一个相对时间戳而不是我们通常理解的绝对时间戳(自1970年1月1日的那种形式),而且你不能把它当做时间来用,在RFC1323中也提到对报文的接收者来讲时间戳可以看做另外一种高阶序列号。

这里就会有一个问题,2个时间戳,一个是自己的,一个是对端的,到底用哪个时间戳来进行比较来确定是否丢弃报文呢?答案是TSval,也就是发送端的时间戳。这样很容易理解,作为主动断开的一方要丢弃的是对端传递过来的重复报文,显然需要用对端的时间戳来判断不可能用自己的时间戳。而且从上图可以看到自己的时间戳和对端的时间戳明显有很大差距,也就是说这个时间戳是通信双方自己生成的。这个时间戳就放在TCP报文的options选项中,如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LXtAREuk-1612454403609)(https://img2018.cnblogs.com/blog/1448094/201907/1448094-20190706161640055-234608219.png)]

可以看到它是options,既然是选项那么就不是必须的,所以这也就是为什么当开启reuse和recycle的时候要求开启这个,因为不开启则无法识别重复的IP分组。

简单原理就是:保存该socket上一次报文的TSval时间戳,如果该socket的4元组被重复利用或者快速回收,那么假如收到了之前连接重复的报文,则比较该报文的时间戳是不是比保存的TSval小,如果小则丢弃。我这里只是简单来说基于时间戳的机制来放置重复报文,整个的PAWS还有其他的原则,具体请查看RFC1323

另外,由于时间戳也是通过一串数字来表示且TCP头的时间戳长度也是32位(每个都是4byes),所以它也会出现循环,时间跳动频率就决定了翻转周期,那这个频率是多少呢,RFC1312中规定建议在1ms到1s之间,这个时间间隔不同系统可能不一样,不过这里内核选项和用户选项的区别:

内核选项,在Linux中cat /boot/config-$(uname -r) | grep -w "CONFIG_HZ"查看,

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TaHpPoTP-1612454403610)(https://img2018.cnblogs.com/blog/1448094/201907/1448094-20190706161650730-326799647.png)]

Jiffies是从计算机启动到现在总共发生多少节拍数,节拍数叫做Tick,Tick是HZ的倒数,如果上所示HZ是1000,每秒发生1000次中断,也就是1毫秒发生一次中断,对应Tick是1ms,也就是每1毫秒Jiffies就加1。当重启电脑的时候Jiffies重置。

用户选项,由于用户空间程序不能直接访问,所以内核还提供了一个USER_HZ来让用户空间程序使用,固定为100,百分之一秒,也就是10毫秒。如何查看呢?getconf CLK_TCK命令:

img

我们从网卡上看也是这个值cat /proc/sys/net/ipv4/neigh/ens33/locktime

img

如果这个间隔是1毫秒,那么时间戳反转一次将是24.8天;如果是10毫秒就是248天,依次类推,但最大不能超过1秒。

2.函数原型:

int listen(int sockfd, int backlog);

作用是用来监听服务器创建的socket的。
第二个参数backlog代表的是相应的socket可以排队的最大连接数。

由于客户端client 发起connect()连接时。需要建立TCP三次握手连接需要时间,所有会有一个队列来存储客户端对服务器发起的connect连接。

  1. 当客户端完成连接后。连接被放到端口的socket的队列里面,等待服务器去响应。
  2. 当服务器调用accept()来获取队列中的连接,这时候就会从队列中移除。
  3. listen()函数的第二个参数就是去设置这个队列的大小的。

backlog的上限值受到系统全局设定的限制。

这个上限存储在:/proc/sys/net/core/somaxconn文件中。

关于backlog大小

典型的服务器程序可以同时服务于多个客户端,服务器调用listen函数声明sockfd处于监听状态,并且最多允许有backlog个tcp连接,当有客户端发起连接时,服务器调用的accept()接受这个连接。

这里写图片描述
(图片来自《linux/unix系统编程手册》)

但是客户端可能会在服务器调用accept之前调用connect,这种情况是有可能发生的,如果此时服务端可能正忙于处理其他客户端,这将产生一个未决连接。系统内核有一个未决连接队列会记录所有未决连接的信息,这样服务器在后面调用accept时就能够处理这些未决连接了,而backlog参数就是用来限制这种未决连接数量的。

如果未决连接队列已经满了,当接收到更多的连接请求就会忽略,也就是说客户端调用connect函数可能会阻塞,直到未决连接队列中的未决连接被accept为止(注意:当一个连接被accept时就会从未决连接队列删除,此时未决连队列有空位了)。

3.正常关闭:四次挥手是可以被感知的。

异常关闭:长连接的环境下,进行一次数据交互后,很长一段时间内无数据交互时,客户端可能意外断电、死机、崩溃、重启,还是中间路由网络无故断开,这些TCP连接并未来得及正常释放,那么,连接的另一方并不知道对端的情况,它会一直维护这个连接,长时间的积累会导致非常多的半打开连接,造成端系统资源的消耗和浪费,且有可能导致在一个无效的数据链路层面发送业务数据,结果就是发送失败。所以服务器端要做到快速感知失败,减少无效链接操作,这就有了TCP的Keepalive(保活探测)机制。

当一个 TCP 连接建立之后,启用 TCP Keepalive 的一端便会启动一个计时器,当这个计时器数值到达 0 之后(也就是经过tcp_keep-alive_time时间后,这个参数之后会讲到),一个 TCP 探测包便会被发出。这个 TCP 探测包是一个纯 ACK 包(规范建议,不应该包含任何数据,但也可以包含1个无意义的字节,比如0x0。),其 Seq号 与上一个包是重复的,所以其实探测保活报文不在窗口控制范围内。

如果一个给定的连接在两小时内(默认时长)没有任何的动作,则服务器就向客户发一个探测报文段,客户主机必须处于以下4个状态之一:

\1. 客户主机依然正常运行,并从服务器可达。客户的TCP响应正常,而服务器也知道对方是正常的,服务器在两小时后将保活定时器复位。

\2. 客户主机已经崩溃,并且关闭或者正在重新启动。在任何一种情况下,客户的TCP都没有响应。服务端将不能收到对探测的响应,并在75秒后超时。服务器总共发送10个这样的探测 ,每个间隔75秒。如果服务器没有收到一个响应,它就认为客户主机已经关闭并终止连接。

\3. 客户主机崩溃并已经重新启动。服务器将收到一个对其保活探测的响应,这个响应是一个复位,使得服务器终止这个连接。

\4. 客户机正常运行,但是服务器不可达,这种情况与2类似,TCP能发现的就是没有收到探测的响应。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yitahutu79

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值