在网络传输中,怎么确保通道连接的可用性是一个很重要的问题,简单的说,在网络通信中有客户端和服务端,一个负责发送请求,一个负责接收请求,在保证连接有效性的背景下,这两个物体扮演了什么角色,心跳机制能有效的保证连接的可用性,那它的机制是什么,下文中将会详细讲解。

在网络传输中,怎么确保通道连接的可用性是一个很重要的问题,简单的说,在网络通信中有客户端和服务端,一个负责发送请求,一个负责接收请求,在保证连接有效性的背景下,这两个物体扮演了什么角色,心跳机制能有效的保证连接的可用性,那它的机制是什么,下文中将会详细讲解。

首先讲一下TCP,在dubbo中的通信是基于TCP的,TCP本身并没有长短连接的区别,在短连接中,每次通信时,都会创建Socket,当该次通信结束后,就会调用socket.close();而在长连接中,每次通信完毕后,不会关闭连接,这样就可以做到连接的复用,长连接的好处是省去了创建连接时的耗时。那么如何确保连接的有效性呢,在TCP中用到了KeepAlive机制,keepalive并不是TCP协议的一部分,但是大多数操作系统都实现了这个机制,在一定时间内,在链路上如果没有数据传送的情况下,TCP层将会发送相应的keepalive探针来确定连接可用性,探测失败后重试10次(tcp_keepalive_probes),每次间隔时间为75s(tcp_keepalive_intvl),所有探测失败后,才认为当前连接已经不可用了。

KeepAlive机制是在网络层保证了连接的可用性,但在应用层我们认为这还是不够的。

  • KeepAlive的报活机制只有在链路空闲的情况下才会起作用,假如此时有数据发送,且物理链路已经不通,操作系统这边的链路状态还是E STABLISHED,这时会发生TCP重传机制,要知道默认的TCP超时重传,指数退避算法也是一个相当长的过程。
  • KeepAlive本身是面向网络的,并不是面向于应用的,可能是由于本身GC问题,系统load高等情况,但网络依然是通的,此时,应用已经失去了活性,所以连接自然认为是不可用的。

    应用层的连接可用性:心跳机制

    如何理解应用层的心跳?简单的说,就是客户端会开启一个定时任务,定时对已经建立连接的对端应用发送请求,服务端则需要特殊处理该请求,返回响应。如果心跳持续多次没有收到响应,客户端会认为连接不可用,主动断开连接。

客户端如何得知请求失败了?

在失败的场景下,服务端是不会返回响应的,所以只能在客户端自身上设计了。
当客户端发起一个RPC请求时,会设置一个超时时间client_timeout,同时它也会开启一个延迟的client_timeout的定时器。当接收到正常响应时,会移除该定时器;而当计时器倒计时完毕后,还没有被移除,则会认为请求超时,构造一个失败的响应传递给客户端。

连接建立时创建定时器

HeaderExchangeClient类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public (Client client, boolean needHeartbeat) {
if (client == null) {
throw new IllegalArgumentException("client == null");
}
this.client = client; this.channel = new HeaderExchangeChannel(client);
// 获得dubbo版本
String dubbo = client.getUrl().getParameter(Constants.DUBBO_VERSION_KEY);
//获得心跳周期配置,如果没有配置,并且dubbo是1.0版本的,则这只为1分钟,否则设置为0
this.heartbeat = client.getUrl().getParameter(Constants.HEARTBEAT_KEY, dubbo != null && dubbo.startsWith("1.0.") ? Constants.DEFAULT_HEARTBEAT : 0);
// 获得心跳超时配置,默认是心跳周期的三倍
this.heartbeatTimeout = client.getUrl().getParameter(Constants.HEARTBEAT_TIMEOUT_KEY, heartbeat * 3); if (needHeartbeat) {
// 开启心跳
long tickDuration = calculateLeastDuration(heartbeat);
heartbeatTimer = new HashedWheelTimer(new NamedThreadFactory("dubbo-client-heartbeat", true) , tickDuration, TimeUnit.MILLISECONDS, Constants.TICKS_PER_WHEEL);
startHeartbeatTimer();
}
}

创建了一个HashedWheelTimer开启心跳检测,这是 Netty 所提供的一个经典的时间轮定时器实现。

HeaderExchangeServer也同时开启了定时器,代码逻辑和上述差不多。

开启两个定时任务

1
2
3
4
5
6
7
8
9
private void startHeartbeatTimer() {
long heartbeatTick = calculateLeastDuration(heartbeat);
long heartbeatTimeoutTick = calculateLeastDuration(heartbeatTimeout);
HeartbeatTimerTask heartBeatTimerTask =new HeartbeatTimerTask(cp, heartbeatTick, heartbeat);
ReconnectTimerTask reconnectTimerTask = new ReconnectTimerTask(cp, heartbeatTimeoutTick, heartbeatTimeout); heartbeatTimer.newTimeout(heartBeatTimerTask, heartbeatTick, TimeUnit.MILLISECONDS);
heartbeatTimer.newTimeout(reconnectTimerTask, heartbeatTimeoutTick, TimeUnit.MILLISECONDS);
}

