可能不少 Java 程序员对 TCP 的理解只有一个三次握手,四次握手的认识,我以为这样的缘由主要在于 TCP 协议自己稍微有点抽象(相比较于应用层的 HTTP 协议);其次,非框架开发者不太须要接触到 TCP 的一些细节。其实我我的对 TCP 的不少细节也并无彻底理解,这篇文章主要针对微信交流群里有人提出的长链接,心跳问题,作一个统一的整理。java
在 Java 中,使用 TCP 通讯,大几率会涉及到 Socket、Netty,本文将借用它们的一些 API 和设置参数来辅助介绍。nginx
TCP 自己并无长短链接的区别,长短与否,彻底取决于咱们怎么用它。程序员
短链接和长链接的优点,分别是对方的劣势。想要图简单,不追求高性能,使用短链接合适,这样咱们就不须要操心链接状态的管理;想要追求性能,使用长链接,咱们就须要担忧各类问题:好比端对端链接的维护,链接的保活。算法
长链接还经常被用来作数据的推送,咱们大多数时候对通讯的认知仍是 request/response 模型,但 TCP 双工通讯的性质决定了它还能够被用来作双向通讯。在长链接之下,能够很方便的实现 push 模型,长链接的这一特性在本文并不会进行探讨,有兴趣的同窗能够专门去搜索相关的文章。bootstrap
短链接没有太多东西能够讲,因此下文咱们将目光聚焦在长链接的一些问题上。纯讲理论未免有些过于单调,因此下文我借助一些 RPC 框架的实践来展开 TCP 的相关讨论。服务器
前面已经提到过,追求性能时,必然会选择使用长链接,因此借助 Dubbo 能够很好的来理解 TCP。咱们开启两个 Dubbo 应用,一个 server 负责监听本地 20880 端口(众所周知,这是 Dubbo 协议默认的端口),一个 client 负责循环发送请求。执行 lsof -i:20880
命令能够查看端口的相关使用状况:微信
*:20880 (LISTEN)
说明了 Dubbo 正在监听本地的 20880 端口,处理发送到本地 20880 端口的请求open too many files
的异常,那就应该检查一下,你是否是建立了太多链接,而没有关闭。细心的读者也会联想到长链接的另外一个好处,那就是会占用较少的文件句柄。由于客户端请求的服务可能分布在多个服务器上,客户端天然须要跟对端建立多条长链接,咱们遇到的第一个问题就是如何维护长链接。网络
// 客户端
public class NettyHandler extends SimpleChannelHandler {
private final Map<String, Channel> channels = new ConcurrentHashMap<String, Channel>(); // <ip:port, channel>
}
// 服务端
public class NettyServer extends AbstractServer implements Server {
private Map<String, Channel> channels; // <ip:port, channel>
}
复制代码
在 Dubbo 中,客户端和服务端都使用 ip:port
维护了端对端的长链接,Channel 即是对链接的抽象。咱们主要关注 NettyHandler 中的长链接,服务端同时维护一个长链接的集合是 Dubbo 的额外设计,咱们将在后面提到。框架
这里插一句,解释下为何我认为客户端的链接集合要重要一点。TCP 是一个双向通讯的协议,任一方均可以是发送者,接受者,那为何还抽象了 Client 和 Server 呢?由于创建链接这件事就跟谈念爱同样,必需要有主动的一方,你主动咱们就会有故事。Client 能够理解为主动创建链接的一方,实际上两端的地位能够理解为是对等的。socket
这个话题就有的聊了,会牵扯到比较多的知识点。首先须要明确一点,为何须要链接的保活?当双方已经创建了链接,但由于网络问题,链路不通,这样长链接就不能使用了。须要明确的一点是,经过 netstat,lsof 等指令查看到链接的状态处于 ESTABLISHED
状态并非一件很是靠谱的事,由于链接可能已死,但没有被系统感知到,更不用提假死这种疑难杂症了。若是保证长链接可用是一件技术活。
首先想到的是 TCP 中的 KeepAlive 机制。KeepAlive 并非 TCP 协议的一部分,可是大多数操做系统都实现了这个机制(因此须要在操做系统层面设置 KeepAlive 的相关参数)。KeepAlive 机制开启后,在必定时间内(通常时间为 7200s,参数 tcp_keepalive_time
)在链路上没有数据传送的状况下,TCP 层将发送相应的 KeepAlive 探针以肯定链接可用性,探测失败后重试 10(参数 tcp_keepalive_probes
)次,每次间隔时间 75s(参数 tcp_keepalive_intvl
),全部探测失败后,才认为当前链接已经不可用。
在 Netty 中开启 KeepAlive:
bootstrap.option(ChannelOption.SO_KEEPALIVE, true)
复制代码
Linux 操做系统中设置 KeepAlive 相关参数,修改 /etc/sysctl.conf
文件:
net.ipv4.tcp_keepalive_time=90
net.ipv4.tcp_keepalive_intvl=15
net.ipv4.tcp_keepalive_probes=2
复制代码
KeepAlive 机制是在网络层面保证了链接的可用性,但站在应用框架层面咱们认为这还不够。主要体如今三个方面:
/etc/sysctl.conf
配置中,这对于应用来讲不够灵活。ESTABLISHED
,这时会发生什么?天然会走 TCP 重传机制,要知道默认的 TCP 超时重传,指数退避算法也是一个至关长的过程。咱们已经为应用层面的链接保活作了足够的铺垫,下面就来一块儿看看,怎么在应用层作链接保活。
终于点题了,文题中提到的心跳即是一个本文想要重点强调的另外一个重要的知识点。上一节咱们已经解释过了,网络层面的 KeepAlive 不足以支撑应用级别的链接可用性,本节就来聊聊应用层的心跳机制是实现链接保活的。
如何理解应用层的心跳?简单来讲,就是客户端会开启一个定时任务,定时对已经创建链接的对端应用发送请求(这里的请求是特殊的心跳请求),服务端则须要特殊处理该请求,返回响应。若是心跳持续屡次没有收到响应,客户端会认为链接不可用,主动断开链接。不一样的服务治理框架对心跳,建连,断连,拉黑的机制有不一样的策略,但大多数的服务治理框架都会在应用层作心跳,Dubbo/HSF 也不例外。
以 Dubbo 为例,支持应用层的心跳,客户端和服务端都会开启一个 HeartBeatTask
,客户端在 HeaderExchangeClient
中开启,服务端将在 HeaderExchangeServer
开启。文章开头埋了一个坑:Dubbo 为何在服务端同时维护 Map<String,Channel>
呢?主要就是为了给心跳作贡献,心跳定时任务在发现链接不可用时,会根据当前是客户端仍是服务端走不一样的分支,客户端发现不可用,是重连;服务端发现不可用,是直接 close。
// HeartBeatTask
if (channel instanceof Client) {
((Client) channel).reconnect();
} else {
channel.close();
}
复制代码
Dubbo 2.7.x 相比 2.6.x 作了定时心跳的优化,使用
HashedWheelTimer
更加精准的控制了只在链接闲置时发送心跳。
再看看 HSF 的实现,并无设置应用层的心跳,准确的说,是在 HSF2.2 以后,使用 Netty 提供的 IdleStateHandler
更加优雅的实现了应用的心跳。
ch.pipeline()
.addLast("clientIdleHandler", new IdleStateHandler(getHbSentInterval(), 0, 0));
复制代码
处理 userEventTriggered
中的 IdleStateEvent
事件
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
callConnectionIdleListeners(client, (ClientStream) StreamUtils.streamOfChannel(ctx.channel()));
} else {
super.userEventTriggered(ctx, evt);
}
}
复制代码
对于客户端,HSF 使用 SendHeartbeat
来进行心跳,每次失败累加心跳失败的耗时,当超过最大限制时断开乱接;对于服务端 HSF 使用 CloseIdle
来处理闲置链接,直接关闭链接。通常来讲,服务端的闲置时间会设置的稍长。
熟悉其余 RPC 框架的同窗会发现,不一样框架的心跳机制真的是差距很是大。心跳设计还跟链接建立,重连机制,黑名单链接相关,还须要具体框架具体分析。
除了定时任务的设计,还须要在协议层面支持心跳。最简单的例子能够参考 nginx 的健康检查,而针对 Dubbo 协议,天然也须要作心跳的支持,若是将心跳请求识别为正常流量,会形成服务端的压力问题,干扰限流等诸多问题。
其中 Flag 表明了 Dubbo 协议的标志位,一共 8 个地址位。低四位用来表示消息体数据用的序列化工具的类型(默认 hessian),高四位中,第一位为1表示是 request 请求,第二位为 1 表示双向传输(即有返回response),第三位为 1 表示是心跳事件。
心跳请求应当和普通请求区别对待。
这压根是两个概念。
启用 TCP KeepAlive 的应用程序,通常能够捕获到下面几种类型错误
ETIMEOUT 超时错误,在发送一个探测保护包通过 (tcp_keepalive_time + tcp_keepalive_intvl * tcp_keepalive_probes)时间后仍然没有接收到 ACK 确认状况下触发的异常,套接字被关闭
java.io.IOException: Connection timed out
复制代码
EHOSTUNREACH host unreachable(主机不可达)错误,这个应该是 ICMP 汇报给上层应用的。
java.io.IOException: No route to host
复制代码
连接被重置,终端可能崩溃死机重启以后,接收到来自服务器的报文,然物是人非,前朝往事,只能报以无奈重置宣告之。
java.io.IOException: Connection reset by peer
复制代码
有三种使用 KeepAlive 的实践方案:
各个框架的设计都有所不一样,例如 Dubbo 使用的是方案三,但阿里内部的 HSF 框架则没有设置 TCP 的 KeepAlive,仅仅由应用心跳保活。和心跳策略同样,这和框架总体的设计相关。
欢迎关注个人微信公众号:「Kirito的技术分享」