浅谈TCP的keep-alive机制
相关背景:
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
更多推荐
所有评论(0)