由于 TCP 协议整个机制也非常复杂我只能尽可能的在某一条线上来说, 不可能面面俱到, 如果有疏漏或者对于内容有异议可以留言. 谢谢大家.
查看服务器上各个状态的统计数量:
netstat -ant | awk '/^tcp/ {++y[$NF]} END {for(w in y) print w, y[w]}'
单独查看 TIME_WAIT,ss -nat | grep TIME-WAIT
ss 命令中的 TIME WAIT 的写法和 netstat 中有所不同
TIME_WAIT 的作用
主动断开的一方的 TCP 连接会在这个状态下保持 2MSL, 其作用就 2 个:
确保对方收到自己发送的最后一个 ACK(因为对方发送了 FIN), 如果对方没有收到自己发送的 ACK 必定会重新发送 FIN, 这样保证 4 次断开的完整性. 因为 MSL 是最大报文生存时间, 如果在 1 个 MSL 时间内自己发送的 ACK 对方没有收到那就注定收不到了, 而且对方肯定还会发送 FIN, 那么一个 FIN 发送过来的最长时间也是 1 个 MSL, 所以这里要等待 2MSL.
另外一个原因就是避免延迟的 IP 报文, 在频繁短连接的场景下客户端通常会对同一个 IP 和端口在短时间内发起多次连接, 而客户端使用的端口是自己系统随机分配的高位端口, 有一定概率发生上一个 socket 四元组和下一个 socket 四元组一样, 如果这时候一个原本属于上一个 socket 四元组的被延迟的 IP 报文送达, 那么这将发送数据混乱的状态, 所以为了避免这种情况就利用 MSL 这个报文最大生存时长机制让残余的 IP 报文在网络中消失. 这时候同样的四元组又可以被使用了.
另外你无须担心这个迟到的 IP 报文是有用的, 因为 TCP 是可靠连接, 它有重传机制, 所以这个迟到的 IP 报文消失不会影响之前通信的数据完整性. 哪怕这个报文是在 2MSL 期间到达也将会被抛弃.
处于该状态的 socket 什么时候可以再次使用:
2MSL 之后
如果处于 2MS 期间, 重用连接那么要保证新连接的 TCP 的 Seq 也就是序列号要比之前的大
如果处于 2MS 期间, 重用的连接要保证后续的时间戳要比之前的连接的时间戳更晚
只有满足上面的条件才不会在发生新连接上出现老连接的延迟的 IP 分组, 满足第一个条件则不会出现延迟的 IP 分组, 如果满足后面的条件那么延迟的 IP 分组即使出现在新连接上也会被直接丢弃进而不会影响现有通信数据.
使用 Wireshark 抓包在 TCP 中显示的 Seq 都是从 0 开始的, 这是这个软件做了处理, 如下图:
要想看真正的序列号需要做一个配置:
Wireshark-->Preferences-->Protocols-->Tcp
点击确定你就可以看到真实的 Seq
2MSL 到底有多长呢? 这个不一定, 1 分钟, 2 分钟或者 4 分钟, 还有的 30 秒. 不同的发行版可能会不同. 在 CentOS 7.6.1810 的 3.10 内核版本上是 60 秒.
TIME_WAIT 会影响什么
端口: 但是这是对于通信过程中扮演客户端角色的一端来说, 因为客户端使用随机端口来访问服务器, 当它主动断开的时候会出现这个状态, 比如第一次系统给它分配了一个 51000 的随机端口访问服务器, 然后客户端主动断开了, 在 2MSL 期间, 该端口就处于 TIME_WAIT 状态, 如果它再次访问相同的服务器, 那么系统会为它再次分配一个随机端口, 如果 51000 端口还处于 TIME_WAIT 状态, 那么这个随机端口就肯定不是 51000, 如果 51000 端口不处于 TIME_WAIT 状态, 那么这个随机端口就有可能是 51000. 所以这个状态在一定期间内对于客户端角色来讲会影响并发量, 大量这个 TIME_WAIT 就导致可用随机端口不断减少.
内存: 这个量会很小, 无需担心, 哪怕是上万的 TIME_WAIT.
文件描述符: 但是处于 TIME_WAIT 状态的套接字其实是已经关闭了文件描述符, 也就是说这个状态并不占用文件描述符这也就是意味着该状态不会对应一个打开的文件.
如何解决
网上很多人给出的答案是调整内核参数比如下面的参数, 但是这些答案有很多误区, 在不同场景下并不一定适用, 所以这里先对参数做一下澄清:
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 官方解释 https://access.redhat.com/solutions/41776 . 至于到底如何修改 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_reuse 和 net.ipv4.tcp_tw_recycle 的前提是开启时间戳 net.ipv4.tcp_timestamps = 1 不过这一项默认是开启的.
作为 7 层的代理的 Nginx
在这种场景下首先要搞清楚哪一侧产生 TIME_WAIT 最多. 为什么要看这个, 我们知道 TIME_WAIT 是主动关闭一方具有的状态, 但是 Nginx 作为 7 层代理对外它是服务器而对内它是客户端(例如, 相对于后端的其他 web 应用比如 Tomcat).
对外侧(被动连接)
ss -nat | grep "TIME-WAIT" | awk '{print $4}' | egrep -w "192.168.71.101:80|192.168.71.101:443" | wc -l
作为代理服务器对外提供的端口就是 80 和 443, 所以我们针对 "TIME-WAIT" 状态来进行过滤本地地址和端口而且通过 - w 进行严格匹配 2 个条件就是 "192.168.71.101:80|192.168.71.101:443", 这样就统计出 Nginx 作为服务器一方的外连 TIME-WAIT 的数量.
看一下抓包情况(下图是测试环境的包, 上图是生产环境的统计)
由于在作为 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
我们增加一个 - v 参数来取反, 这样就获取了本地地址中不是 80 和 443 端口的 TIME-WAIT 状态数量, 那么这个数量就是 Nginx 作为客户端进行内连后端服务器所产生的.
很明显对内侧的 TIME_WAIT 明显比对外侧要高, 这就是因为 Nginx 反向代理到后端使用随机端口来主动连接后端服务的固定端口, 在短连接的情况下(通常是短连接),Nginx 作为主动发起连接的一方会主动断开, 所以在业务繁忙的 Nginx 代理服务器上会看到大量的对内侧的 TIME_WAIT.
基于这种情况可以采用 net.ipv4.tcp_tw_reuse 和 net.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 以后这个参数也被弃用了参见.
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 的时候提到会对比时间戳, 如果收到的报文时间戳小于最近连接的时间戳就会被丢弃, 那么我们如何获取这个时间戳呢? 我们先看看它长什么样子:
TSval: 发送端时间戳
TSecr: 对端回显时间戳
我们看第三行, 如下图:
这一行是客户端回复 ACK 给服务器完成三次握手的最后一个阶段, TSval 就是客户端的时间戳这个和第一行一样这是因为速度快还没有走完一个时间周期, 这一行的 TSecr 是 434971890, 这个就是第二行服务器回复 SYN 时候给客户端发来的服务器的时间戳, 这个就叫做回显时间戳.
这个时间戳是一个相对时间戳而不是我们通常理解的绝对时间戳(自 1970 年 1 月 1 日的那种形式), 而且你不能把它当做时间来用, 在 RFC1323 中也提到对报文的接收者来讲时间戳可以看做另外一种高阶序列号.
这里就会有一个问题, 2 个时间戳, 一个是自己的, 一个是对端的, 到底用哪个时间戳来进行比较来确定是否丢弃报文呢? 答案是 TSval, 也就是发送端的时间戳. 这样很容易理解, 作为主动断开的一方要丢弃的是对端传递过来的重复报文, 显然需要用对端的时间戳来判断不可能用自己的时间戳. 而且从上图可以看到自己的时间戳和对端的时间戳明显有很大差距, 也就是说这个时间戳是通信双方自己生成的. 这个时间戳就放在 TCP 报文的 options 选项中, 如下图:
可以看到它是 options, 既然是选项那么就不是必须的, 所以这也就是为什么当开启 reuse 和 recycle 的时候要求开启这个, 因为不开启则无法识别重复的 IP 分组.
简单原理就是: 保存该 socket 上一次报文的 TSval 时间戳, 如果该 socket 的 4 元组被重复利用或者快速回收, 那么假如收到了之前连接重复的报文, 则比较该报文的时间戳是不是比保存的 TSval 小, 如果小则丢弃. 我这里只是简单来说基于时间戳的机制来放置重复报文, 整个的 PAWS 还有其他的原则, 具体请查看 RFC1323 https://tools.ietf.org/html/rfc1323#section-3.4 .
另外, 由于时间戳也是通过一串数字来表示且 TCP 头的时间戳长度也是 32 位(每个都是 4byes), 所以它也会出现循环, 时间跳动频率就决定了翻转周期, 那这个频率是多少呢, RFC1312 中规定建议在 1ms 到 1s 之间, 这个时间间隔不同系统可能不一样, 不过这里内核选项和用户选项的区别:
内核选项, 在 Linux 中 cat /boot/config-$(uname -r) | grep -w "CONFIG_HZ" 查看,
Jiffies 是从计算机启动到现在总共发生多少节拍数, 节拍数叫做 Tick,Tick 是 HZ 的倒数, 如果上所示 HZ 是 1000, 每秒发生 1000 次中断, 也就是 1 毫秒发生一次中断, 对应 Tick 是 1ms, 也就是每 1 毫秒 Jiffies 就加 1. 当重启电脑的时候 Jiffies 重置.
用户选项, 由于用户空间程序不能直接访问, 所以内核还提供了一个 USER_HZ 来让用户空间程序使用, 固定为 100, 百分之一秒, 也就是 10 毫秒. 如何查看呢? getconf CLK_TCK 命令:
我们从网卡上看也是这个值 cat /proc/sys.NET/ipv4/neigh/ens33/locktime
如果这个间隔是 1 毫秒, 那么时间戳反转一次将是 24.8 天; 如果是 10 毫秒就是 248 天, 依次类推, 但最大不能超过 1 秒.
查询内核参数及含义 https://sysctl-explorer.net/
在线查询内核代码 https://elixir.bootlin.com/linux/
来源: https://www.cnblogs.com/rexcheny/p/11143128.html