HeLei Blog

TCP状态机分析(二)CLOSE_WAIT&TIME_WAIT

CLOSE_WAIT

在被动关闭连接(对方关闭)情况下,在已经接收到FIN,但是还没有发送自己的FIN的时刻,连接处于CLOSE_WAIT状态。出现大量close_wait的现象,主要原因是某种情况下对方关闭了socket链接,但是我方忙与读或者写,没有关闭连接。代码需要判断socket,一旦读到0,断开连接,read返回负,检查一下errno,如果不是AGAIN,就断开连接。

解决方法:
基本的思想就是要检测出对方已经关闭的socket,然后关闭它。

  1. 代码需要判断socket,一旦read返回0,断开连接,read返回负,检查一下errno,如果不是AGAIN,也断开连接。(注:在UNP 7.5节的图7.6中,可以看到使用select能够检测出对方发送了FIN,再根据这条规则就可以处理CLOSE_WAIT的连接)
  2. 给每一个socket设置一个时间戳last_update,每接收或者是发送成功数据,就用当前时间更新这个时间戳。定期检查所有的时间戳,如果时间戳与当前时间差值超过一定的阈值,就关闭这个socket。
  3. 使用一个Heart-Beat线程,定期向socket发送指定格式的心跳数据包,如果接收到对方的RST报文,说明对方已经关闭了socket,那么我们也关闭这个socket。
  4. 设置SO_KEEPALIVE选项,并修改内核参数,下面会详细介绍
    1
    2
    a. 参数设置
    查看相关的参数,这是当前线上环境的配置

sysctl -a|grep tcp_keepalive
net.ipv4.tcp_keepalive_intvl = 300
net.ipv4.tcp_keepalive_probes = 9
net.ipv4.tcp_keepalive_time = 75

1
2
参数的含义

tcp_keepalive_intvl: 在TCP保活打开的情况下,最后一次数据交换到TCP发送第一个保活探测包的间隔,即允许的持续空闲时长,或者说每次正常发送心跳的周期。
tcp_keepalive_probes: 在tcp_keepalive_time之后,没有接收到对方确认,继续发送保活探测包次数,默认值为9(次)
tcp_keepalive_time: tcp_keepalive_time之后,没有接收到对方确认,继续发送保活探测包的发送频率,默认值为75s。

1
2
设置相关的参数

sysctl -w net.ipv4.tcp_keepalive_time = 75
也可以直接打开/etc/sysctl.conf
加入net.ipv4.tcp_keepalive_time = 75
``

让参数生效
sysctl -p

(tcp_keepalive_time + tcp_keepalive_intvl * tcp_keepalive_probes)时间内可检测到连接失效与否。

b. 开启keepalive属性
int keepAlive = 1;
setsockopt(client_fd, SOL_SOCKET, SO_KEEPALIVE, (void*)&keepAlive, sizeof(keepAlive));

c. 系统调用设置
这样只会影响单个连接,上面修改内核参数会影响所有设置keepalive属性的连接

#include

#include

#include

int keepAlive = 1; // 开启keepalive属性
int keepIdle = 1800; // 如该连接在1800秒内没有任何数据往来,则进行探测
int keepInterval = 3; // 探测时发包的时间间隔为3秒
int keepCount = 2; // 探测尝试的次数.如果第1次探测包就收到响应了,则后几次的不再发.
setsockopt(client_fd, SOL_SOCKET, SO_KEEPALIVE, (void)&keepAlive, sizeof(keepAlive));
setsockopt(client_fd, SOL_TCP, TCP_KEEPIDLE, (void
)&keepIdle, sizeof(keepIdle));
setsockopt(client_fd, SOL_TCP,TCP_KEEPINTVL, (void )&keepInterval, sizeof(keepInterval));
setsockopt(client_fd, SOL_TCP, TCP_KEEPCNT, (void
)&keepCount, sizeof(keepCount));

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
这篇文章对于一次生产环境遇到的CLOSE_WAIT问题分析很到位
转载:https://mp.weixin.qq.com/s?__biz=MzI4MjA4ODU0Ng==&mid=402163560&idx=1&sn=5269044286ce1d142cca1b5fed3efab1&3rd=MzA3MDU4NTYzMw==&scene=6#rd
### 总结
1. 默认情况下使用keepalive周期为2个小时,如不选择更改,属于误用范畴,造成资源浪费:内核会为每一个连接都打开一个保活计时器,N个连接会打开N个保活计时器。 优势很明显:
* TCP协议层面保活探测机制,系统内核完全替上层应用自动给做好了
* 内核层面计时器相比上层应用,更为高效
* 上层应用只需要处理数据收发、连接异常通知即可
* 数据包将更为紧凑
2. 关闭TCP的keepalive,完全使用业务层面心跳保活机制 完全应用掌管心跳,灵活和可控,比如每一个连接心跳周期的可根据需要减少或延长
业务心跳 + TCP keepalive一起使用,互相作为补充,但TCP保活探测周期和应用的心跳周期要协调,以互补方可,不能够差距过大,否则将达不到设想的效果。朋友的公司所做IM平台业务心跳2-5分钟智能调整 + tcp keepalive 300秒,组合协作,据说效果也不错。
虽然说没有固定的模式可遵循,那么有以下原则可以参考:
1. 不想折腾,那就弃用TCP Keepalive吧,完全依赖应用层心跳机制,灵活可控性强
2. 除非可以很好把控TCP Keepalive机制,那就可以根据需要自由使用吧
### http keep-alive与tcp keep-alive
http keep-alive与tcp keep-alive,不是同一回事,意图不一样。http keep-alive是为了让tcp活得更久一点,以便在同一个连接上传送多个http,提高socket的效率。而tcp keep-alive是TCP的一种检测TCP连接状况的保活机制。
## TIME_WAIT
主动发起close的一方会出现TIME_WAIT,比如nginx到web server之间,TIME_WAIT状态是为了保护TCP协议的正确性,避免端口发生复用后老的TCP连接残留在网络上的报文进入新的连接里。但这也引入了一个问题,临时端口数量有限,耗尽后,新建连接就会报错EADDRNOTAVAIL
我们来看下,为什么这个状态能影响到一个处理大量连接的服务器,从下面三个方面来说:
* 新老连接(相同四元组)在TCP连接表中的slot复用避免
* 内核中,socket结构体的内存占用
* 额外的CPU开销
### 解决方法
* 首先要增加临时端口的数量,增加可被消耗的临时端口资源

