在现代应用系统实现中,我们一般使用长连接与服务端进行交互,以减少建立连接的开销。
同时使用连接池,维护特定数量的连接以备随时使用。
但是在NAT等有中间设备的情况下,长连接很可能因为某些原因被断开。
从而导致连接池失去应有的作用。
基于这些需求,就需要使用TCP的keepalive功能。
它能提供一种自动、高效、可灵活配置,相比应用层心跳包更轻量的连接保活方式。
背景介绍
当我们需要对外提供服务的时候,尤其是KV存储这种对稳定性更高的中间件服务,一般会使用VIP, 而不是让用户直连:
- 减少变更时对用户的影响:如proxy替换、扩容、维护
- 高可用:更方便的高可用,用户无需探活健康检查等
- 隐藏拓扑细节:更高的安全性
使用VIP时需要注意的是,长时间idle的连接会被reset。
原因是LVS一般配置为保持连接有时间限制,如果超过此时间后连接上一直没有报文,LVS就会发送RST报文断开链接。
这么做的原因是为了节省连接资源,也避免攻击。
但是在实际使用场景中,用户一般使用资源池维护长连接。
在连接池中预留部分多余连接是合理的,此部分连接应该保证长期有效。
实现细节
相关参数
具体可以通过man 7 tcp查看。
Linux中有三相关配置项,可使用sysctl -a
查看:
# 以下单位都为秒 |
使用上述配置,表现大概如下:
连接空闲7200s后触发keepalive,发送第一次ACK
- 连接如果正常,对端会回应ACK
- 连接如果一直正常,会再次等待空闲7200s后发送ACK
- 如果连接异常,对端未回应ACK,会等待75s后发送第二个ack
- 如果一直未成功,第9次发送ACK未回应后,发送RST包关闭连接
(注意:以上参数在不同系统下表现可能不一致,我们只说linux下的表现。)
系统的配置只是默认值。关闭一个连接需要的时间计算公式为:tcp_keepalive_time + tcp_keepalive_probes* tcp_keepalive_intvl
默认为7200s+9*75s = 2h 11min 15s
。
这其中存在两个问题:
- 默认idle时间为2小时超过2个小时才开始keepalive,时间太长了,这个时间内一般应用早已经超时
- 没有提供全局开启keepalive的选项
系统调用
如上,Linux中没有提供全局开启keepalive的选项,必须通过setsockopt系统调用针对单独的socket进行设置。
TCP socket 中有三个选项和内核对应:
TCPKEEPIDLE: 覆盖 tcp_keepalive_time |
python demo
服务端启动:
nc -l 9999 |
客户端代码:
# -*- coding: utf-8 -*- |
在运行一段时间后通过iptables阻断通信:
iptables -A INPUT -p tcp --destination-port 9999 -j DROP |
抓包查看
使用tcpdump抓包可看到以下包的交互:
15:23:12.110546 IP 127.0.0.1.39455 > 127.0.0.1.9999: Flags [S], seq 4140259582, win 65495, options [mss 65495,sackOK,TS val 2989652584 ecr 0,nop,wscale 7], length 0 |
其中:
- 15:23:12.110546 - 15:23:12.110576为三次握手建立连接
- 15:23:12.110634 - 15:23:12.110642为数据发送接收ACK
- 15:23:17.110179 - 15:23:22.110200为5s探活一次,并接收到9999返回的ACK
- 15:23:26通过iptables阻断掉9999端口
- 15:23:27.110191 - 15:23:45.110449每隔2s发送一次探活包,但并无应答
- 15:23:47.110448发送RST包关闭连接
抓包分析结果基本与我们预期一致。
如何使用
KeepAlive in C
C中可以非常简单的通过系统调用对是否开启及3个参数进行配置。
C语言实现代码示例:
|
KeepAlive in Go
Go中net.TCPConn结构提供以下两个参数:
func (c *TCPConn) SetKeepAlive(keepalive bool) error // 开启关闭keepalive |
目前不支持设置TCP_KEEPCNT的,其它选项可以方便的配置,代码示例如下:
func SetKeepAlivePeriod(c *net.Conn, d time.Duration) error { |
注意
使用注意
- 合理配置,不要探测太频繁,太频繁是对网络资源的浪费
- 不要滥用,是对连接资源的浪费,资源池中还是应该使用合理的连接数
- 还是需要对连接的异常进程处理,不能完全保证连接的可用
与HTTP keepalive
TCP的keep alive与HTTP的keep alive不是一个概念。
HTTP的keep alive功能指的是HTTP 1.1后,请求默认是使用持久连接,即服务器在响应后保持连接,后续的请求通过该连接继续传输,减少建立连接的开销。
与重传
如果发送方发送的数据包没有收到接收方回复的ACK数据包,此时TCP keepalive机制并不会被启动,而是启动超时重传机制。而因为重传也算作网络传输,因此就使得TCP Keep-alive机制在未收到ACK包时失效。
与应用层心跳
不能取代应用层心跳。因为:
- keepalive某些场景下会失效
- 应用层心跳更有效,keepalive只能保证连接层的有效,而实际场景中连接可能有效,但服务已经无法响应
- 应用层心跳可用实现更个性化的功能
连接测试
既然连接池中的连接不能保证一定有些,那可不可以在每次使用前增加检测呢?例如jedispool就提供了TestOnBorrow、TestOnBorrow、TestWhileIdle三个选项,提供在特定场景下对连接测试的功能。
但是不建议使用。
原因是:
- 连接正常的场景下,相当于浪费一次RTT,增加了延迟
- 检测过后,也不能保证连接一定有效
因此还是建议开启keepalive配合完善的异常处理、重试的方式实现。
总结
TCP keepalive通过在空闲时发送ACK数据包,然后对方回应ACK来实现连接的包含。尤其适用于有中间设备的长连接通信。
使用方便,除设置选项后应用无需做出任何改变,也更轻量。但是不能完全取代应用层心跳。
适合用来做连接保活,很难用于做探活。