TCP是一个有状态通讯协议,所谓的有状态是指通信过程中通信的双方各自维护连接的状态。

一、TCP keepalive

先简单回顾一下TCP连接建立和断开的整个过程。(这里主要考虑主流程,关于丢包、拥塞、窗口、失败重试等情况后面详细讨论。)

首先是客户端发送syn(Synchronize Sequence Numbers:同步序列编号)包给服务端,告诉服务端我要连接你,syn包里面主要携带了客户端的seq序列号;服务端回发一个syn+ack,其中syn包和客户端原理类似,只不过携带的是服务端的seq序列号,ack包则是确认客户端允许连接;最后客户端再次发送一个ack确认接收到服务端的syn包。这样客户端和服务端就可以建立连接了。整个流程称为三次握手。

建立连接后,客户端或者服务端便可以通过已建立的socket连接发送数据,对端接收数据后,便可以通过ack确认已经收到数据。

数据交换完毕后,通常是客户端便可以发送FIN包,告诉另一端我要断开了;另一端先通过ack确认收到FIN包,然后发送FIN包告诉客户端我也关闭了;最后客户端回应ack确认连接终止。整个流程成为四次挥手。

TCP的性能经常为大家所诟病,除了TCP+IP额外的header以外,它建立连接需要三次握手,关闭连接需要四次挥手。如果只是发送很少的数据,那么传输的有效数据是非常少的。

是不是建立一次连接后续可以继续复用呢?的确可以这样做,但这又带来另一个问题,如果连接一直不释放,端口被占满了咋办。为此引入了今天讨论的第一个话题TCP keepalive。所谓的TCP keepalive是指TCP连接建立后会通过keepalive的方式一直保持,不会在数据传输完成后立刻中断,而是通过keepalive机制检测连接状态。

Linux控制keepalive有三个参数:保活时间net.ipv4.tcp_keepalive_time、保活时间间隔net.ipv4.tcp_keepalive_intvl、保活探测次数net.ipv4.tcp_keepalive_probes,默认值分别是 7200 秒(2 小时)、75 秒和 9 次探测。如果使用 TCP 自身的 keep-Alive 机制,在 Linux 系统中,最少需要经过 2 小时 + 9*75 秒后断开。譬如我们SSH登录一台服务器后可以看到这个TCP的keepalive时间是2个小时,并且会在2个小时后发送探测包,确认对端是否处于连接状态。

之所以会讨论TCP的keepalive,是因为发现服器上有泄露的TCP连接:

# ll /proc//fd/
lrwx------ root root Jan : /proc//fd/ -> socket:[]
# date
Sun Jan :: CST

已经建立连接两天,但是对方已经断开了(非正常断开)。由于使用了比较老的go(1.9之前版本有问题)导致连接没有释放。

解决这类问题,可以借助TCP的keepalive机制。新版go语言支持在建立连接的时候设置keepalive时间。首先查看网络包中建立TCP连接的DialContext方法中

if tc, ok := c.(*TCPConn); ok && d.KeepAlive >=  {
setKeepAlive(tc.fd, true)
ka := d.KeepAlive
if d.KeepAlive == {
ka = defaultTCPKeepAlive
}
setKeepAlivePeriod(tc.fd, ka)
testHookSetKeepAlive(ka)
}

其中defaultTCPKeepAlive是15s。如果是HTTP连接,使用默认client,那么它会将keepalive时间设置成30s。

var DefaultTransport RoundTripper = &Transport{
Proxy: ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: * time.Second,
KeepAlive: * time.Second,
DualStack: true,
}).DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: ,
IdleConnTimeout: * time.Second,
TLSHandshakeTimeout: * time.Second,
ExpectContinueTimeout: * time.Second,
}

下面通过一个简单的demo测试一下,代码如下:

