深入分析服务端大量close_wait的根源

选自于:https://www.jianshu.com/p/42918db85f19 


在开发网络服务器应用系统的时候,有时会碰到服务器有大量的socket处于CLOSE_WAIT状态,也无法关闭,导致服务器无法接受新的用户请求,最终导致服务器奔溃,系统重启才能解决。

为什么会出现大量的CLOSE_WAIT状态呢?

要解决这个问题,我们得先介绍一下socket断开过程中的四次挥手。

终止TCP连接的四次挥手

由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。
假设终止命令由client端发起。

image-20181208175427046

  •  CLOSED表示socket连接没被使用。
  • LISTENING表示正在监听进入的连接。
  • SYN-SENT表示正在试着建立连接。
  • SYN_RECEIVED进行连接初始同步。
  • ESTABLISHED表示连接已被建立。
  • CLOSEWAIT表示远程计算器关闭连接,正在等待socket连接的关闭。
  • FIN_WAIT1表示socket连接关闭,正在关闭连接。
  • CLOSING先关闭本地socket旌接,然后关闭远程socket连接,最后等待确认信息。
  • LASTACK远程计算器关闭后,等待确认信号。
  • FINWAIT2 socket连接关闭后,等待来自远程计算器的关闭信号。
  • TIMEWAIT连接关闭后,等待远程计算器关闭重发。

 

当clien端传输完成数据,或者需要断开连接时:

  1. Client端发送一个FIN报文给Server端。(序号为M)
    1.1. 表示要终止Client到Server这个方向的连接。
    1.1. 通过调用close(socket) API。
    1.3 表示Client不再会发送数据到Server端。(但Server还能继续发给Client端)
    1.4 Client状态变为FIN_WAIT_1
  2. Server端收到FIN后,发送一个ACK报文给Client端。(序号为M+1)
    2.1 Server状态变为CLOSE_WAIT
    2.2 Client收到序号为(M+1)的ACK后状态变为FIN_WAIT_2
     
  3. Server端也发送一个FIN报文给Client端。(序号为N)
    3.1 表示Server也要终止到Client端这个方向的连接。
    3.2. 通过调用close(socket) API。
    3.3 Server端状态变为LAST_ACK
  4. Client端收到报文FIN后,也发送一个ACK报文给服务器。(序号N+1)
    4.1 Client状态变为TIME_WAIT
  5. Server端收到序号为(N+1)的ACK
    5.1 Server的状态变为CLOSED.
  6. 等带2MSL之后
    6.1 Client的状态也变为CLOSE.
  7. 至此,一个完整的TCP连接就关闭了。

两个基本问题:

  1. Q: 我们看到CLOSE_WAIT出现在什么时候呢?
    A: 在Sever端收到Client的FIN消息之后。
  2. Q: 状态CLOSE_WAIT在什么时候转换成下一个状态呢?
    A: 在Server端向Client发送FIN消息之后。

至此似乎明白了为什么会出现CLOSE_WAIT的状态:如果Server端一直没有向client端发送FIN消息(调用close() API),那么这个CLOSE_WAIT会一直存在下去。 

原因分析

从上面我们看到出现CLOSE_WAIT,说明Server端没有发起close()操作,这基本上是用户server端程序的问题了;通常情况下,Server都是等待Client访问,如果Client退出请求关闭连接,server端自觉close()对应的连接。

当然这也可能是业务实现上的需要,暂时不发送FIN,因为服务器可能还有数据要发往客户端,等发送完所有应用数据最后再发送FIN消息了;这个场景并不是这里我们讨论的大量COLSE_WAIT的问题了,因为这个还是可控的。

我们要讨论的场景是什么?我们先介绍两个系统调用,前面也提到并且用到的close(socket)和shutdown(socket,HOW)接着往下分析。

我们知道一个进程打开一个socket,然后此进程fork出子进程的时候,父进程已打开的socket是会被继承的,即子进程能够继续访问这个socket。其结果就是,一个socket被两个进程打开,一个父进程和一个子进程,此时socket的引用计数会变成2。

  • 调用close(socket)时,内核先检查socket上的引用计数器:如果引用计数大于1,那么将这个引用计数减1,然后直接返回。如果引用计数等于1,那么内核才会真正关闭此socket。(通过发送FIN到对端来关闭TCP连接)
  • 调用shutdown(socket,HOW)时,内核不会检查此socket对应的引用计数器,直接向对端发送FIN来关闭TCP连接。

据此分析,很大可能性是用户服务器的程序实现有问题导致的大量CLOSE_WAIT的socket,比如父进程打开了socket,然后通过fork出子进程来处理业务,父进程继续对网络请求进行监听,永远不会终止;当客户端发FIN过来的时候,处理业务的子进程处理此FIN消息,调用close()对本端进行关闭,然而这个close()调用只是把socket的引用计数器减1,因为父进程还在运行,socket并没关闭,这样就导致系统中又多了一个CLOSE_WAIT的socket,长此以往,就这样了。

关于TIME_WAIT状态

多说两句关于TIME_WAIT的状态,这个发生在client端,而且是不可避免的,其时间长度是固定的2MSL,到期自动转为CLOSED,不会导致系统资源耗尽的问题。

MSL是一个系统级参数,可调。



作者:CodingCode
链接:https://www.jianshu.com/p/42918db85f19
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

是这个代码报错from nebula3.gclient.net import ConnectionPool from nebula3.Config import Config # 配置参数保持不变 config = { "graphd_hosts": ["10.25.10.205"], "graphd_port": 9669, "user": "root", "password": "nebula", "space_name": "my_graph" } def configure_connection_pool(): cfg = Config() cfg.max_connection_pool_size = 10 connection_pool = ConnectionPool() if not connection_pool.init([(host, config["graphd_port"]) for host in config["graphd_hosts"]], cfg): raise RuntimeError("连接池初始化失败") return connection_pool def execute_in_session(conn_pool, queries): """关键修改:在单个会话中执行多个查询""" try: with conn_pool.session_context(config["user"], config["password"]) as session: # 强制选择图空间(每次会话必须执行) use_space = f'USE {config["space_name"]};' if not session.execute(use_space).is_succeeded(): print("切换图空间失败") return False for query in queries: result = session.execute(query) if not result.is_succeeded(): print(f"操作失败: {result.error_msg()}") return False return True except Exception as e: print(f"会话异常: {str(e)}") return False def insert_data_sample(conn_pool): """整合操作到单次会话""" queries = [ # 插入顶点 ''' INSERT VERTEX player(name, age) VALUES "player_101":("LeBron James", 39), "player_102":("Stephen Curry", 35); ''', # 插入边 ''' INSERT EDGE follow(degree) VALUES "player_101" -> "player_102":(98); ''' ] if execute_in_session(conn_pool, queries): print("数据插入成功") else: print("数据插入失败") def main(): try: conn_pool = configure_connection_pool() print("已连接Nebula Graph") insert_data_sample(conn_pool) except Exception as e: print(f"连接错误: {str(e)}") finally: if 'conn_pool' in locals(): conn_pool.close() print("连接池已释放") if __name__ == "__main__": main() 已连接Nebula Graph 操作失败: SemanticError: No schema found for `player' 数据插入失败 连接池已释放
最新发布
04-09
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值