dubbo技术干货

深入分析dubbo延迟暴露

2020-01-11  本文已影响0人  爱说的夏老师

一、引子

最近搭建了一个新的Java工程,主要是提供dubbo服务给其他业务用的。突然想起之前dubbo服务都会配置延迟暴露来解决平滑发布的问题,但是好像现在新的Java项目都没有配置延迟暴露了,觉得很奇怪,所以去研究了一下关于dubbo延迟暴露的细节。

说明:

  • 延迟暴露(export)也叫延迟注册(register),为了统一概念,后续内容统一称“延迟暴露”。
  • 本篇文章是基于dubbo 2.6.6来讲的。

本篇文章主要介绍了以下几点:

二、什么是dubbo延迟暴露

dubbo service默认是在容器启动的时候暴露的,一旦暴露,consumer端就可以发现这个service并且调用到这个provider。所谓延迟暴露即在启动之后延迟一定时间再暴露,比如延迟3s。

三、为什么需要延迟暴露

3.1 场景一:组件初始化需要一定的时间

比如你提供的service需要初始化缓存数据,这个数据需要读取DB,然后进行计算(假设这个时间需要10s)。如果提早暴露了service,consumer在调用时就会穿透缓存,导致DB压力变大。

这个时候设置一个延迟时间(>10s)来让service晚一点暴露则是很关键的。

3.2 场景二:平滑发布(本篇重点)

某些外部容器(比如tomcat)在未完全启动完毕之前,对于dubbo service的调用会存在阻塞,导致consumer端timeout,这种情况在发布的时候有一定概率会发生。

为了避免这个问题,设置一定的延时时间(保证在tomcat启动完毕之后)就可以做到平滑发布。

四、dubbo延迟暴露使用及原理

4.1 使用

老的spring工程(xml)和spring boot工程(properties)的用法不太一样,下面针对这2种用法做介绍。

4.1.1 xml配置

provider级别的配置:

<!-- delay属性,表示延迟时间,单位ms。这里延迟20s暴露 -->
<dubbo:provider delay="20000"/>

service级别的配置:

<!-- 关键就是delay属性,这里延迟3s暴露 -->
<dubbo:service interface="com.xxx.xxxService" ref="xxxService" delay="3000"/>

思考题:会不会有method级别的delay配置?想想dubbo的注册流程...

4.1.2 Spring Boot工程的配置

springboot工程的特色就是配置变少了,少量的properties配置+各种组件的xxx-spring-boot-autoconfigure就搞定了大部分的配置。

dubbo延迟暴露在application.properties中的配置如下:

# 单位也是ms,这里表示延迟3s暴露
dubbo.provider.delay = 3000

注意:在properties中只能配置provider级别的延迟,如果你想配置service级别的延迟,可以通过xml或者注解的方式。

用注解的方式配置service级别的延迟如下:

import com.alibaba.dubbo.config.annotation.Service;

@Service(delay = 3000)
public class CategoryTreeServiceImpl implements CategoryTreeService {
 ...   
}

注意:上面@Service注解import的是dubbo包的,不是用的spring包的

4.2 原理

dubbo延迟暴露在源码中主要体现在ServiceBean类和它的父类ServiceConfig中。

以下是我从dubbo源码中把延迟暴露相关的代码抠出来的精简代码。

/**
 * 这个类相当于就是在xml中配置的<dubbo:service ... />所代表的一个bean
 */
public class ServiceBean<T> extends ServiceConfig<T> implements InitializingBean,                                                   DisposableBean, ApplicationContextAware,                                                ApplicationListener<ContextRefreshedEvent>,                                           BeanNameAware, ApplicationEventPublisherAware {
    //...
    //此方法是在spring容器初始化完成后触发的一个事件回调
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        if (isDelay() && !isExported() && !isUnexported()) {
            //...
            export();
        }
    }

    private boolean isDelay() {
        Integer delay = getDelay();//这里取的是service中的delay配置
        ProviderConfig provider = getProvider();
        
        //如果service没有配置delay则再取provider级别的delay配置
        if (delay == null && provider != null) {
            delay = provider.getDelay();
        }
        
        /*
         * supportedApplicationListener你可以理解成肯定是true,所以结果就看后面
         * 1. 默认不配置delay(即delay=null)或配置delay=-1的情况下则return true
         * 2. 如果delay配置了除-1以外的值(如delay=3000)则return false
         */
        return supportedApplicationListener && (delay == null || delay == -1);
    }
    
    @Override
    public void afterPropertiesSet() throws Exception {
        //...
        if (!isDelay()) {
            export();
        }
    }
    
    @Override
    public void export() {
        super.export();
        //...
    }
}

/**
 * 这个类是真正处理service暴露的地方
 */
public class ServiceConfig<T> extends AbstractServiceConfig {
    //...
    
    public synchronized void export() {
        if (provider != null) {
            if (export == null) {
                export = provider.getExport();
            }
            //这里优先用的是service级别的delay配置, 如果为null则再取provider级别的delay配置
            if (delay == null) {
                delay = provider.getDelay();
            }
        }
        if (export != null && !export) {
            return;
        }

        //如果配置了delay, 则用延迟任务(延迟时间就是delay的配置)去执行doExport()
        if (delay != null && delay > 0) {
            delayExportExecutor.schedule(new Runnable() {
                @Override
                public void run() {
                    doExport();
                }
            }, delay, TimeUnit.MILLISECONDS);
        } else {//如果没有配置delay, 则马上执行doExport()
            doExport();//这是真正暴露服务的方法
        }
    }
}

从上面的代码分析,ServiceBean作为spring bean时有2个关键的生命周期:

  1. 在初始化一个ServiceBean时,会执行afterPropertiesSet()
  2. 在spring容器初始化完成时,会执行onApplicationEvent(ContextRefreshedEvent event)

