TCP是无感知的虚拟连接,中间断开两端不会立刻得到通知。一般在使用长连接的环境下,需要心跳保活机制可以勉强感知其存活。业务层面有心跳机制,TCP协议也提供了心跳保活机制。

长连接的环境下,人们一般使用业务层面或上层应用层协议(诸如MQTT,SOCKET.IO等)里面定义和使用。一旦有热数据需要传递,若此时连接已经被中介设备断开,应用程序没有及时感知的话,那么就会导致在一个无效的数据链路层面发送业务数据,结果就是发送失败。

无论是因为客户端意外断电、死机、崩溃、重启,还是中间路由网络无故断开、NAT超时等,服务器端要做到快速感知失败,减少无效链接操作。

一、协议解读

基于RFC1122#TCP Keep-Alives

  1. TCP Keepalive虽不是标准规范,但操作系统一旦实现,默认情况下须为关闭,可以被上层应用开启和关闭。
  2. TCP Keepalive必须在没有任何数据(包括ACK包)接收之后的周期内才会被发送,允许配置,默认值不能够小于2个小时
  3. 不包含数据的ACK段在被TCP发送时没有可靠性保证,意即一旦发送,不确保一定发送成功。系统实现不能对任何特定探针包作死连接对待
  4. 规范建议keepalive保活包不应该包含数据,但也可以包含1个无意义的字节,比如0x0。
  5. SEG.SEQ = SND.NXT-1,即TCP保活探测报文序列号将前一个TCP报文序列号减1。SND.NXT = RCV.NXT,即下一次发送正常报文序号等于ACK序列号;总之保活报文不在窗口控制范围内 有一张图,可以很容易说明,但请仔细观察Tcp Keepalive部分:

  1. 不太好的TCP堆栈实现,可能会要求保活报文必须携带有1个字节的数据负载
  2. TCP Keepalive应该在服务器端启用,客户端不做任何改动;若单独在客户端启用,若客户端异常崩溃或出现连接故障,存在服务器无限期的为已打开的但已失效的文件描述符消耗资源的严重问题。但在特殊的NFS文件系统环境下,需要客户端和服务器端都要启用Tcp Keepalive机制。
  3. TCP Keepalive不是TCP规范的一部分,有三点需要注意:
    • 在短暂的故障期间,它们可能引起一个良好连接(good connection)被释放(dropped)
    • 它们消费了不必要的宽带
    • 在以数据包计费的互联网消费(额外)花费金钱

二。Tcp keepalive 如何使用

以下环境是在Linux服务器上进行。应用程序若想使用,需要设置SO_KEEPALIVE套接口选项才能够生效。

1. 系统内核参数配置

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

发送频率tcp_keepalive_intvl乘以发送次数tcp_keepalive_probes,就得到了从开始探测到放弃探测确定连接断开的时间

若设置,服务器在客户端连接空闲的时候,每90秒发送一次保活探测包到客户端,若没有及时收到客户端的TCP Keepalive ACK确认,将继续等待15秒*2=30秒。总之可以在90s+30s=120秒(两分钟)时间内可检测到连接失效与否。

以下改动,需要写入到/etc/sysctl.conf文件:

net.ipv4.tcp_keepalive_time=90
net.ipv4.tcp_keepalive_intvl=15
net.ipv4.tcp_keepalive_probes=2

保存退出,然后执行sysctl -p生效。可通过 sysctl -a | grep keepalive 命令检测一下是否已经生效。

针对已经设置SO_KEEPALIVE的套接字,应用程序不用重启,内核直接生效。

2. Java/netty服务器如何使用

只需要在服务器端一方设置即可,客户端完全不用设置,比如基于netty 4服务器程序:

       ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 100)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(
new EchoServerHandler());
}
}); // Start the server.
ChannelFuture f = b.bind(port).sync(); // Wait until the server socket is closed.
f.channel().closeFuture().sync();

Java程序只能做到设置SO_KEEPALIVE选项,至于TCP_KEEPCNT,TCP_KEEPIDLE,TCP_KEEPINTVL等参数配置,只能依赖于sysctl配置,系统进行读取。

netty 4.1.0.final 可以做到连接级别的TCP_KEEPCNT,TCP_KEEPIDLE,TCP_KEEPINTVL等参数配置。

https://netty.io/4.1/api/io/netty/channel/epoll/EpollChannelOption.html

http://alvinalexander.com/java/jwarehouse/netty-4.1/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollSocketChannelConfig.java.shtml

还有调用c接口的实现jnasockopt:https://github.com/abligh/jnasockopt

A TCP keep-alive packet is an ACK with the sequence number set to one less than the current sequence number for the connection.

3. C语言如何设置