在该方法中主要开启了两个定时器

  • HeartbeatTimerTask 主要是定时发送心跳请求
  • ReconnectTimerTask 主要是心跳失败后处理重连,断连的逻辑

旧版的心跳处理HeartBeatTask类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
final class HeartBeatTask implements Runnable {

    private static final Logger logger = LoggerFactory.getLogger(HeartBeatTask.class);

    /**
* 通道管理
*/
private ChannelProvider channelProvider; /**
* 心跳间隔 单位:ms
*/
private int heartbeat; /**
* 心跳超时时间 单位:ms
*/
private int heartbeatTimeout; HeartBeatTask(ChannelProvider provider, int heartbeat, int heartbeatTimeout) {
this.channelProvider = provider;
this.heartbeat = heartbeat;
this.heartbeatTimeout = heartbeatTimeout;
} public void run() {
try {
long now = System.currentTimeMillis();
// 遍历所有通道
for (Channel channel : channelProvider.getChannels()) {
// 如果通道关闭了,则跳过
if (channel.isClosed()) {
continue;
}
try {
// 最后一次接收到消息的时间戳
Long lastRead = (Long) channel.getAttribute(
HeaderExchangeHandler.KEY_READ_TIMESTAMP);
// 最后一次发送消息的时间戳
Long lastWrite = (Long) channel.getAttribute(
HeaderExchangeHandler.KEY_WRITE_TIMESTAMP);
// 如果最后一次接收或者发送消息到时间到现在的时间间隔超过了心跳间隔时间
if ((lastRead != null && now - lastRead > heartbeat)
|| (lastWrite != null && now - lastWrite > heartbeat)) {
// 创建一个request
Request req = new Request();
// 设置版本号
req.setVersion(Version.getProtocolVersion());
// 设置需要得到响应
req.setTwoWay(true);
// 设置事件类型,为心跳事件
req.setEvent(Request.HEARTBEAT_EVENT);
// 发送心跳请求
channel.send(req);
if (logger.isDebugEnabled()) {
logger.debug("Send heartbeat to remote channel " + channel.getRemoteAddress()
+ ", cause: The channel has no data-transmission exceeds a heartbeat period: " + heartbeat + "ms");
}
}
// 如果最后一次接收消息的时间到现在已经超过了超时时间
if (lastRead != null && now - lastRead > heartbeatTimeout) {
logger.warn("Close channel " + channel
+ ", because heartbeat read idle time out: " + heartbeatTimeout + "ms");
// 如果该通道是客户端,也就是请求的服务器挂掉了,客户端尝试重连服务器
if (channel instanceof Client) {
try {
// 重新连接服务器
((Client) channel).reconnect();
} catch (Exception e) {
//do nothing
}
} else {
// 如果不是客户端,也就是是服务端返回响应给客户端,但是客户端挂掉了,则服务端关闭客户端连接
channel.close();
}
}
} catch (Throwable t) {
logger.warn("Exception when heartbeat to remote channel " + channel.getRemoteAddress(), t);
}
}
} catch (Throwable t) {
logger.warn("Unhandled exception when heartbeat, cause: " + t.getMessage(), t);
}
} 大专栏  Dubbo之心跳机制 · 房东的小黑lass="line"> interface ChannelProvider {
// 获得所有的通道集合,需要心跳的通道数组
Collection<Channel> getChannels();
} }

它首先遍历所有的Channel,在服务端对用的是所有客户端连接,在客户端对应的是服务端连接,判断当前TCP连接是否空闲,如果空闲就发送心跳报文,判断是否空闲,根据Channel是否有读或写来决定,比如一分钟内没有读或写就发送心跳报文,然后是处理超时的问题,处理客户端超时重新建立TCP连接,目前的策略是检查是否在3分钟内都没有成功接受或发送报文,如果在服务端检测则就会主动关闭远程客户端连接。

新版本的心跳机制

定时任务一: 发送心跳请求