sysctl -w “net.ipv4.ip_local_port_range=1024 65535”

1
2
3
* 然后要加速临时端口回收,可以对内核参数做优化(/etc/sysctl.conf)
1. 启用tcp_tw_reuse

sysctl -w net.ipv4.tcp_timestamps=1
sysctl -w net.ipv4.tcp_tw_reuse=1
sysctl -p

1
2
3
4
5
它定义了一个新的TCP选项–两个四字节的timestamp fields时间戳字段,第一个是TCP发送方的当前时钟时间戳,而第二个是从远程主机接收到的最新时间戳。
启用net.ipv4.tcp_tw_reuse后,如果新的时间戳,比以前存储的时间戳更大,那么linux将会从TIME-WAIT状态的存活连接中,选取一个,重新分配给新的连接出去的TCP连接。
连出的TIME-WAIT状态连接,仅仅1秒后就可以被重用了
2. 方法更激进些,启用tw_recycle,tw_recycle允许在两个RTT。当多个客户端处于NAT后时,在服务器端开启tw_recycle会引起丢包问题,如果丢SYN包,就会造成新建连接失败

sysctl -w net.ipv4.tcp_timestamps=1
sysctl -w net.ipv4.tcp_tw_recycle=1
sysctl -p

1
2
3
4
5
6
7
8
3. 给socket配置SO_LINGER,on设为1,linger设为0,这样关闭连接后TCP状态从ESTAB直接进入CLOSED,向服务器发rst包而不是fin包来关闭连接。这种方法风险最高,会丢弃buffer里未发送完的数据,不过通过设计协议(客户端和服务器协议上协商后再关闭TCP连接)可以规避这个问题,使用需要小心,选择合适的场景。
### 代码验证
"纸上得来终觉浅,绝知此事要躬行", 下面我们来验证这样的方法是否真的可行
1. TIME_WAIT过多会耗尽端口,为了模拟端口耗尽的情况,先修改本地临时端口只有一个可用

sysctl -w “net.ipv4.ip_local_port_range=20000 20000”

1
2
2. 启动一个server进程

./tcp_server05 127.0.0.1

1
2
3. 第一个客户端连接

./tcp_client01 127.0.0.1

1
2
3
4
5
关闭client,查看连接状态
![img01](/2017/11/29/network-tcp-machine-state/tcp_machine_state.png)
4. 第二个客户端连接

chris@ubuntu:~/myspace/test/network$ ./tcp_client01 127.0.0.1
connect: Cannot assign requested address

1
2
5. 修改系统参数

sysctl -w net.ipv4.tcp_tw_reuse=1
```

  1. 重复3、4,可以看到可以成功建立连接

参数说明

  • tcp_tw_recycle (Boolean; default: disabled; since Linux 2.4)
    Enable fast recycling of TIME_WAIT sockets. Enabling this option is not recommended since this causes problems when working with NAT (Network Address Translation).
  • tcp_tw_reuse (Boolean; default: disabled; since Linux 2.4.19/2.6)
    Allow to reuse TIME_WAIT sockets for new connections when it is safe from protocol viewpoint. It should not be changed without advice/request of technical experts.

参考文献

坚持原创技术分享,您的支持将鼓励我继续创作!