下面代码摘取自libkeepalive源码,C语言可以设置更为详细的TCP内核参数。

int socket(int domain, int type, int protocol)
{
int (*libc_socket)(int, int, int);
int s, optval;
char *env; *(void **)(&libc_socket) = dlsym(RTLD_NEXT, "socket");
if(dlerror()) {
errno = EACCES;
return -;
} if((s = (*libc_socket)(domain, type, protocol)) != -) {
if((domain == PF_INET) && (type == SOCK_STREAM)) {
if(!(env = getenv("KEEPALIVE")) || strcasecmp(env, "off")) {
optval = ;
} else {
optval = ;
}
if(!(env = getenv("KEEPALIVE")) || strcasecmp(env, "skip")) {
setsockopt(s, SOL_SOCKET, SO_KEEPALIVE, &optval, sizeof(optval));
}
#ifdef TCP_KEEPCNT
if((env = getenv("KEEPCNT")) && ((optval = atoi(env)) >= )) {
setsockopt(s, SOL_TCP, TCP_KEEPCNT, &optval, sizeof(optval));
}
#endif
#ifdef TCP_KEEPIDLE
if((env = getenv("KEEPIDLE")) && ((optval = atoi(env)) >= )) {
setsockopt(s, SOL_TCP, TCP_KEEPIDLE, &optval, sizeof(optval));
}
#endif
#ifdef TCP_KEEPINTVL
if((env = getenv("KEEPINTVL")) && ((optval = atoi(env)) >= )) {
setsockopt(s, SOL_TCP, TCP_KEEPINTVL, &optval, sizeof(optval));
}
#endif
}
} return s;
}

4. 针对已有程序没有硬编码KTTCP EEPALIVE实现

完全可以借助于第三方工具libkeepalive,通过LD_PRELOAD方式实现。比如

LD_PRELOAD=/the/path/libkeepalive.so java -jar /your/path/yourapp.jar &

这个工具还有一个比较方便的地方,可以直接在程序运行前指定TCP保活详细参数,可以省去配置sysctl.conf的麻烦:

LD_PRELOAD=/the/path/libkeepalive.so \
> KEEPCNT=20 \
> KEEPIDLE=180 \
> KEEPINTVL=60 \
> java -jar /your/path/yourapp.jar &

针对较老很久不更新的程序,可以尝试一下嘛。

三。Linux内核层面对keepalive处理

参数和定义

#define MAX_TCP_KEEPIDLE     32767
#define MAX_TCP_KEEPINTVL 32767
#define MAX_TCP_KEEPCNT 127
#define MAX_TCP_SYNCNT 127 #define TCP_KEEPIDLE 4 /* Start keeplives after this period */
#define TCP_KEEPINTVL 5 /* Interval between keepalives */
#define TCP_KEEPCNT 6 /* Number of keepalives before death */

net/ipv4/Tcp.c,可以找到对应关系:

     case TCP_KEEPIDLE:
val = (tp->keepalive_time ? : sysctl_tcp_keepalive_time) / HZ;
break;
case TCP_KEEPINTVL:
val = (tp->keepalive_intvl ? : sysctl_tcp_keepalive_intvl) / HZ;
break;
case TCP_KEEPCNT:
val = tp->keepalive_probes ? : sysctl_tcp_keepalive_probes;
break;

初始化:

 case TCP_KEEPIDLE:
if (val < || val > MAX_TCP_KEEPIDLE)
err = -EINVAL;
else {
tp->keepalive_time = val * HZ;
if (sock_flag(sk, SOCK_KEEPOPEN) &&
!(( << sk->sk_state) &
(TCPF_CLOSE | TCPF_LISTEN))) {
__u32 elapsed = tcp_time_stamp - tp->rcv_tstamp;
if (tp->keepalive_time > elapsed)
elapsed = tp->keepalive_time - elapsed;
else
elapsed = ;
inet_csk_reset_keepalive_timer(sk, elapsed);
}
}
break;
case TCP_KEEPINTVL:
if (val < || val > MAX_TCP_KEEPINTVL)
err = -EINVAL;
else
tp->keepalive_intvl = val * HZ;
break;
case TCP_KEEPCNT:
if (val < || val > MAX_TCP_KEEPCNT)
err = -EINVAL;
else
tp->keepalive_probes = val;
break;

这里可以找到大部分处理逻辑,net/ipv4/Tcp_timer.c:

static void tcp_keepalive_timer (unsigned long data)
{
struct sock *sk = (struct sock *) data;
struct inet_connection_sock *icsk = inet_csk(sk);
struct tcp_sock *tp = tcp_sk(sk);
__u32 elapsed; /* Only process if socket is not in use. */
bh_lock_sock(sk);
if (sock_owned_by_user(sk)) {
/* Try again later. */
inet_csk_reset_keepalive_timer (sk, HZ/);
goto out;
} if (sk->sk_state == TCP_LISTEN) {
tcp_synack_timer(sk);
goto out;
}
// 关闭状态的处理
if (sk->sk_state == TCP_FIN_WAIT2 && sock_flag(sk, SOCK_DEAD)) {
if (tp->linger2 >= ) {
const int tmo = tcp_fin_time(sk) - TCP_TIMEWAIT_LEN; if (tmo > ) {
tcp_time_wait(sk, TCP_FIN_WAIT2, tmo);
goto out;
}
}
tcp_send_active_reset(sk, GFP_ATOMIC);
goto death;
} if (!sock_flag(sk, SOCK_KEEPOPEN) || sk->sk_state == TCP_CLOSE)
goto out; elapsed = keepalive_time_when(tp); /* It is alive without keepalive 8) */
if (tp->packets_out || sk->sk_send_head)
goto resched; elapsed = tcp_time_stamp - tp->rcv_tstamp; if (elapsed >= keepalive_time_when(tp)) {
if ((!tp->keepalive_probes && icsk->icsk_probes_out >= sysctl_tcp_keepalive_probes) ||
(tp->keepalive_probes && icsk->icsk_probes_out >= tp->keepalive_probes)) {
tcp_send_active_reset(sk, GFP_ATOMIC);
tcp_write_err(sk); // 向上层应用汇报连接异常
goto out;
}
if (tcp_write_wakeup(sk) <= ) {
icsk->icsk_probes_out++; // 这里仅仅是计数,并没有再次发送保活探测包
elapsed = keepalive_intvl_when(tp);
} else {
/* If keepalive was lost due to local congestion,
* try harder.
*/
elapsed = TCP_RESOURCE_PROBE_INTERVAL;
}
} else {
/* It is tp->rcv_tstamp + keepalive_time_when(tp) */
elapsed = keepalive_time_when(tp) - elapsed;
} TCP_CHECK_TIMER(sk);
sk_stream_mem_reclaim(sk); resched:
inet_csk_reset_keepalive_timer (sk, elapsed);
goto out; death:
tcp_done(sk); out:
bh_unlock_sock(sk);
sock_put(sk);
}

keepalive_intvl_when 函数定义:

static inline int keepalive_intvl_when(const struct tcp_sock *tp)
{
return tp->keepalive_intvl ? : sysctl_tcp_keepalive_intvl;
}

四。TCP Keepalive 引发的错误

启用TCP Keepalive的应用程序,一般可以捕获到下面几种类型错误

  1. ETIMEOUT 超时错误,在发送一个探测保护包经过(tcp_keepalive_time + tcp_keepalive_intvl * tcp_keepalive_probes)时间后仍然没有接收到ACK确认情况下触发的异常,套接字被关闭

    java.io.IOException: Connection timed out
  2. EHOSTUNREACH host unreachable(主机不可达)错误,这个应该是ICMP汇报给上层应用的。
    java.io.IOException: No route to host
  3. 链接被重置,终端可能崩溃死机重启之后,接收到来自服务器的报文,然物是人非,前朝往事,只能报以无奈重置宣告之。
    java.io.IOException: Connection reset by peer

五。常见的使用模式

  1. 默认情况下使用keepalive周期为2个小时,如不选择更改,属于误用范畴,造成资源浪费:内核会为每一个连接都打开一个保活计时器,N个连接会打开N个保活计时器优势很明显:
  • TCP协议层面保活探测机制,系统内核完全替上层应用自动给做好了
  • 内核层面计时器相比上层应用,更为高效
  • 上层应用只需要处理数据收发、连接异常通知即可
  • 数据包将更为紧凑
  1. 关闭TCP的keepalive,完全使用业务层面心跳保活机制 完全应用掌管心跳,灵活和可控,比如每一个连接心跳周期的可根据需要减少或延长
  2. 业务心跳 + TCP keepalive一起使用,互相作为补充,但TCP保活探测周期和应用的心跳周期要协调,以互补方可,不能够差距过大,否则将达不到设想的效果。朋友的公司所做IM平台业务心跳2-5分钟智能调整 + tcp keepalive 300秒,组合协作,据说效果也不错。

虽然说没有固定的模式可遵循,那么有以下原则可以参考:

  • 不想折腾,那就弃用TCP Keepalive吧,完全依赖应用层心跳机制,灵活可控性强
  • 除非可以很好把控TCP Keepalive机制,那就可以根据需要自由使用吧

六。注意和 HTTP的Keep-Alive区别

  • HTTP协议的Keep-Alive意图在于连接复用,同一个连接上串行方式传递请求-响应数据
  • TCP的keepalive机制意图在于保活、心跳,检测连接错误。

七。引用

  1. TCP Keepalive HOWTO

转自:

http://www.blogjava.net/yongboy/archive/2015/04/14/424413.html

TCP Keepalive笔记的更多相关文章

  1. 随手记之TCP Keepalive笔记-tcp_keepalive_timer

    这里可以找到大部分处理逻辑,net/ipv4/Tcp_timer.c: static void tcp_keepalive_timer (unsigned long data) { struct so ...

  2. TCP keepalive overview

    2. TCP keepalive overview In order to understand what TCP keepalive (which we will just call keepali ...

  3. 【转载】TCP保活(TCP keepalive)

    下图是我遇到tcp keepalive的例子: 以下为转载: TCP保活的缘起 双方建立交互的连接,但是并不是一直存在数据交互,有些连接会在数据交互完毕后,主动释放连接,而有些不会,那么在长时间无数据 ...

  4. TCP keepalive under Linux

    TCP Keepalive HOWTO Prev   Next 3. Using TCP keepalive under Linux Linux has built-in support for ke ...

  5. TCP keepalive

      2. TCP keepalive overview In order to understand what TCP keepalive (which we will just call keepa ...

  6. TCP连接探测中的Keepalive和心跳包. 关键字: tcp keepalive, 心跳, 保活

    1. TCP保活的必要性 1) 很多防火墙等对于空闲socket自动关闭 2) 对于非正常断开, 服务器并不能检测到. 为了回收资源, 必须提供一种检测机制. 2. 导致TCP断连的因素 如果网络正常 ...

  7. TCP Keepalive HOWTO

    TCP Keepalive HOWTO Fabio Busatto <fabio.busatto@sikurezza.org> 2007-05-04 Revision History Re ...

  8. 【 总结 】Tcp Keepalive 和 HTTP Keepalive 详解

    TCP Keepalive Tcp keepalive的起源          双方建立交互的连接,但是并不是一直存在数据交互,有些连接会在数据交互完毕后,主动释放连接,而有些不会,那么在长时间无数据 ...

  9. tcp keepalive选项

    之前一直对tcp keepalive选项理解有误, 以为通过setsockopt函数设置SO_KEEPALIVE和相关参数后该socket则使用设置的keepalive相关参数 否则使用系统默认的:k ...

随机推荐

  1. Java 之 Response 对象

    Response 对象 一.Response 原理和继承结构 原理和继承结构参考 request. 二.Response 对象 Response 就是用来设置响应消息. 1.设置响应行   响应头格式 ...

  2. js switch case 判断的是绝对相对===,值和类型都要相等

    js switch case 判断的是绝对相对===,值和类型都要相等

  3. Linux kernel启动选项(参数)(转)

    Linux kernel启动选项(参数)  转载链接https://www.cnblogs.com/linuxbo/p/4286227.html 在Linux中,给kernel传递参数以控制其行为总共 ...

  4. MySQL用户与权限

    用户连接到mysql,并做各种查询,在用户和服务器中间分为两个阶段: 1:用户是否有权连接上来 2:用户是否有权执行此操作(如select,update等等) 先看第一个阶段:服务器如何判断用户是否有 ...

  5. DP-LIS and LCS

    最长上升子串 f[i]=f[I-1]+1(f[I]>f[I-1]) f[I]=1;(f[I]<=f[I-1]) 输出max(f(I)) 最长上升子序列 f[I]=max(f[I],f[j] ...

  6. c语言实现基本的数据结构(二) 链表(包括链表的三种简单排序算法)

    #include "stdafx.h" #include <stdlib.h> //创建一个节点,data为value,指向NULL Node* Create(int ...

  7. drf框架(2)

    drf框架 """接口: 接口规范: drf的生命周期: 序列化组件: 三大认证:过滤,删选,排序组件 请求,响应,解析,异常 jwt:json web tooken & ...

  8. oracle 年龄分档,不用case when 的方法

    一般我们出分档数据都是case when ,但是如果是对年龄等一些字段进行细分,比如五岁一档,我们如果用case when就会特别麻烦,写的特别多,这里我介绍一种简单的方法,对细分的字段进行处理: 建 ...

  9. B君的历史——复数乘法&&爆搜

    题意 设 $r = \frac{-1+\sqrt7 i}{2}$,对任意整数 $x, y$ 都可以找到一个有限的整数集合 $S$,使得 $$x + y\sqrt7 i = \sum_{k \in S ...

  10. LeetCode 1087. Brace Expansion

    原题链接在这里:https://leetcode.com/problems/brace-expansion/ 题目: A string S represents a list of words. Ea ...