func main() {

   wg := &sync.WaitGroup{}

   c := http.DefaultClient
for i := ; i < ; i++ {
wg.Add()
go func() {
defer wg.Done()
for {
r, err := c.Get("http://10.143.135.95:8080")
if err != nil {
fmt.Println(err)
return
}
_, err = ioutil.ReadAll(r.Body)
r.Body.Close()
if err != nil {
fmt.Println(err)
return
} time.Sleep( * time.Millisecond)
}
}()
}
wg.Wait()
}

执行程序后,可以查看连接。初始设置keepalive为30s。

然后不断递减,至0后,又会重新获取30s。

整个过程可以通过tcpdump抓包获取。

# tcpdump -i bond0 port  -nvv -A

其实很多应用并非是通过TCP的keepalive机制探活的,因为默认的两个多小时检查时间对于很多实时系统是完全没法满足的,通常的做法是通过应用层的定时监测,如PING-PONG机制(就像打乒乓球,一来一回),应用层每隔一段时间发送心跳包,如websocket的ping-pong。

二、TCP Time_wait

第二个希望和大家分享的话题是TCP的Time_wait状态。、

为啥需要time_wait状态呢?为啥不直接进入closed状态呢?直接进入closed状态能更快地释放资源给新的连接使用了,而不是还需要等待2MSL(Linux默认)时间。

有两个原因:

一是为了防止“迷路的数据包”,如下图所示,如果在第一个连接里第三个数据包由于底层网络故障延迟送达。等待新的连接建立后,这个迟到的数据包才到达,那么将会导致接收数据紊乱。

第二个原因则更加简单,如果因为最后一个ack丢失,那么对方将一直处于last ack状态,如果此时重新发起新的连接,对方将返回RST包拒绝请求,将会导致无法建立新连接。

为此设计了time_wait状态。在高并发情况下,如果能将time_wait的TCP复用, time_wait复用是指可以将处于time_wait状态的连接重复利用起来。从time_wait转化为established,继续复用。Linux内核通过net.ipv4.tcp_tw_reuse参数控制是否开启time_wait状态复用。

读者可能很好奇,之前不是说time_wait设计之初是为了解决上面两个问题的吗?如果直接复用不是反而会导致上面两个问题出现吗?这里先介绍Linux默认开启的一个TCP时间戳策略net.ipv4.tcp_timestamps = 1。

时间戳开启后,针对第一个迷路数据包的问题,由于晚到数据包的时间戳过早会被直接丢弃,不会导致新连接数据包紊乱;针对第二个问题,开启reuse后,当对方处于last-ack状态时,发送syn包会返回FIN,ACK包,然后客户端发送RST让服务端关闭请求,从而客户端可以再次发送syn建立新的连接。

最后还需要提醒读者的是,Linux 4.1内核版本之前除了tcp_tw_reuse以外,还有一个参数tcp_tw_recycle,这个参数就是强制回收time_wait状态的连接,它会导致NAT环境丢包,所以不建议开启。

作者:陈晓宇

作者著作《云计算那些事儿:从IaaS到PaaS进阶》

