tcp 使用场景,一般区分为“长连接”和“短连接”

如何解决tcp短连接的TIME_WAIT问题

短连接最大缺点:占用资源(如:本地端口、socket句柄)


正常的TCP client关闭后,进入TIME_WAIT,持续的时间一般在1~4分钟

每秒建1000个短连接(Web请求访问memcached),假TIME_WAIT时间是1分钟,1分钟内需要建立6W个短连接,这些短连接1分钟内都处于TIME_WAIT状态,都不会释放,Linux默认本地端口范围net.ipv4.ip_local_port_range = 32768 61000 不到3W,由于没有本地端口就不能建立了

解决:

  • 改长连接,脚本语言,需要通过proxy之类的软件才能实现长连接
  • ipv4.ip_local_port_range,增大,只治标
  • 客户端程序中设置socket SO_LINGER
  • 客户端机器打开tcp_tw_recycle和tcp_timestamps
  • 客户端机器打开tcp_tw_reuse和tcp_timestamps
  • 客户端机器设置tcp_max_tw_buckets为一个很小的值


SO_LINGER 数据结构

struct linger {
     int l_onoff; /* 0 = off, nozero = on */
     int l_linger; /* linger time */
};
  1. l_onoff=0,l_linger被忽略,内核缺省情况,close调用立即返回给调用者,如果可能将会传输任何未发送的数据
  2. l_onoff 非0,l_linger=0,socket关闭时TCP夭折连接,丢弃发送缓冲区中的任何数据并发送一个RST,而不是通常的四分组终止序列,避免了TIME_WAIT状态
  3. l_onoff 非0,l_linger 非0,当socket关闭时内核将拖延一段时间(由l_linger决定)

如缓冲区残留数据,进程睡眠,直到 a. 所有数据发送完且被对方确认,之后进行正常的终止序列(描述字访问计数为0) 或 b. 延迟时间到

应用检查close返回值非常重要,如果在数据发送完并被确认前时间到,close将返回EWOULDBLOCK错误且套接口发送缓冲区中的任何数据都丢失 close的成功返回仅告诉我们发送的数据(和FIN)已由对方TCP确认,它并不能告诉我们对方应用进程是否已读了数据。如果socket设为非阻塞的,将不等待close完成

第 2 种 只有在丢弃数据的时候才发送RST”,如果没有丢弃数据,则走正常的关闭流程

if (data_was_unread) {
/* Unread data was tossed, zap the connection. */
NET_INC_STATS_USER(sock_net(sk), LINUX_MIB_TCPABORTONCLOSE);
tcp_set_state(sk, TCP_CLOSE);
tcp_send_active_reset(sk, sk->sk_allocation);
} 

第三种情况其实就是第一种和第二种的折中处理,且当socket为非阻塞的场景下是没有作用的

对于应对短连接导致的大量TIME_WAIT连接问题,第二种处理是最优的选择,libmemcached就是采用这种方式


linux TIME_WAIT 相关参数: 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 = 60 如果套接字由本端要求关闭,这个参数决定了它保持在FIN-WAIT-2状态的时间(可改为30,一般来说FIN-WAIT-2的连接也极少)

注意:

  • 不像Windows 可以修改注册表修改2MSL 的值,linux 是没有办法修改MSL的,tcp_fin_timeout 不是2MSL 而是Fin-WAIT-2状态

  • tcp_tw_reuse 和SO_REUSEADDR 是两个完全不同的东西

SO_REUSEADDR 允许同时绑定 127.0.0.1 和 0.0.0.0 同一个端口; SO_RESUSEPORT linux 3.7才支持,用于绑定相同ip:port,像nginx 那样 fork方式也能实现

  1. tw_reuse,tw_recycle 必须在客户端和服务端timestamps(默认打开)开启时才管用

  2. tw_reuse 只对客户端起作用,开启后客户端在1s内回收

  3. tw_recycle 对客户端和服务器同时起作用,开启后在 3.5*RTO 内回收,RTO 200ms~ 120s 具体时间视网络状况。

内网状况比tw_reuse 稍快,公网尤其移动网络大多要比tw_reuse 慢,优点就是能够回收服务端的TIME_WAIT数量

对于客户端 1. 作为客户端因为有端口65535问题,TIME_OUT过多直接影响处理能力,打开tw_reuse 即可解决,不建议同时打开tw_recycle,帮助不大。

  1. tw_reuse 帮助客户端1s完成连接回收,基本可实现单机6w/s请求,需要再高就增加IP数量吧。

  2. 如果内网压测场景,且客户端不需要接收连接,同时tw_recycle 会有一点点好处。

  3. 业务上也可以设计由服务端主动关闭连接

对于服务端

  1. 打开tw_reuse无效

  2. 线上环境 tw_recycle 不要打开

服务器处于NAT 负载后,或者客户端处于NAT后(这是一定的事情,基本公司家庭网络都走NAT);

 公网服务打开就可能造成部分连接失败,内网的话到时可以视情况打开;

像我所在公司对外服务都放在负载后面,负载会把timestamp 都给清空,好吧,就算你打开也不起作用。

  1. 服务器TIME_WAIT 高怎么办

不像客户端有端口限制,处理大量TIME_WAIT Linux已经优化很好了,每个处于TIME_WAIT 状态下连接内存消耗很少,

而且也能通过tcp_max_tw_buckets = 262144 配置最大上限,现代机器一般也不缺这点内存。

一台每秒峰值1w请求的http 短连接服务,长期处于tw_buckets 溢出状态,很稳定

slabtop ss -s

唯一不爽的就是:

系统日志中overflow 错误一直再刷屏,也许该buckets 调大一下了

TCP: time wait bucket table overflow

  1. 业务上也可以设计由客户端主动关闭连接

原理分析

  1. MSL 由来

  发起连接关闭方回复最后一个fin 的ack,为避免对方ack 收不到、重发的或还在中间路由上的fin 把新连接给干掉了,等个2MSL,4min。

  也就是连接有谁关闭的那一方有time_wait问题,被关那方无此问题。

  1. reuse、recycle

    通过timestamp的递增性来区分是否新连接,新连接的timestamp更大,那么小的timestamp的fin 就不会fin掉新连接。

  2. reuse

    通过timestamp 递增性,客户端、服务器能够处理outofbind fin包

  3. recycle

    对于服务端,同一个src ip,可能会是NAT后很多机器,这些机器timestamp递增性无可保证,服务器会拒绝非递增请求连接。

细节之处还得好好阅读tcp 协议栈源码了