暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

浅谈TCP的keep-alive机制

风筝Lee聊大数据 2020-11-16
2280

相关背景:

hbase集群大量regionserver节点进程挂掉,排查log发现每个节点上的有大量的和datanode建立连接失败的报错信息,进一步排查是大量的Too Many Open Files异常导致的,所以挂掉的原因是Linux服务器上的File Descriptor数量不足,无法新建socket连接去读写datanode数据导致进程退出。每台服务器上处于CLOSE_WAIT状态的tcp连接有10万+,基本都是regionserver访问datanode产生的大量tcp连接;简单回顾一下TCP断开连接的过程,主动方发送FIN包请求关闭连接,被动关闭的一方响应并发出 ACK 包之后就进入了 CLOSE_WAIT 状态。如果一切正常,稍后被动关闭的一方也会发出 FIN 包,然后迁移到 LAST_ACK 状态。因此,CLOSE_WAIT产生的原因是被动关闭的一方收到FIN请求后没有发送FIN包,也就是没有执行close方法;


对应到当前问题,regionserver大量访问Datanode服务进程,Datanode端已经由于某种原因(超时等)主动关闭了socket连接,但是regionserver端没有调用close去释放当前的socket连接。

所以解决的问题就是regionserver关闭与datanode的socket连接的问题;参考hadoop和hbase社区已经有了相关的解决方案,大概思路是regonserver端在访问hdfs数据后调用hdfs接口释放掉无用socket连接;

    https://issues.apache.org/jira/browse/HBASE-9393(Region Server fails to properly close socket resulting in many CLOSE_WAIT to Data Nodes)
    https://issues.apache.org/jira/browse/HDFS-7694(FSDataInputStream should support "unbuffer"
    复制

    CLOSE_WAIT

    CLOSE_WAIT 是TCP关闭连接过程中的一个正常状态,通常情况下,CLOSE_WAIT 状态维持时间很短,如果你发现TCP连接长时间的处于CLOSE_WAIT 状态,那么就意味着被动关闭的一方没有及时发出 FIN 包,说明代码层面存在一定的问题,比如代码本身没有close socket的调用逻辑、或者逻辑错误导致无法执行到close方法、或者双方超时设置问题导致一方发生timeout直接关闭连接,另外一方长时间处理业务逻辑导致close socket调用被延后等等;

    大量CLOSE_WAIT的连接堆积存在很大的隐患问题,如果CLOSE_WAIT状态连接的一直保持着,那么意味着对应数据的通道就一直被占用,典型的”占着茅坑不拉屎“,因为linux分配给每一个用户的文件句柄是有限的,一旦达到句柄数上限,新的连接请求就无法被处理,请求就会报大量的Too Many Open Files异常,从而导致服务异常;另外可以手动调高用户级别的文件句柄数量配置,但是没有解决根本问题,同时分配的值过大的话反而会影响操作系统性能,所以需要根据具体应用调配权衡。

    回到本文重点,大量异常连接问题,其实都可以通过操作系统的keepalive机制进行处理。在一个idle TCP连接上,没有任何的数据流,当另外一端服务器出现问题时(例如断电),TCP无法知晓连接是否出现异常,如果在本端的socket上应用keepalive机制的话,可以彻底解决这个问题,keepalive机制会在空闲一段时间之后发送探测packet进行检测,从而发现连接异常,下面开始详细介绍tcp的keepalive机制。


    keepalive机制

    TCP保活机制,就是为了保证连接的有效性,探测连接的对端是否存活的作用,在间隔一定的时间发探测包,根据回复来确认该连接是否有效。通常上层应用会自己提供心跳检测机制,而Linux内核本身也提供了从内核层面的确保连接有效性的方式。

    在双方交互过程中,可能存在以下的几种情况:

    • 客户端或者服务端意外断电、死机、进程挂掉重启等;

    • 中间网络出现问题,连接双方无法知道一直等待;

    • 程序问题导致的长时间CLOSE_WAIT问题;

    此时,tcp keep-alive机制就可以解决大量无用连接无法回收、占用资源的问题了. KeepAlive并不是TCP协议规范的一部分,但在几乎所有的TCP/IP协议栈(不管是Linux还是Windows)中,都实现了KeepAlive功能,本片文章主要是基于linux操作系统上来进行说明。

     

    系统内核参数配置

    通过 sysctl -a | grep keepalive 命令查看

      net.ipv4.tcp_keepalive_intvl = 75 
      net.ipv4.tcp_keepalive_probes = 9
      net.ipv4.tcp_keepalive_time = 7200
      复制

      参数解释:

      • tcp_keepalive_time,在TCP保活打开的情况下,最后一次数据交换到TCP发送第一个保活探测包的间隔,即允许的持续空闲时长,或者说每次正常发送心跳的周期,默认值为7200s(2h)。

      • tcp_keepalive_probes 在tcp_keepalive_time之后,没有接收到对方确认,继续发送保活探测包次数,默认值为9(次)

      • tcp_keepalive_intvl,在tcp_keepalive_time之后,没有接收到对方确认,继续发送保活探测包的发送频率,默认值为75s。

      然后我结合相关的内核参数梳理下keep-alive机制的原理流程,如下面两个图所示

                                                 图一. 正常ack,保持连接


                                            图二. 对方响应rst,释放连接 


                                          图三. 对方服务无响应,释放连接 


      所以可以这样理解:按照默认值,在一个TCP 连接上,tcp_keepalive_time(2h)时间内没有任何数据包传输,则开启keepalive的一端发送keepalive探测包,三种情况:1. 对端正常发送ack,继续保持连接,重置保活定时器;2. 对端能正常响应,但是发送的是RST包,证明对端服务出现问题,于是发送RST终止当前连接,本端释放连接;3.则如果没有收到应答,则间隔tcp_keepalive_intvl的时间再次发送,经过tcp_keepalive_probes的次数后仍然没有收到应答,则发送 RST包关闭该连接。

      可以通过 sysctl -w 来修改内核参数,或者修改/etc/sysctl.conf 后 sysctl -p 让参数生效,针对已经设置SO_KEEPALIVE的套接字,应用程序不用重启,内核直接生效。应用程序若想使用需要设置SO_KEEPALIVE套接口选项才能够生效。

      源码解析:

      下面通过java socket代码分析,java socket 的setKeepAlive源码:

        /**
        * Enable/disable {@link SocketOptions#SO_KEEPALIVE SO_KEEPALIVE}.
        *
        * @param on whether or not to have socket keep alive turned on.
        * @exception SocketException if there is an error
        * in the underlying protocol, such as a TCP error.
        * @since 1.3
        * @see #getKeepAlive()
        */
        public void setKeepAlive(boolean on) throws SocketException {
        if (isClosed())
        throw new SocketException("Socket is closed");
        getImpl().setOption(SocketOptions.SO_KEEPALIVE, Boolean.valueOf(on));
        }
        复制


        SocketOptions相关代码:

          /**
          * When the keepalive option is set for a TCP socket and no data
          * has been exchanged across the socket in either direction for
          * 2 hours (NOTE: the actual value is implementation dependent),
          * TCP automatically sends a keepalive probe to the peer. This probe is a
          * TCP segment to which the peer must respond.
          * One of three responses is expected:
          * 1. The peer responds with the expected ACK. The application is not
          * notified (since everything is OK). TCP will send another probe
          * following another 2 hours of inactivity.
          * 2. The peer responds with an RST, which tells the local TCP that
          * the peer host has crashed and rebooted. The socket is closed.
          * 3. There is no response from the peer. The socket is closed.
          *
          * The purpose of this option is to detect if the peer host crashes.
          *
          * Valid only for TCP socket: SocketImpl
          *
          * @see Socket#setKeepAlive
          * @see Socket#getKeepAlive
          */
          @Native public final static int SO_KEEPALIVE = 0x0008;
          复制


          可见,Java程序只能做到设置SO_KEEPALIVE选项,至于TCP_KEEPCNT,TCP_KEEPIDLE,TCP_KEEPINTVL等参数配置,只能依赖于sysctl在系统层面进行配置;

          Java 对于客户端中打开keep alive直接调用Socket.setKeepAlive函数,而在服务器端的ServerSocket 却不允许设置keep alive的开关,只能在accept 一个新的连接的socket 的时候设置。

           

          C语言中,在sock 函数中设置keep alive开关,默认socket 是关闭keep alive的。

            int opt = 1;
            setsockopt(sockfd, SOL_SOCKET, SO_KEEPALIVE, (void*)&opt, sizeof(opt));
            复制

            linux tcp keepalive机制相关源码实现可查看net/core/sock.c、net/ipv4/tcp_timer.c、net/ipv4/tcp_timer.c


            重点总结

            a. java层面开启keepalive需要通过socket实例调用setKeepAlive进行设置(建议在两端均设置),只能配置开关,其他参数依赖于sysctl在系统层面进行配置。

            b. C语言开启keepalive需要在socket实例上调用setsockopt设置。

            c. 调整keepalive内核参数后对现有已打开keepalive机制的socket链接直接生效,无需重启。

            d. tcp keep-alive机制可以解决大量连接无法回收、占用资源的问题.




            文章转载自风筝Lee聊大数据,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

            评论