Dubbo 的心跳设计,值得学习!

Stella981
• 阅读 487

作者:ksfzhaohui317 https://segmentfault.com/a/1190000022591346

前言

谈到RPC肯定绕不开TCP通信,而主流的RPC框架都依赖于Netty等通信框架,这时候我们还要考虑是使用长连接还是短连接:

  • 短连接:每次通信结束后关闭连接,下次通信需要重新创建连接;优点就是无需管理连接,无需保活连接;

  • 长连接:每次通信结束不关闭连接,连接可以复用,保证了性能;缺点就是连接需要统一管理,并且需要保活;

主流的RPC框架都会追求性能选择使用长连接,所以如何保活连接就是一个重要的话题,也是本文的主题,下面会重点介绍一些保活策略;

为什么需要保活

上面介绍的长连接、短连接并不是TCP提供的功能,所以长连接是需要应用端自己来实现的,包括:连接的统一管理,如何保活等;如何保活之前我们了解一下为什么需要保活?

主要原因是网络不是100%可靠的,我们创建好的连接可能由于网络原因导致连接已经不可用了,如果连接一直有消息往来,那么系统马上可以感知到连接断开;

但是我们系统可能长时间没有消息来往,导致系统不能及时感知到连接不可用,也就是不能及时处理重连或者释放连接;常见的保活策略使用心跳机制由应用层来实现,还有网络层提供的TCP Keepalive保活探测机制;

TCP Keepalive机制

TCP Keepalive是操作系统实现的功能,并不是TCP协议的一部分,需要在操作系统下进行相关配置,开启此功能后,如果连接在一段时间内没有数据往来,TCP将发送Keepalive探针来确认连接的可用性,Keepalive几个内核参数配置:

  • tcp_keepalive_time:连接多长时间没有数据往来发送探针请求,默认为7200s(2h);

  • tcp_keepalive_probes:探测失败重试的次数默认为10次;

  • tcp_keepalive_intvl:重试的间隔时间默认75s;

以上参数可以修改到/etc/sysctl.conf文件中;是否使用Keepalive用来保活就够了,其实还不够,Keepalive只是在网络层就行保活,如果网络本身没有问题,但是系统由于其他原因已经不可用了,这时候Keepalive并不能发现;所以往往还需要结合心跳机制来一起使用;

心跳机制

何为心跳机制,简单来讲就是客户端启动一个定时器用来定时发送请求,服务端接到请求进行响应,如果多次没有接受到响应,那么客户端认为连接已经断开,可以断开半打开的连接或者进行重连处理;下面以Dubbo为例来看看是如何具体实施的;

Dubbo2.6.X

在HeaderExchangeClient中启动了定时器ScheduledThreadPoolExecutor来定期执行心跳请求:

ScheduledThreadPoolExecutor scheduled = new ScheduledThreadPoolExecutor(2,   
    new NamedThreadFactory("dubbo-remoting-client-heartbeat", true));  

在实例化HeaderExchangeClient时启动心跳定时器:

private void startHeartbeatTimer() {  
    stopHeartbeatTimer();          
    if (heartbeat > 0) {  
        heartbeatTimer = scheduled.scheduleWithFixedDelay(  
            new HeartBeatTask(new HeartBeatTask.ChannelProvider() {                          
                @Override  
                public Collection<Channel> getChannels() {                              
                    return Collections.<Channel>singletonList(HeaderExchangeClient.this);  
            }  
        }, heartbeat, heartbeatTimeout),  
        heartbeat, heartbeat, TimeUnit.MILLISECONDS);  
    }  
}  

heartbeat默认为60秒,heartbeatTimeout默认为heartbeat*3,可以理解至少出现三次心跳请求还未收到回复才会任务连接已经断开;HeartBeatTask为执行心跳的任务:

public void run() {  
    long now = System.currentTimeMillis();          
    for (Channel channel : channelProvider.getChannels()) {              
        if (channel.isClosed()) {                  
            continue;  
        }              
        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)) {                  
            // 发送心跳  
        }              
        if (lastRead != null && now - lastRead > heartbeatTimeout) {                  
            if (channel instanceof Client) {  
                    ((Client) channel).reconnect();  
            } else {  
                channel.close();  
            }  
        }  
    }  
}  