而对dubbo服务的暴露时机也是基于上面这2个入口控制的,中间穿插了对delay配置的判断及延迟任务的控制。

ServiceBean类中的isDelay()这个方法主要就是用来判断服务是否需要延迟暴露的。

注意!注意!注意!下面这个点必须注意!

这里的isDelay()方法从名字上会让人理解成是配置delay则返回true,没有配置delay则返回false。但事实刚好相反,\color{red}{配了}delay参数(比如delay=2000)时isDelay()返回\color{red}{false}\color{red}{未配置}delay参数时isDelay()返回\color{red}{true}

ServiceBean类的afterPropertiesSetonApplicationEvent方法中都有可能执行export()来暴露服务,区别就是这2个方法中对isDelay()的判断是相反的,afterPropertiesSet中是if(!isDelay())onApplicationEvent中是if(isDelay()),所以最终只会在其中一个地方去执行export()。

4.2.1 代码执行时序图

下面是没有配置延迟配置了延迟这2种情况分别对应的时序图。

非延迟时序图

延迟时序图

小结:

说明:dubbo 2.6.5之前版本和之后版本在延迟暴露策略有一些区别,这里不再展开讨论,可以参考官方文档http://dubbo.apache.org/zh-cn/docs/user/demos/delay-publish.html

五、平滑发布案例分析

5.1 老的Java工程为什么需要延迟暴露

5.1.1 当rest协议和外置Tomcat结合时

rest协议其实就是http请求,所以需要配合web server来使用。由于我司用的是Tomcat,所以我以Tomcat为例来说。

当用的是外置Tomcat作为容器时,rest协议配置的端口号(port)需要和Tomcat中server.xml的http端口号保持一致。

5.1.1.1 未配置延迟暴露的问题

假设现在配置的rest协议端口号是8001,那么在非延迟暴露的情况下,整个启动的流程如下图所示:

上图的关键点有2个:

  1. 一个服务暴露出去按照协议会注册多个provider的URL(这里rest和dubbo协议会注册2个URL),consumer端如果没有指定reference的协议,那么负载均衡器有一定概率会走到rest协议对应的URL(原理见下面的图4),这个时候就会通过Tomcat所监听的8001端口。
  2. dubbo provider在暴露服务的时候,Tomcat还没有进行组件start的步骤,此时虽然8001端口已经暴露出去,但是socket是不接受请求的。此时如果有8001端口的请求进来,会wait直到Tomcat启动完毕。
图4

基于以上2点,我们在看consumer端配置的timeout是多少,假设rest请求到Tomcat启动完毕的时间超过了timeout,那么consumer端就会throw Exception:timeout。这样,未配置延迟暴露所导致的平滑发布问题就出现了。

5.1.1.2 配置延迟暴露来解决问题

接下来我们再看下配置了延迟暴露后的启动流程:

上图的关键点就在于通过延时任务来进行服务暴露,而延时任务的触发是在Tomcat启动完成之后,这样来保证rest请求过来时,Tomcat已经准备好并且可以正常处理请求了。以此解决了平滑发布的问题。

注意:这里的延时任务的触发时间是通过delay的具体值来保证的,如果delay配的特别小,那么延时任务的触发并一定在Tomcat启动完成之后。

5.1.2 dubbo协议会出问题吗

上面我们讨论的都是基于rest协议的请求可能会出现平滑发布的问题,那么如果consumer用的是dubbo协议,问题还会出现吗?

其实dubbo协议是不会有问题的。原因在于dubbo协议的请求在provider端是用NettyServer来处理的,而NettyServer在第一个服务暴露之前就会完全初始化完毕并等待连接了,NettyServer本身不依赖Tomcat,所以不存在Tomcat这种服务暴露和接受请求之间存在时间差的问题。

那么本质上来讲,上面的问题主要还是由于rest协议所引起的(PHP只能通过rest协议调用,有些Java的consumer也没有指定协议),如果指定用dubbo协议去调用服务的话,这个问题也就没有了。

5.2 新的Spring Boot工程为什么就不用了

Spring Boot工程除了配置少,我个人觉得最大的好处就是集成了内嵌的服务器(比如Tomcat),部署特别简单,直接调main函数就行。那在dubbo服务暴露的问题上,Spring Boot工程和老的spring工程到底有什么区别呢?

5.2.1 当rest协议和内嵌Tomcat结合时

我们先来看一下Spring Boot工程基于内嵌Tomcat的启动流程,这里只是关注dubbo服务暴露的问题。

注意:上图是基于未配置延迟暴露下的启动流程。

上图的关键点就在于暴露服务前会先启动内嵌的Tomcat,等待内嵌Tomcat启动完毕之后再去做暴露动作,这个时候Tomcat已经具备了完整的处理能力,在步骤1.5请求进来时,Tomcat就开始马上处理请求了。

因为当Spring Boot工程结合内嵌Tomcat部署时,则不存在上面说的平滑发布的问题。

5.2.2 rest协议已不受待见

除了Spring Boot本身的原因以外,rest协议本身的使用场景已经越来越少了,也就是说以后这样的平滑发布问题其实就越来越少了。

因为rest的短连接(http)请求对于高并发的接口调用场景是不太适合的。而dubbo协议是基于长连接,避免了创建连接和销毁连接的消耗,更适合互联网的高并发场景。

那rest的存在还有什么意义?

我理解rest的意义主要还是为了跨语言(比如给PHP调用),因为rest协议本质就是http。

但是现在公司都在各种Java化,大部分后端业务用的都是Java语言,所以rest的跨语言优势就没那么明显了,包括公司现在的Java网关在进行dubbo泛化调用时,都指定了使用dubbo协议

六、总结

上一篇下一篇

猜你喜欢

热点阅读