TCP漫谈之keepalive和time_wait的更多相关文章

  1. TCP中的KeepAlive与HTTP中的Keep-Alive

    KeepAlive 与 Keep-Alive 前言 昨天被问到了HTTP中Keep-Alive的概念,看名字我只知道是保持连接用的,但是对于他怎么结束连接,为什么要用他这些就不是很清楚了,今天查了一下 ...

  2. tcp中的keepalive(转)

    理解Keepalive(1) 大家都听过keepalive,但是其实对于keepalive这个词还是很晦涩的,至少我一直都只知道一个大概,直到之前排查线上一些问题,发现keepalive还是有很多玄机 ...

  3. TCP编程:系统出现 TIME_WAIT 原因及解决办法

    解决办法 打开 sysctl.conf 文件,修改以下几个参数: net.ipv4.tcp_tw_recycle = 1net.ipv4.tcp_tw_reuse = 1net.ipv4.tcp_ti ...

  4. 网络编程中 TCP 半开连接和TIME_WAIT 学习

    https://blog.csdn.net/chrisnotfound/article/details/80112736 上面的链接就是说明来 SO_KEEPALIVE 选项 为什么还需要 在应用层开 ...

  5. 闲说HeartBeat心跳包和TCP协议的KeepAlive机制

    很多应用层协议都有HeartBeat机制,通常是客户端每隔一小段时间向服务器发送一个数据包,通知服务器自己仍然在线,并传输一些可能必要的数据.使用心跳包的典型协议是IM,比如QQ/MSN/飞信等协议. ...

  6. TCP HTTP 详细内存分析 & time_wait setsockopt

    http://www.kegel.com/c10k.html#nb.edge http://www.chinasb.org/archives/2012/11/4954.shtml UDP协议:发送进程 ...

  7. TCP释放连接时为什么time_wait状态必须等待2MSL时间

    为什么上图中的A在TIME-WAIT状态必须等待2MSL时间呢? 第一,为了保证A发送的最后一个ACK报文能够到达B.这个ACK报文段有可能丢失,因而使处在LAST-ACK状态的B收不到对已发送的FI ...

  8. 聊聊 TCP 中的 KeepAlive 机制

    KeepAlive并不是TCP协议规范的一部分,但在几乎所有的TCP/IP协议栈(不管是Linux还是Windows)中,都实现了KeepAlive功能 RFC1122#TCP Keep-Alives ...

  9. Tcp协议的keepalive功能

    L:128

随机推荐

  1. 【DPDK】谈谈DPDK如何实现bypass内核的原理 其一 PCI设备与UIO驱动

    [前言] 随着网络的高速发展,对网络的性能要求也越来越高,DPDK框架是目前的一种加速网络IO的解决方案之一,也是最为流行的一套方案.DPDK通过bypass内核协议栈与内核驱动,将驱动的工作从内核态 ...

  2. [转帖]RSYNC 的核心算法

    RSYNC 的核心算法 https://coolshell.cn/articles/7425.html rsync是unix/linux下同步文件的一个高效算法,它能同步更新两处计算机的文件与目录,并 ...

  3. String常见的方法有哪些?

    length();//计算字符串的长度 charAt();//截取一个字符 getChars();//截取多个字符 equals();//比较两个字符串 equalsIgnoreCase();//比较 ...

  4. php制作缩略图

    PHP制作缩略图 1.制作缩略图的函数 imagecopyresampled(dest,src,dx,dy,sx,sy,dw,dh,sw,sh) 说明: dest 目标画布 src 原图(要缩略的图片 ...

  5. mysql 如果没有密码 就不能远程连接

    mysql 如果没有密码 就不能远程连接

  6. Openstack计算Nova组件

    欢迎来到虚拟机的世界,如果我们将Openstack环境里运行在各个无力节点上的各种服务看座生命体,而不是死的指令集合,那么就是一个虚拟机的世界. Openstack的计算组件,也就是Nova项目实现了 ...

  7. 蓝桥杯vip 字符串对比

    蓝桥杯vip 字符串对比 题目如下 给定两个仅由大写字母或小写字母组成的字符串(长度介于1到10之间),它们之间的关系是以下4中情况之一: 1:两个字符串长度不等.比如 Beijing 和 Hebei ...

  8. Mybatis总结一之SQL标签方法

    ---恢复内容开始--- 定义:mapper.xml映射文件中定义了操作数据库的sql,并且提供了各种标签方法实现动态拼接sql.每个sql是一个statement,映射文件是mybatis的核心. ...

  9. SpringMVC框架——原理及使用

    核心组件 1. DispatcherServlet:前端控制器器,负责调度其他组件的执行,可降低不同组件之间的耦合性,是Spring MVC 的核心模块. 2. Handler:处理器,完成具体的业务 ...

  10. WebStorm添加Angular2服务启动的脚本命令

    注意:选择的start在package.json可以修改. 例如添加一个run命令: