IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    随手记之TCP Keepalive笔记

    nieyong发表于 2015-04-14 09:08:00
    love 0

    零。前言

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

    一。TCP Keepalive解读

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

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

    1. 交互过程

    2. 协议解读

    下面协议解读,基于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() {
                     @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配置,系统进行读取。

    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 -1;
      }
    
      if((s = (*libc_socket)(domain, type, protocol)) != -1) {
        if((domain == PF_INET) && (type == SOCK_STREAM)) {
          if(!(env = getenv("KEEPALIVE")) || strcasecmp(env, "off")) {
            optval = 1;
          } else {
            optval = 0;
          }
          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)) >= 0)) {
            setsockopt(s, SOL_TCP, TCP_KEEPCNT, &optval;, sizeof(optval));
          }
    #endif
    #ifdef TCP_KEEPIDLE
          if((env = getenv("KEEPIDLE")) && ((optval = atoi(env)) >= 0)) {
            setsockopt(s, SOL_TCP, TCP_KEEPIDLE, &optval;, sizeof(optval));
          }
    #endif
    #ifdef TCP_KEEPINTVL
          if((env = getenv("KEEPINTVL")) && ((optval = atoi(env)) >= 0)) {
            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 < 1 || val > MAX_TCP_KEEPIDLE)
               err = -EINVAL;
          else {
               tp->keepalive_time = val * HZ;
               if (sock_flag(sk, SOCK_KEEPOPEN) &&
                   !((1 << 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 = 0;
                    inet_csk_reset_keepalive_timer(sk, elapsed);
               }
          }
          break;
     case TCP_KEEPINTVL:
          if (val < 1 || val > MAX_TCP_KEEPINTVL)
               err = -EINVAL;
          else
               tp->keepalive_intvl = val * HZ;
          break;
     case TCP_KEEPCNT:
          if (val < 1 || 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/20);
              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 >= 0) {
                   const int tmo = tcp_fin_time(sk) - TCP_TIMEWAIT_LEN;
    
                   if (tmo > 0) {
                        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) <= 0) {
                   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保活
    2. TCP Keepalive HOWTO


    nieyong 2015-04-14 17:08 发表评论


沪ICP备19023445号-2号
友情链接