实践解读CLOSE_WAIT和TIME_WAIT

简介: 实践解读CLOSE_WAIT和TIME_WAIT

CLOSE_WAIT和TIME_WAIT是如何产生的?大量的CLOSE_WAIT和TIME_WAIT又有何隐患?本文将通过实践角度来带你揭开CLOSE_WAIT和TIME_WAIT的神秘面纱!事不决先祭此图:

微信图片_20220528192202.png


稍微补充一点:TIME_WAIT到CLOSED,这一步是超时自动迁移。


两条竖线分别是表示:


  • 主动关闭(active close)的一方
  • 被动关闭(passive close)的一方

网络上类似的图有很多,但是有的细节不够,有的存在误导。有的会把两条线分别标记成Client和Server。给读者造成困惑。对于断开连接这件事,客户端和服务端都能作为主动方发起,也就是「主动关闭」可以是客户端,可以是服务端。而对端相应的就是「被动关闭」。不管谁发起,状态迁移如上图。


1. 耗尽的是谁的端口?


首先解答初学者对socket的认识存在一个常见的误区。作为服务端,不管哪个WAIT都不会耗尽客户端的端口!


举个例子,我在某云的云主机上有个Server程序:echo_server。我启动它,监听2605端口。然后我在自己的MacBook上用telnet去连接它。连上之后,在云主机上用 netstat -anp看一下:


[guodong@yun test]netstat -anp|grep 2605
tcp    0    0 0.0.0.0:2605    0.0.0.0:*            LISTEN      3354/./echo_server
tcp    0    172.12.0.2:2605   xx.xx.xx.xx:31559    ESTABLISHED 3354/./echo_server


xx.xx.xx.xx是我Macbook的本机IP