因为Dubbo双端都会发送心跳请求,所以可以发现有两个时间点分别是:lastRead和lastWrite;当然时间和最后读取,最后写的时间间隔大于heartbeat就会发送心跳请求;

如果多次心跳未返回结果,也就是最后读取消息时间大于heartbeatTimeout会判定当前是Client还是Server,如果是Client会发起reconnect,Server会关闭连接,这样的考虑是合理的,客户端调用是强依赖可用连接的,而服务端可以等待客户端重新建立连接;

以上只是介绍的Client,同样Server端也有相同的心跳处理,在可以查看HeaderExchangeServer;

Dubbo2.7.0

Dubbo2.7.0的心跳机制在2.6.X的基础上得到了加强,同样在HeaderExchangeClient中使用HashedWheelTimer开启心跳检测,这是Netty提供的一个时间轮定时器,在任务非常多,并且任务执行时间很短的情况下,HashedWheelTimer比Schedule性能更好,特别适合心跳检测;

HashedWheelTimer heartbeatTimer = new HashedWheelTimer(new NamedThreadFactory("dubbo-client-heartbeat", true), tickDuration,  
                    TimeUnit.MILLISECONDS, Constants.TICKS_PER_WHEEL);  

分别启动了两个定时任务:startHeartBeatTask和startReconnectTask:

private void startHeartbeatTimer() {  
    AbstractTimerTask.ChannelProvider cp = () -> Collections.singletonList(HeaderExchangeClient.this);          
    long heartbeatTick = calculateLeastDuration(heartbeat);          
    long heartbeatTimeoutTick = calculateLeastDuration(heartbeatTimeout);  
    HeartbeatTimerTask heartBeatTimerTask = new HeartbeatTimerTask(cp, heartbeatTick, heartbeat);  
    ReconnectTimerTask reconnectTimerTask = new ReconnectTimerTask(cp, heartbeatTimeoutTick, heartbeatTimeout);          // init task and start timer.  
    heartbeatTimer.newTimeout(heartBeatTimerTask, heartbeatTick, TimeUnit.MILLISECONDS);  
    heartbeatTimer.newTimeout(reconnectTimerTask, heartbeatTimeoutTick, TimeUnit.MILLISECONDS);  
}  

HeartbeatTimerTask:用来定时发送心跳请求,心跳间隔时间默认为60秒;这里重新计算了时间,其实就是在原来的基础上除以3,其实就是缩短了检测间隔时间,增大了及时发现死链的概率;分别看一下两个任务:

protected void doTask(Channel channel) {  
    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);  
    }  
}  

同上检测最后读写时间和heartbeat的大小,注:普通请求和心跳请求都会更新读写时间;Netty 在 Dubbo 中是如何应用的?这篇推荐大家看一下。

protected void doTask(Channel channel) {  
    Long lastRead = lastRead(channel);  
    Long now = now();        if (lastRead != null && now - lastRead > heartbeatTimeout) {            if (channel instanceof Client) {  
            ((Client) channel).reconnect();  
        } else {  
            channel.close();  
        }  
    }  
}  

同样的在超时的情况下,Client重连,Server关闭连接;同样Server端也有相同的心跳处理,在可以查看HeaderExchangeServer;

Dubbo2.7.1-X

在Dubbo2.7.1之后,借助了Netty提供的IdleStateHandler来实现心跳机制服务:

public IdleStateHandler(  
        long readerIdleTime, long writerIdleTime, long allIdleTime,  
        TimeUnit unit) {  
    this(false, readerIdleTime, writerIdleTime, allIdleTime, unit);  
}  
  • readerIdleTime:读超时时间;

  • writerIdleTime:写超时时间;

  • allIdleTime:所有类型的超时时间;

根据设置的超时时间,循环检查读写事件多久没有发生了,在pipeline中加入IdleSateHandler之后,可以在此pipeline的任意Handler的userEventTriggered方法之中检测IdleStateEvent事件;下面看看具体Client和Server端添加的IdleStateHandler:

Client端