在新版本下,去除了HeartBeatTask类,添加了HeartbeatTimerTask和ReconnectTimerTask类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class HeartbeatTimerTask extends AbstractTimerTask {

    private static final Logger logger = LoggerFactory.getLogger(HeartbeatTimerTask.class);

    private final int heartbeat;

    HeartbeatTimerTask(ChannelProvider channelProvider, Long heartbeatTick, int heartbeat) {
super(channelProvider, heartbeatTick);
this.heartbeat = heartbeat;
} protected void doTask(Channel channel) {
try {
Long lastRead = lastRead(channel);
Long lastWrite = lastWrite(channel);
if ((lastRead != null && now() - lastRead > heartbeat)
|| (lastWrite != null && now() - lastWrite > heartbeat)) {
Request req = new Request();
req.setVersion(Version.getProtocolVersion());
req.setTwoWay(true);
req.setEvent(Request.HEARTBEAT_EVENT);
channel.send(req);
if (logger.isDebugEnabled()) {
logger.debug("Send heartbeat to remote channel " + channel.getRemoteAddress()
+ ", cause: The channel has no data-transmission exceeds a heartbeat period: "
+ heartbeat + "ms");
}
}
} catch (Throwable t) {
logger.warn("Exception when heartbeat to remote channel " + channel.getRemoteAddress(), t);
}
}
}

Dubbo采取的是双向心跳设计,即服务端会向客户端发送心跳,客户端也会向服务端发送心跳,接收的一方更新lastread字段,发送的一方更新lastWrite字段,超过心跳间隙的时间,便发送心跳请求给对端。

定时任务二: 处理重连和断连

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class ReconnectTimerTask extends AbstractTimerTask {

    private static final Logger logger = LoggerFactory.getLogger(ReconnectTimerTask.class);

    private final int idleTimeout;

    public ReconnectTimerTask(ChannelProvider channelProvider, Long heartbeatTimeoutTick, int idleTimeout) {
super(channelProvider, heartbeatTimeoutTick);
this.idleTimeout = idleTimeout;
} protected void doTask(Channel channel) {
try {
Long lastRead = lastRead(channel);
Long now = now(); // Rely on reconnect timer to reconnect when AbstractClient.doConnect fails to init the connection
if (!channel.isConnected()) {
try {
logger.info("Initial connection to " + channel);
((Client) channel).reconnect();
} catch (Exception e) {
logger.error("Fail to connect to " + channel, e);
}
// check pong at client
} else if (lastRead != null && now - lastRead > idleTimeout) {
logger.warn("Reconnect to channel " + channel + ", because heartbeat read idle time out: "
+ idleTimeout + "ms");
try {
((Client) channel).reconnect();
} catch (Exception e) {
logger.error(channel + "reconnect failed during idle time.", e);
}
}
} catch (Throwable t) {
logger.warn("Exception when reconnect to remote channel " + channel.getRemoteAddress(), t);
}
}
}

不同类型处理机制不同,当超过设置的心跳总时间后,客户端选择的是重新连接,服务端是选择直接断开连接。

心跳改进方案

Netty对空闲连接的检测提供了天然的支持,使用IdleStateHandler可以很方便的实现空闲检测逻辑。

1
public IdleStateHandler(long readerIdleTime, long writerIdleTime, long allIdleTime, TimeUnit unit){}
  • readerIdleTime: 读超时的时间
  • writerIdleTime: 写超时的时间
  • allIdleTime: 所有类型的超时时间
    客户端和服务端配置
    客户端:
    1
    2
    3
    4
    5
    6
    bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
    
        protected void initChannel(NioSocketChannel ch) throws Exception {
    ch.pipeline().addLast("clientIdleHandler", new IdleStateHandler(60, 0, 0));
    }
    });

服务端:

1
2
3
4
5
6
serverBootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {

    protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast("serverIdleHandler",new IdleStateHandler(0, 0, 200));
}
}

从上面看出,客户端配置了read超时为60s,服务端配置了write/read超时未200s,

空闲超时逻辑-客户端

对于空闲超时的处理逻辑,客户端和服务端是不同的,首先来看客户端的:

1
2
3
4
5
6
7
8
9
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
// send heartbeat
sendHeartBeat();
} else {
super.userEventTriggered(ctx, evt);
}
}

检测到空闲超时后,采取的行为是向服务端发送心跳包,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void sendHeartBeat() {
Invocation invocation = new Invocation();
invocation.setInvocationType(InvocationType.HEART_BEAT);
channel.writeAndFlush(invocation).addListener(new CallbackFuture() {
@Override
public void callback(Future future) {
RPCResult result = future.get();
//超时 或者 写失败
if (result.isError()) {
channel.addFailedHeartBeatTimes();
if (channel.getFailedHeartBeatTimes() >= channel.getMaxHeartBeatFailedTimes()) {
channel.reconnect();
}
} else {
channel.clearHeartBeatFailedTimes();
}
}
});
}

构造一个心跳包发送到服务端,接受响应结果

  • 响应成功,清除请求失败标记
  • 响应失败,心跳失败标记+1,如果超过配置的失败次数,则重新连接

    空闲超时逻辑 - 服务端

    1
    2
    3
    4
    5
    6
    7
    8
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
    if (evt instanceof IdleStateEvent) {
    channel.close();
    } else {
    super.userEventTriggered(ctx, evt);
    }
    }