其中有两条记录,LISTEN的表示是我的echo_server监听一个端口。ESTABLISHED表示已经有一个客户端连接了。第三列的IP端口是我echo_server的(这个显示IP是局域网的;第四列显示的是客户端的IP和端口,也就是我MacBook。


要说明的是这个端口:31559是客户端的。这个是建立连接时的MacBook分配的随机端口。


我们看一下echo_server占用的fd。使用 ls /proc/3354/fd -l 命令查看,其中 3354是echo_server的pid:


0 -> /dev/pts/6
1 -> /dev/pts/6
2 -> /dev/pts/6
3 -> anon_inode:[eventpoll]
4 -> socket:[674802865]
5 -> socket:[674804942]


0,1,2是三巨头(标准输入,输出,错误)自不必言。3是因为我使用了epoll,所以有一个epfd。


4其实就是我服务端监听端口打开的被动套接字;


5就是客户端建立连接到时候,分配给客户端的连接套接字。server程序只要给5这个fd写数据,就相当于返回数据给客户端。


服务端怎么会耗尽客户端的端口号的。这里消耗的其实是服务端的fd(也不是端口)!


2. 什么时候出现CLOSE_WAIT?


2.1 举个例子


回到我的MacBook终端,查看一下2605有关的连接(Mac上netstat不太好用,只能用lsof了):


[guodong@MacBook test]lsof -iTCP:2605
COMMAND    PID    USER    FD    TYPE DEVICE             SIZE/OFF    NODE NAME
telnet     74131  guodong 3u    IPv4 0x199db390a76b3eb3 0t0         TCP  192.168.199.155:50307->yy.yy.yy.yy:nsc-posa(ESTABLISHED)


yy.yy.yy.yy表示的远程云主机的IP


nsc-posa其实就是端口2605,因为2605也是某个经典协议(NSC POSA)的默认端口,所以这种网络工具直接显示成了那个协议的名称。


客户端pid为74135。当然,我其实知道我是用telnet连接的,只是为了查pid的话,ps aux|grep telnet也可以。


注意:为了测试。我这里的echo_server是写的有问题的。就是没有处理客户端异常断开的事件。


下面我kill掉telnet(kill -9 74131)。再回到云主机查看一下:


[guodong@yun test]netstat -anp|grep 2605
tcp    0    0 0.0.0.0:2605    0.0.0.0:*            LISTEN      3354/./echo_server
tcp    1    172.12.0.2:2605   xx.xx.xx.xx:31559    CLOSE_WAIT  3354/./echo_server


由于echo_server内没对连接异常进行侦测和处理。所以可以看到原先ESTABLISHED的连接变成了CLOSE_WAIT。并且会持续下去。我们再看一下它打开的fd:


0 -> /dev/pts/6
1 -> /dev/pts/6
2 -> /dev/pts/6
3 -> anon_inode:[eventpoll]
4 -> socket:[674865719]
5 -> socket:[674865835]


5这个fd还存在,并且会一直存在。所以当有大量CLOSE_WAIT的时候会占用服务器的fd。而一个机器能打开的fd数量是有限的。超过了,因为无法分配fd,就无法建立新连接啦!


2.2 怎么避免客户端异常断开时的服务端CLOSE_WAIT?


有一个方法。比如我用了epoll,那么我监听客户端连接套接字(5)的EPOLLRDHUP这个事件。当客户端意外断开时,这个事件就会被触发,触发之后。我们针对性的对这个fd(5)执行close()操作就可以了。改下代码,重新模拟一下上述流程,blabla细节略过。现在我们新echo_server启动。MacBook的telnet连接成功。然后我kill掉了telnet。观察一下云主机上的状况:


[guodong@yun test]netstat -anp|grep 2605
tcp    0    0 0.0.0.0:2605    0.0.0.0:*            LISTEN      7678/./echo_server
tcp    1    172.12.0.2:2605   xx.xx.xx.xx:31559    LAST_ACK    -


出现了LAST_ACK。我们看下fd。命令:ls /proc/7678/fd -l


0 -> /dev/pts/6
1 -> /dev/pts/6
2 -> /dev/pts/6
3 -> anon_inode:[eventpoll]
4 -> socket:[674905737]


fd(5)其实已经关闭了。过一会我们重新netstat看下:


[guodong@yun test]netstat -anp|grep 2605
tcp    0    0 0.0.0.0:2605    0.0.0.0:*            LISTEN      7678/./echo_server


LAST_ACK也消失了。为什么出现LAST_ACK。翻到开头,看我那张图啊!


CLOSE_WAIT不会自动消失,而LAST_TACK会超时自动消失,时间很短,即使在其存续期内,fd其实也是关闭状态。实际我这个简单的程序,测试的时候不会每次都捕捉到LAST_WAIT。有时候用netstat 命令查看的时候,就是最终那副图了。


3. 什么时候出现TIME_WAIT?


看我开篇那个图就知道了。


现在我kill掉我的echo_server!


[guodong@yun test]netstat -anp|grep 2605
tcp    0    0 172.17.0.2:2605    xx.xx.xx.xx:51327    TIME_WAIT    -


云主机上原先ESTABLISHED的那条瞬间变成TIME_WAIT了。


这个TIME_WAIT也是超时自动消失的。时间是2MSL。MSL是多长?


cat /proc/sys/net/ipv4/tcp_fin_timeout


一般是60。2MSL也就2分钟。在2分钟之内,对服务端有啥副作用吗?有,但问题不大。那就是这期间重新启动Server会报端口占用。这个等待,一方面是担心对方收不到自己的确认,等对方重发FIN。另一方面2MSL是报文的最长生命周期,可以避免Server重启(或其他Server绑同样端口)接收到了上一次的数据。


当然这个2MLS的等待,也可以通过给socket添加选项(SO_REUSEADDR)的方式来避免。Server可以立即重启(这样Server的监控进程就可以放心的重新拉起Server啦)。


通常情况下TIME_WAIT对服务端影响有限,而大量CLOSE_WAIT风险较高,但正确编写代码基本可以避免。为什么只说通常情况呢?因为生产环境是复杂的,一个服务通常会和多个下游服务用各种各样的协议进行通信。TIME_WAIT和CLOSE_WAIT在一些异常条件下,还是会触发的。


并不是说TIME_WAIT就真的无风险,其实无论是TIME_WAIT还是CLOSE_WAIT,永远记住当你的服务出现这两种现象的时候,它们只是某个问题导致的结果,而不是问题本身。有些网络教程教你怎么调大这个或那个的OS系统设置,个人感觉只是治标不治本。找到本质原因,避免TIME_WAIT和CLOSE_WAIT的产生,才是问题解决之道!


把这些了解清楚时候,是不是可以轻松应对什么4次挥手之类的面试题了?

相关实践学习
深入解析Docker容器化技术
Docker是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的Linux机器上,也可以实现虚拟化,容器是完全使用沙箱机制,相互之间不会有任何接口。Docker是世界领先的软件容器平台。开发人员利用Docker可以消除协作编码时“在我的机器上可正常工作”的问题。运维人员利用Docker可以在隔离容器中并行运行和管理应用,获得更好的计算密度。企业利用Docker可以构建敏捷的软件交付管道,以更快的速度、更高的安全性和可靠的信誉为Linux和Windows Server应用发布新功能。 在本套课程中,我们将全面的讲解Docker技术栈,从环境安装到容器、镜像操作以及生产环境如何部署开发的微服务应用。本课程由黑马程序员提供。     相关的阿里云产品:容器服务 ACK 容器服务 Kubernetes 版(简称 ACK)提供高性能可伸缩的容器应用管理能力,支持企业级容器化应用的全生命周期管理。整合阿里云虚拟化、存储、网络和安全能力,打造云端最佳容器化应用运行环境。 了解产品详情: https://www.aliyun.com/product/kubernetes
相关文章
|
应用服务中间件 Linux
Tomcat不定期close_wait过多
Tomcat不定期close_wait过多
860 0
|
存储 SQL 关系型数据库
PostgreSQL系统字段cmin和cmax详解
1.cmin和cmax是什么 PG中每个表都包含了一些系统字段,其中包括cmin和cmax。 cmin:插入该元组的命令在插入事务中的命令标识(从0开始累加) cmax:删除该元组的命令在插入事务中的命令标识(从0开始累加) 可以在Select命令的输出列表中显式地指定系统字段。
3818 0
|
存储 算法 C++
【C++】STL的基本用法
【C++】STL的基本用法
588 0
|
存储 应用服务中间件 nginx
使用filebeat简单收集多个nginx应用服务器日志(一)
使用filebeat简单收集日志 1.filebeat原理与介绍 filebeat收集日志的原理类似于tail -f命令,等待应用日志产生后,每隔30s将日志进行收集,收集完成后存放在es的索引库中,最后展示在kibana上
2125 0
使用filebeat简单收集多个nginx应用服务器日志(一)
|
Java 测试技术 Python
Python:Flask使用ThreadPoolExecutor执行异步任务
Python:Flask使用ThreadPoolExecutor执行异步任务
1117 0
|
8月前
Mac 上怎么安装 IDEA 2020.1.dmg?简单几步搞定(附安装包)
下载IDEA 2020.1.dmg文件后,双击打开并拖拽IntelliJ IDEA图标至“Applications”文件夹,等待复制完成。随后通过启动台或应用程序文件夹打开软件,首次运行时点击“打开”确认信任。安装成功,即可开始编程!(238字)
|
SQL 缓存 Kubernetes
K8S网络诊断之要命的5S抖动
某用户反馈8月4号凌晨00:30分左右,生产业务平均RT从100ms飙升到1000ms且抖动较大,如图1-1所示,(绿线为8月3号同时间段的RT,蓝线为异常后的RT)
2123 129
|
算法
动态规划算法学习三:0-1背包问题
这篇文章是关于0-1背包问题的动态规划算法详解,包括问题描述、解决步骤、最优子结构性质、状态表示和递推方程、算法设计与分析、计算最优值、算法实现以及对算法缺点的思考。
945 2
动态规划算法学习三:0-1背包问题
余三码和8421码的关系以及使用场景
余三码与8421码是两种不同的二进制编码方式,常用于表示十进制数。余三码是8421码加上3形成的无权码,具有自补性和进位信号特点,适合错误检测,但求和需修正。8421码是恒权码,方便二进制与十进制转换,常用于数字显示、数据传输和精确十进制运算。在计算机领域,两者各有应用场景,如BCD码用于七段显示器和精确计算,余三码则用于错误检测和简化算术操作逻辑设计。
|
存储 缓存 程序员
C++内存管理:避免内存泄漏与性能优化的策略
C++内存管理涉及程序稳定性、可靠性和性能。理解堆和栈的区别至关重要,其中堆内存需手动分配和释放。避免内存泄漏的策略包括及时释放内存、使用智能指针和避免野指针。性能优化策略则包括减少内存分配、选用合适数据结构、避免深拷贝及缓存常用数据。通过这些最佳实践,可提升C++程序的效率和质量。