sofa-bolt 心跳检测

一、概述

soft-bolt心跳机制是基于Netty,因此在分析soft-bolt的心跳之前,先分析一下netty心跳实现。

二、netty心跳实现

Netty提供了IdleStateHandler类,用于支持心跳检查,其构造参数

/**
 * Creates a new instance firing {@link IdleStateEvent}s.
 *
 * @param readerIdleTime 读超时时间
 * @param writerIdleTime 写超时时间
 * @param allIdleTime    读写超时时间
 * @param unit           时间单位
 */
public IdleStateHandler(long readerIdleTime, long writerIdleTime, long allIdleTime, TimeUnit unit) 

该类是一个ChannelHandler,需要加入到ChannelPipeline里面,参考代码如下:

this.bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
​
    @Override
    protected void initChannel(SocketChannel channel) {
        ChannelPipeline pipeline = channel.pipeline();
        pipeline.addLast("decoder", codec.newDecoder());
        pipeline.addLast("encoder", codec.newEncoder());
        if (idleSwitch) {
            pipeline.addLast("idleStateHandler", new IdleStateHandler(5, 0, 0, TimeUnit.MILLISECONDS));
            pipeline.addLast("serverIdleHandler", serverIdleHandler);
        }
        pipeline.addLast("connectionEventHandler", connectionEventHandler);
        pipeline.addLast("handler", rpcHandler);
        createConnection(channel);
    }

在channel链中加入了IdleSateHandler,第一个参数是5,单位是秒,服务器端会每隔5秒来检查一下channelRead方法被调用的情况,如果在5秒内该链上的channelRead方法都没有被触发,就会调用userEventTriggered方法 ,下面看一下IdleStateHandler中的channelRead方法。

public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    if (readerIdleTimeNanos > 0 || allIdleTimeNanos > 0) {
        reading = true;
        firstReaderIdleEvent = firstAllIdleEvent = true;
    }
    ctx.fireChannelRead(msg);
}

该方法只是记录了一下调用时间,然后将请求往下透传,接下来看一下channelActive方法。

public void channelActive(ChannelHandlerContext ctx) throws Exception {
    // This method will be invoked only if this handler was added
    // before channelActive() event is fired.  If a user adds this handler
    // after the channelActive() event, initialize() will be called by beforeAdd().
    initialize(ctx);
    super.channelActive(ctx);
}

在客户端与服务端建立连接以后,会调用channelActive方法,在IdleSateHandler的channelActive方法中调用initialize()方法进行连接心跳的初始化操作,具体实现如下:

