本文基于dubbo 2.7.5版本代码
本文以底层通讯工具为netty进行介绍。
dubbo默认使用Netty作为通讯工具,Netty在消费端和服务端之间建立长连接。当建立长连接后,需要使用心跳机制判断双方是否在线。
检测对方是否在线是长连接非常重要的一种机制。如果不检测,那么自身是无法感知对方掉线的,对方一旦掉线了,长连接也就断开了,双方通讯无法正常完成。有些检测机制是通过发送心跳报文完成,有些检测机制是通过发送业务请求时,顺带检测是否在线。
dubbo使用心跳机制检测对方是否在线。先简单描述一下实现逻辑。
一、实现原理
dubbo的心跳发送是由Netty的IdleStateHandler对象处理的。该对象是一个handler,配置在netty的责任链里面,当发送请求或者收到响应时,都会经过该对象处理。在双方通讯开始后该对象会创建一些空闲检测定时器,用于检测读事件(收到请求会触发读事件)和写事件(连接、发送请求会触发写事件)。当在指定的空闲时间内没有收到读事件或写事件,便会触发超时事件,然后IdleStateHandler将超时事件交给责任链里面的下一个handler NettyClientHandler或者NettyServertHandler处理,NettyClientHandler是客户端使用的,它收到事件后向对方发送一个心跳事件的请求,NettyServertHandler是服务端使用的,它收到事件后将关闭超时的连接。
dubbo还提供了HeaderExchangeClient。HeaderExchangeClient在客户端使用,当发现连接在指定时间内没有收到响应报文,而且连接已经不可用,那么会启动重连机制,与服务端重新建立连接。
下面先来分析IdleStateHandler和NettyClientHandler、NettyServerHandler,之后在介绍HeaderExchangeClient。
二、发送心跳
dubbo服务端的Netty创建和初始化是由NettyServer类完成的,下面的代码是NettyServer的doOpen方法的一部分,该方法将IdleStateHandler和NettyServerHandler加入了责任链。
final NettyServerHandler nettyServerHandler = new NettyServerHandler(getUrl(), this);
channels = nettyServerHandler.getChannels();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childOption(ChannelOption.TCP_NODELAY, Boolean.TRUE)
.childOption(ChannelOption.SO_REUSEADDR, Boolean.TRUE)
.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
int idleTimeout = UrlUtils.getIdleTimeout(getUrl());
NettyCodecAdapter adapter = new NettyCodecAdapter(getCodec(), getUrl(), NettyServer.this);
if (getUrl().getParameter(SSL_ENABLED_KEY, false)) {
ch.pipeline().addLast("negotiation",
SslHandlerInitializer.sslServerHandler(getUrl(), nettyServerHandler));
}
ch.pipeline()
.addLast("decoder", adapter.getDecoder())
.addLast("encoder", adapter.getEncoder())
//将IdleStateHandler加入责任链
.addLast("server-idle-handler", new IdleStateHandler(0, 0, idleTimeout, MILLISECONDS))
//将NettyServerHandler加入责任链
.addLast("handler", nettyServerHandler);
}
});
我们先来看一下IdleStateHandler构造方法各个入参的含义:
- readerIdleTime:读空闲超时检测定时任务会在每readerIdleTime时间内启动一次,检测在readerIdleTime内是否发生过读事件,如果没有发生过,则触发读超时事件READER_IDLE_STATE_EVENT,并将超时事件交给NettyClientHandler处理。如果为0,则不创建定时任务。
- writerIdleTime:与readerIdleTime作用类似,只不过该参数定义的是写事件。
- allIdleTime:同时检测读事件和写事件,如果在allIdleTime时间内即没有发生过读事件,也没有发生过写事件,则触发超时事件ALL_IDLE_STATE_EVENT。
- unit:表示前面三个参数的单位,就上面代码来说,表示的是毫秒。
//服务端创建IdleStateHandler
//idleTimeout=“heartbeat”*3或者“heartbeat.timeout”,默认定时任务启动时间间隔是3分钟
new IdleStateHandler(0, 0, idleTimeout, MILLISECONDS)
服务端创建的IdleStateHandler设置了allIdleTime,所以服务端的定时任务需要检测读事件和写事件。定时任务的启动时间间隔是参数“heartbeat”设置值的3倍,heartbeat默认是1分钟,也可以通过参数“heartbeat.timeout”设置定时任务的启动时间间隔。单位都是毫秒。
客户端初始化Netty是在NettyClient的doOpen方法中完成,下面来看一下代码:
ch.pipeline()
.addLast("decoder", adapter.getDecoder())
.addLast("encoder", adapter.getEncoder())
.addLast("client-idle-handler", new IdleStateHandler(heartbeatInterval, 0, 0, MILLISECONDS))
.addLast("handler", nettyClientHandler);
NettyClient创建IdleStateHandler只设置了readerIdleTime入参,表示客户端启动定时任务只检测读事件。定时任务的时间间隔由参数“heartbeat”指定,默认是1分钟。
new IdleStateHandler(heartbeatInterval, 0, 0, MILLISECONDS)
上面介绍了服务端和消费端创建了IdleStateHandler对象并且将其加入到责任链中,下面来看一下IdleStateHandler如何创建的定时任务以及如何检测超时事件。
当Channel被激活的时候,也就是连接被建立起来之后,会调用IdleStateHandler的initialize方法:
private void initialize(ChannelHandlerContext ctx) {
//调用该方法时,state=0
switch (state) {
case 1:
case 2:
return;
}
state = 1;
initOutputChanged(ctx);
//readerIdleTimeNanos、writerIdleTimeNanos、allIdleTimeNanos三个值其实是构造方法的入参对应的纳秒值
lastReadTime = lastWriteTime = ticksInNanos();
if (readerIdleTimeNanos > 0) {
//schedule方法用于创建定时任务,下面的定时任务用于检测读超时事件
//入参readerIdleTimeNanos表示定时任务在readerIdleTimeNanos纳秒后执行一次
readerIdleTimeout = schedule(ctx, new ReaderIdleTimeoutTask(ctx),
readerIdleTimeNanos, TimeUnit.NANOSECONDS);
}
if (writerIdleTimeNanos > 0) {
//下面的定时任务用于检测写超时事件
writerIdleTimeout = schedule(ctx, new WriterIdleTimeoutTask(ctx),
writerIdleTimeNanos, TimeUnit.NANOSECONDS);
}
if (allIdleTimeNanos > 0) {
//下面的定时任务用于检测读写超时事件
allIdleTimeout = schedule(ctx, new AllIdleTimeoutTask(ctx),
allIdleTimeNanos, TimeUnit.NANOSECONDS);
}
}
下面来看一下ReaderIdleTimeoutTask类中最关键的run方法,定时任务每次运行的时候都是执行run方法,其他两个定时任务类与ReaderIdleTimeoutTask类似:
protected void run(ChannelHandlerContext ctx) {
long nextDelay = readerIdleTimeNanos;
//reading为true表示当前正在从网络读取数据
if (!reading) {
//lastReadTime表示最后一次发生读事件的时间
nextDelay -= ticksInNanos() - lastReadTime;
}
//nextDelay小于0表示在指定的空闲时间内没有发生过读事件
if (nextDelay <= 0) {
//创建相同的定时任务,等待下次检测
readerIdleTimeout = schedule(ctx, this, readerIdleTimeNanos, TimeUnit.NANOSECONDS);
boolean first = firstReaderIdleEvent;
firstReaderIdleEvent = false;
try {
//创建读超时事件
IdleStateEvent event = newIdleStateEvent(IdleState.READER_IDLE, first);
//将超时事件交给责任链里面的下一个handler,也就是NettyClientHandler
channelIdle(ctx, event);
} catch (Throwable t) {
ctx.fireExceptionCaught(t);
}
} else {
//在指定的空闲时间内发生过读事件,那么创建一个定时任务以便下次继续检测
readerIdleTimeout = schedule(ctx, this, nextDelay, TimeUnit.NANOSECONDS);
}
}
对于服务端来说,ReaderIdleTimeoutTask将超时事件交给NettyServerHandler,客户端是交给NettyClientHandler,下面分别来看这两个handler有哪些不同。
首先来看NettyServerHandler。NettyServerHandler收到超时事件后调用:
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
NettyChannel channel = NettyChannel.getOrAddChannel(ctx.channel(), url, handler);
try {
logger.info("IdleStateEvent triggered, close channel " + channel);
//如果是超时事件,则将连接关闭
channel.close();
} finally {
//将关闭的连接标记为不活动
NettyChannel.removeChannelIfDisconnected(ctx.channel());
}
}
//将超时事件接着向下一个责任链传递
super.userEventTriggered(ctx, evt);
}
NettyClientHandler收到超时事件后就要触发心跳报文的发送:
//入参evt是IdleStateHandler中创建的超时事件对象
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
try {
NettyChannel channel = NettyChannel.getOrAddChannel(ctx.channel(), url, handler);
//代码有删减
//创建心跳请求报文Request对象
Request req = new Request();
req.setVersion(Version.getProtocolVersion());
req.setTwoWay(true);
req.setEvent(HEARTBEAT_EVENT);//HEARTBEAT_EVENT表示是心跳报文
channel.send(req);//调用Netty将报文发送到对方
} finally {
//检测当前Channel是否可用,如果不可用则修改状态为非活动状态
NettyChannel.removeChannelIfDisconnected(ctx.channel());
}
} else {
super.userEventTriggered(ctx, evt);
}
}
服务端收到心跳报文后,不会对此回复任何消息。
从上面可以看出,dubbo对超时的检测是借助Netty的IdleStateHandler完成的,一旦发生超时,则创建超时事件并交给NettyClientHandler或者NettyServerHandler处理,服务端是关闭连接,客户端是发送心跳报文继续维持连接。
三、超时重连
当客户端发现某个连接长时间没有收到响应数据,dubbo在exchange信息交换层提供了类HeaderExchangeClient会对该连接进行超时重连。
我们来看一下代码,HeaderExchangeClient的构造方法会调用下面的方法:
private void startReconnectTask(URL url) {
//可以通过参数“reconnect”设置是否启动重连,默认是true
if (shouldReconnect(url)) {
AbstractTimerTask.ChannelProvider cp = () -> Collections.singletonList(HeaderExchangeClient.this);
//idleTimeout=“heartbeat”*3或者“heartbeat.timeout”,默认空闲超时时间是3分钟
int idleTimeout = getIdleTimeout(url);
//heartbeatTimeoutTick=idleTimeout/3,heartbeatTimeoutTick 最小是1000
long heartbeatTimeoutTick = calculateLeastDuration(idleTimeout);
//创建重连任务
this.reconnectTimerTask = new ReconnectTimerTask(cp, heartbeatTimeoutTick, idleTimeout);
//启动重连任务,每heartbeatTimeoutTick时间执行一次
IDLE_CHECK_TIMER.newTimeout(reconnectTimerTask, heartbeatTimeoutTick, TimeUnit.MILLISECONDS);
}
}
HeaderExchangeClient在创建对象的过程中创建定时任务ReconnectTimerTask,下面来看一下定时任务的run方法:
public void run(Timeout timeout) throws Exception {
Collection<Channel> c = channelProvider.getChannels();
//遍历连接某一服务端的所有连接
for (Channel channel : c) {
if (channel.isClosed()) {
continue;
}
doTask(channel);
}
//创建定时任务用于下次检测超时重连,定时任务每次执行完都需要重新创建
reput(timeout, tick);
}
protected void doTask(Channel channel) {
try {
//获取最后一次收到消息的事件
Long lastRead = lastRead(channel);
Long now = now();
if (!channel.isConnected()) {
try {
logger.info("Initial connection to " + channel);
//如果连接已经关闭,则重连
((Client) channel).reconnect();
} catch (Exception e) {
logger.error("Fail to connect to " + channel, e);
}
//如果在指定的时间内没有收到任何消息,则重连,
//reconnect方法内部有判断,如果当前连接是正常的,则不进行重连
//这里的idleTimeout是startReconnectTask方法中的heartbeatTimeoutTick,默认是1分钟
} 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);
}
}
HeaderExchangeClient在创建的对象的时候启动定时任务ReconnectTimerTask,默认定时任务每1分钟执行一次,发现不可用的连接或者默认1分钟内没有收到消息的连接都会进行重连。
四、总结
当服务端发生超时事件后,服务端会将对应的连接关闭。
当客户端发生超时事件后,客户端通过超时重连以及发送心跳尝试维持连接。
服务端和客户端对超时后作出的不同操作也反映了双方不同的策略。因为连接占用系统资源,服务端要尽可能的将资源留给其他请求,对于服务端来说,如果某个连接长时间没有数据传输,说明与该客户端的连接已经断开,或者客户端访问已经结束最近不需要再次访问,无论哪种情况,对于服务端来说最好的处理都是断开与客户端的连接。
而客户端则不同,客户端想尽全力保证连接的可用,因为客户端访问服务时最希望的是尽快得到响应,因此客户端最好是时时刻刻保持连接的可用,这样访问服务时可以省去建立连接的时间消耗。
另外一点也要主要,服务端和客户端启动定时任务的时间是不同的,默认服务端是3分钟,客户端是1分钟,dubbo要求服务端定时任务的启动时间间隔最小是客户端的2倍。