相关背景:

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方法;

TCP 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

 

 

 

GitHub 加速计划 / li / linux-dash
10.39 K
1.2 K
下载
A beautiful web dashboard for Linux
最近提交(Master分支:2 个月前 )
186a802e added ecosystem file for PM2 4 年前
5def40a3 Add host customization support for the NodeJS version 4 年前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