private void initialize(ChannelHandlerContext ctx) {
        // Avoid the case where destroy() is called before scheduling timeouts.
    // See: https://github.com/netty/netty/issues/143
    switch (state) {
    case 1:
    case 2:
        return;
    }
​
    state = 1;
    initOutputChanged(ctx);
​
    lastReadTime = lastWriteTime = ticksInNanos();
    if (readerIdleTimeNanos > 0) {
        //创建定时任务处理读超时
        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定时任务做了什么操作?

protected void run(ChannelHandlerContext ctx) {
    long nextDelay = readerIdleTimeNanos;
    if (!reading) {
        nextDelay -= ticksInNanos() - lastReadTime;
    }
​
    if (nextDelay <= 0) {
        // Reader is idle - set a new timeout and notify the callback.
        readerIdleTimeout = schedule(ctx, this, readerIdleTimeNanos, TimeUnit.NANOSECONDS);
​
        boolean first = firstReaderIdleEvent;
        firstReaderIdleEvent = false;
​
        try {
            IdleStateEvent event = newIdleStateEvent(IdleState.READER_IDLE, first);
            channelIdle(ctx, event);
        } catch (Throwable t) {
            ctx.fireExceptionCaught(t);
        }
    } else {
        // Read occurred before the timeout - set a new timeout with shorter delay.
        readerIdleTimeout = schedule(ctx, this, nextDelay, TimeUnit.NANOSECONDS);
    }
}

当前时间减去最后一次channelRead方法调用的时间,如果该时间间隔大于设置的读超时时间,就触发读空闲时间,并且创建定时任务继续检查。

上面的代码分析了读超时问题,写超时和读写超时的代码类似,可以自行分析。

三、soft-bolt 心跳实现

在soft-bolt实现了通过HeartbeatHandler来处理连接心跳。具体实现如下:

public class HeartbeatHandler extends ChannelDuplexHandler {
    public void userEventTriggered(final ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent) {
            ProtocolCode protocolCode = ctx.channel().attr(Connection.PROTOCOL).get();
            Protocol protocol = ProtocolManager.getProtocol(protocolCode);
            protocol.getHeartbeatTrigger().heartbeatTriggered(ctx);
        } else {
            super.userEventTriggered(ctx, evt);
        }
    }
}

上面判断如果事件类型IdleStateEvent,马上就进行心跳处理,心跳处理是heartbeatTriggered方法实现的

public void heartbeatTriggered(final ChannelHandlerContext ctx) throws Exception {
    Integer heartbeatTimes = ctx.channel().attr(Connection.HEARTBEAT_COUNT).get();
    final Connection conn = ctx.channel().attr(Connection.CONNECTION).get();
  
    //如果心跳超过设定次数没有响应,就断开连接
    if (heartbeatTimes >= maxCount) {
        try {
            conn.close();
            logger.error(
                "Heartbeat failed for {} times, close the connection from client side: {} ",
                heartbeatTimes, RemotingUtil.parseRemoteAddress(ctx.channel()));
        } catch (Exception e) {
            logger.warn("Exception caught when closing connection in SharableHandler.", e);
        }
    } else {
        boolean heartbeatSwitch = ctx.channel().attr(Connection.HEARTBEAT_SWITCH).get();
        if (!heartbeatSwitch) {
            return;
        }
        final HeartbeatCommand heartbeat = new HeartbeatCommand();
        //添加回调listener
        final InvokeFuture future = new DefaultInvokeFuture(heartbeat.getId(),
            new InvokeCallbackListener() {
                @Override
                public void onResponse(InvokeFuture future) {
                    ResponseCommand response;
                    try {
                        response = (ResponseCommand) future.waitResponse(0);
                    } catch (InterruptedException e) {
                        logger.error("Heartbeat ack process error! Id={}, from remoteAddr={}",
                            heartbeat.getId(), RemotingUtil.parseRemoteAddress(ctx.channel()),
                            e);
                        return;
                    }
                    if (response != null
                        && response.getResponseStatus() == ResponseStatus.SUCCESS) {
                        if (logger.isDebugEnabled()) {
                            logger.debug("Heartbeat ack received! Id={}, from remoteAddr={}",
                                response.getId(),
                                RemotingUtil.parseRemoteAddress(ctx.channel()));
                        }
                        // 如果心跳请求被成功响应,设置心跳次数为0
                        ctx.channel().attr(Connection.HEARTBEAT_COUNT).set(0);
                    } else {
                        if (response == null) {
                            logger.error("Heartbeat timeout! The address is {}",
                                RemotingUtil.parseRemoteAddress(ctx.channel()));
                        } else {
                            logger.error(
                                "Heartbeat exception caught! Error code={}, The address is {}",
                                response.getResponseStatus(),
                                RemotingUtil.parseRemoteAddress(ctx.channel()));
                        }
                        // 心跳请求响应异常或者超时,心跳次数加1
                        Integer times = ctx.channel().attr(Connection.HEARTBEAT_COUNT).get();
                        ctx.channel().attr(Connection.HEARTBEAT_COUNT).set(times + 1);
                    }
                }
                @Override
                public String getRemoteAddress() {
                    return ctx.channel().remoteAddress().toString();
                }
            }, null, heartbeat.getProtocolCode().getFirstByte(), this.commandFactory);
        final int heartbeatId = heartbeat.getId();
        conn.addInvokeFuture(future);
        if (logger.isDebugEnabled()) {
            logger.debug("Send heartbeat, successive count={}, Id={}, to remoteAddr={}",
                heartbeatTimes, heartbeatId, RemotingUtil.parseRemoteAddress(ctx.channel()));
        }
        // 发送心跳请求
        ctx.writeAndFlush(heartbeat).addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) throws Exception {
                if (future.isSuccess()) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Send heartbeat done! Id={}, to remoteAddr={}",
                            heartbeatId, RemotingUtil.parseRemoteAddress(ctx.channel()));
                    }
                } else {
                    logger.error("Send heartbeat failed! Id={}, to remoteAddr={}", heartbeatId,
                        RemotingUtil.parseRemoteAddress(ctx.channel()));
                }
            }
        });
        // 处理心跳请求超时
        TimerHolder.getTimer().newTimeout(new TimerTask() {
            @Override
            public void run(Timeout timeout) throws Exception {
                InvokeFuture future = conn.removeInvokeFuture(heartbeatId);
                if (future != null) {
                    future.putResponse(commandFactory.createTimeoutResponse(conn
                        .getRemoteAddress()));
                    future.tryAsyncExecuteInvokeCallbackAbnormally();
                }
            }
        }, heartbeatTimeoutMillis, TimeUnit.MILLISECONDS);
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容