一、引子
最近搭建了一个新的Java工程,主要是提供dubbo服务给其他业务用的。突然想起之前dubbo服务都会配置延迟暴露来解决平滑发布的问题,但是好像现在新的Java项目都没有配置延迟暴露了,觉得很奇怪,所以去研究了一下关于dubbo延迟暴露的细节。
说明:
- 延迟暴露(export)也叫延迟注册(register),为了统一概念,后续内容统一称“延迟暴露”。
- 本篇文章是基于dubbo 2.6.6来讲的。
本篇文章主要介绍了以下几点:
- 什么是dubbo延迟暴露
- 延迟暴露解决了什么问题
- dubbo延迟暴露使用及原理
- 结合公司老项目和新项目的平滑发布问题来分析延迟暴露的使用案例
二、什么是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个关键的生命周期:
- 在初始化一个
ServiceBean
时,会执行afterPropertiesSet()
- 在spring容器初始化完成时,会执行
onApplicationEvent(ContextRefreshedEvent event)
而对dubbo服务的暴露时机也是基于上面这2个入口控制的,中间穿插了对delay配置的判断及延迟任务的控制。
在ServiceBean
类中的isDelay()
这个方法主要就是用来判断服务是否需要延迟暴露的。
注意!注意!注意!下面这个点必须注意!
这里的isDelay()
方法从名字上会让人理解成是配置delay则返回true,没有配置delay则返回false。但事实刚好相反,delay参数(比如delay=2000)时isDelay()
返回,delay参数时isDelay()
返回。
ServiceBean
类的afterPropertiesSet
和onApplicationEvent
方法中都有可能执行export()来暴露服务,区别就是这2个方法中对isDelay()
的判断是相反的,afterPropertiesSet
中是if(!isDelay())
,onApplicationEvent
中是if(isDelay())
,所以最终只会在其中一个地方去执行export()。
4.2.1 代码执行时序图
下面是没有配置延迟和配置了延迟这2种情况分别对应的时序图。
非延迟时序图
延迟时序图
小结:
- 延迟(配了delay参数)暴露服务是在
ServiceBean
的afterPropertiesSet
方法(bean初始化时)中执行export(),然后通过延时任务(ScheduledExecutor
)来触发服务暴露的。 - 非延迟(未配置delay参数)暴露服务是在
ServiceBean
的onApplicationEvent
方法(spring容器初始化完成时)中执行export()来立即触发服务暴露的。
说明: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个:
- 一个服务暴露出去按照协议会注册多个provider的URL(这里rest和dubbo协议会注册2个URL),consumer端如果没有指定reference的协议,那么负载均衡器有一定概率会走到rest协议对应的URL(原理见下面的图4),这个时候就会通过Tomcat所监听的8001端口。
- dubbo provider在暴露服务的时候,Tomcat还没有进行组件start的步骤,此时虽然8001端口已经暴露出去,但是socket是不接受请求的。此时如果有8001端口的请求进来,会wait直到Tomcat启动完毕。
基于以上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协议。
六、总结
- dubbo服务默认是在spring容器初始化完成时(
onApplicationEvent
)暴露,如果配置了delay参数且delay>0(单位ms),则会进行延迟暴露(初始化bean时afterPropertiesSet
->export
->ScheduleExecutor
)。 - delay的配置有provider级别和service级别2种,Spring工程可在xml中配置;Spring Boot工程可在properties中声明provider级别配置,在service实现类上通过注解声明service级别配置。
- 外置Tomcat部署dubbo应用时的平滑发布问题(consumer调用会timeout),本质是因为consumer端用rest协议请求的时候provider端的Tomcat还没有完全启动所导致的,可以通过dubbo服务延迟暴露来解决。
- Spring Boot工程结合内嵌Tomcat不会有平滑发布的问题,因为在服务暴露前会等待内嵌Tomcat完全启动。
- consumer端可以尽量指定使用dubbo协议来提升一点点的调用性能。