服务端直接关闭连接。

Dubbo之心跳机制 · 房东的小黑的更多相关文章

  1. dubbo之心跳机制

    在网络传输中,怎么确保通道连接的可用性是一个很重要的问题,简单的说,在网络通信中有客户端和服务端,一个负责发送请求,一个负责接收请求,在保证连接有效性的背景下,这两个物体扮演了什么角色,心跳机制能有效 ...

  2. 9.7 dubbo心跳机制

    dubbo的心跳机制: 目的:检测provider与consumer之间的connection连接是不是还连接着,如果连接断了,需要作出相应的处理. 原理: provider:dubbo的心跳默认是在 ...

  3. dubbo心跳机制 (1)

    此文已由作者赵计刚授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. dubbo的心跳机制: 目的:检测provider与consumer之间的connection连接是不是还连 ...

  4. dubbo心跳机制 (3)

    此文已由作者赵计刚授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. 二.consumer端心跳机制                       //创建ExchangeClie ...

  5. rabbitmq 的心跳机制&应用

    官方文档说: If a consumer dies (its channel is closed, connection is closed, or TCP connection is lost) w ...

  6. zookeeper心跳机制流程梳理

    zookeeper心跳机制流程梳理 Processor链Chain protected void setupRequestProcessors() { RequestProcessor finalPr ...

  7. 一个Socket连接管理池(心跳机制)

    一个Socket连接管理池(心跳机制) http://cuisuqiang.iteye.com/blog/1489661

  8. ESFramework 开发手册(07) -- 掉线与心跳机制(转)

    虽然我们前面已经介绍完了ESFramework开发所需掌握的各种基础设施,但是还不够.想要更好地利用ESFramework这一利器,有些背景知识是我们必须要理解的.就像本文介绍的心跳机制,在严峻的In ...

  9. 判定生死的心跳机制 --ESFramework 4.0 快速上手(07)

    在Internet上采用TCP进行通信的系统,都会遇到一个令人头疼的问题,就是"掉线".而"TCP掉线"这个问题远比我们通常所能想象的要复杂的多 -- 网络拓扑 ...

随机推荐

  1. MySQL数据库数据迁移:从一个服务器到另一个服务器

    需要两个服务器数据库版本相同才可迁移 1:单个或多个数据库 mysqldump -h远程ip -u用户 -p密码 -P3306 -- -uroot -p -P3306 执行后输入本地数据库密码即可 : ...

  2. nodejs服务后台持续运行三种方法

    一.利用 forever forever是一个nodejs守护进程,完全由命令行操控.forever会监控nodejs服务,并在服务挂掉后进行重启. 1.安装 forever npm install ...

  3. Kafka、RabbitMQ、RocketMQ等消息中间件的介绍和对比

    本博客强烈推荐: Java电子书高清PDF集合免费下载 https://www.cnblogs.com/yuxiang1/p/12099324.html 前言 在分布式系统中,我们广泛运用消息中间件进 ...

  4. 1.linux系统调优

    首先来说调优是一门黑色艺术,使用来满足人的感知,通过人的感觉来进行配置,达到让人感觉操作系统速度很块的感觉. 操作系统拥有四个瓶颈:cpu,内存,网络,磁盘.调优主要是对上述四个子系统进行配置优化,其 ...

  5. org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.reflection.ReflectionException: There is no getter for property named 'socialCode' in 'class java.lang.String'

    异常: org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.reflection.Refl ...

  6. myeclipse 编写java代码提示 dead code 原因

    经常使用MyEclipse或Eclipse编辑器编写java代码的程序员,可能经常遇到一个黄线警告提示:dead code:一般程序员遇到这些问题都会置之不理,反正也不影响程序的编译执行.对,这不是b ...

  7. 如何编译(helloworld)可以在开发板上运行的应用

    本节介绍如何编译可以在开发板上运行的应用,编译方法很简单.基于:iTOP4412开发板首先要确定一下环境变量,如下图所示,使用“cd”命令回到根目录,然后使用命令“vim .bashrc”打开环境变量 ...

  8. [LC] 659. Split Array into Consecutive Subsequences

    Given an array nums sorted in ascending order, return true if and only if you can split it into 1 or ...

  9. 吴裕雄--天生自然python机器学习:使用朴素贝叶斯过滤垃圾邮件

    使用朴素贝叶斯解决一些现实生活中 的问题时,需要先从文本内容得到字符串列表,然后生成词向量. 准备数据:切分文本 测试算法:使用朴素贝叶斯进行交叉验证 文件解析及完整的垃圾邮件测试函数 def cre ...

  10. F. Rhyme scheme

    题: https://nanti.jisuanke.com/t/41414 #include<bits/stdc++.h> using namespace std; typedef __i ...