protected void initChannel(Channel ch) throws Exception {          
    final NettyClientHandler nettyClientHandler = new NettyClientHandler(getUrl(), this);          
    int heartbeatInterval = UrlUtils.getHeartbeat(getUrl());  
    ch.pipeline().addLast("client-idle-handler", new IdleStateHandler(heartbeatInterval, 0, 0, MILLISECONDS))  
                .addLast("handler", nettyClientHandler);  
}  

Client端在NettyClient中添加了IdleStateHandler,指定了读写超时时间默认为60秒;60秒内没有读写事件发生,会触发IdleStateEvent事件在NettyClientHandler处理:

public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {        if (evt instanceof IdleStateEvent) {            try {  
            NettyChannel channel = NettyChannel.getOrAddChannel(ctx.channel(), url, handler);  
            Request req = new Request();  
            req.setVersion(Version.getProtocolVersion());  
            req.setTwoWay(true);  
            req.setEvent(Request.HEARTBEAT_EVENT);  
            channel.send(req);  
        } finally {  
            NettyChannel.removeChannelIfDisconnected(ctx.channel());  
        }  
   } else {              
     super.userEventTriggered(ctx, evt);  
   }  
}  

可以发现接收到IdleStateEvent事件发送了心跳请求;至于Client端如何处理重连,同样在HeaderExchangeClient中使用HashedWheelTimer定时器启动了两个任务:心跳任务和重连任务,感觉这里已经不需要心跳任务了,至于重连任务其实也可以放到userEventTriggered中处理;

Server端

protected void initChannel(NioSocketChannel ch) throws Exception {        int idleTimeout = UrlUtils.getIdleTimeout(getUrl());        final NettyServerHandler nettyServerHandler = new NettyServerHandler(getUrl(), this);  
    ch.pipeline().addLast("server-idle-handler", new IdleStateHandler(0, 0, idleTimeout, MILLISECONDS))  
                .addLast("handler", nettyServerHandler);  
}  

Server端指定的超时时间默认为60*3秒,在NettyServerHandler中处理userEventTriggered

public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {        if (evt instanceof IdleStateEvent) {  
        NettyChannel channel = NettyChannel.getOrAddChannel(ctx.channel(), url, handler);            try {  
            channel.close();  
        } finally {  
            NettyChannel.removeChannelIfDisconnected(ctx.channel());  
        }  
    }          
    super.userEventTriggered(ctx, evt);  
}  

Server端在指定的超时时间内没有发生读写,会直接关闭连接;相比之前现在只有Client发送心跳,单向发送心跳;

同样的在HeaderExchangeServer中并没有启动多个认为,仅仅启动了一个CloseTimerTask,用来检测超时时间关闭连接;感觉这个任务是不是也可以不需要了,IdleStateHandler已经实现了此功能;

综上:在使用IdleStateHandler的情况下来同时在HeaderExchangeClient启动心跳+重连机制,HeaderExchangeServer启动了关闭连接机制;主要是因为IdleStateHandler是Netty框架特有了,而Dubbo是支持多种底层通讯框架的包括Mina,Grizzy等,应该是为了兼容此类框架存在的;

总结

本文首先介绍了RPC中引入的长连接方式,继而引出长连接的保活机制,为什么需要保活?然后分别介绍了网络层保活机制TCP Keepalive机制,应用层心跳机制;最后已Dubbo为例看各个版本中对心跳机制的进化。

推荐去我的博客阅读更多:

1.Java JVM、集合、多线程、新特性系列教程

2.Spring MVC、Spring Boot、Spring Cloud 系列教程

3.Maven、Git、Eclipse、Intellij IDEA 系列工具教程

4.Java、后端、架构、阿里巴巴等大厂最新面试题

觉得不错,别忘了点赞+转发哦!

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Easter79 Easter79
3年前
swap空间的增减方法
(1)增大swap空间去激活swap交换区:swapoff v /dev/vg00/lvswap扩展交换lv:lvextend L 10G /dev/vg00/lvswap重新生成swap交换区:mkswap /dev/vg00/lvswap激活新生成的交换区:swapon v /dev/vg00/lvswap
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
ES6 新增的数组的方法
给定一个数组letlist\//wu:武力zhi:智力{id:1,name:'张飞',wu:97,zhi:10},{id:2,name:'诸葛亮',wu:55,zhi:99},{id:3,name:'赵云',wu:97,zhi:66},{id:4,na
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
11个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这