一驹闰、引子
最近搭建了一個(gè)新的Java工程爷辱,主要是提供dubbo服務(wù)給其他業(yè)務(wù)用的。突然想起之前dubbo服務(wù)都會(huì)配置延遲暴露來(lái)解決平滑發(fā)布的問(wèn)題跷车,但是好像現(xiàn)在新的Java項(xiàng)目都沒(méi)有配置延遲暴露了区丑,覺(jué)得很奇怪拧粪,所以去研究了一下關(guān)于dubbo延遲暴露的細(xì)節(jié)。
說(shuō)明:
- 延遲暴露(export)也叫延遲注冊(cè)(register)沧侥,為了統(tǒng)一概念可霎,后續(xù)內(nèi)容統(tǒng)一稱“延遲暴露”。
- 本篇文章是基于dubbo 2.6.6來(lái)講的宴杀。
本篇文章主要介紹了以下幾點(diǎn):
- 什么是dubbo延遲暴露
- 延遲暴露解決了什么問(wèn)題
- dubbo延遲暴露使用及原理
- 結(jié)合公司老項(xiàng)目和新項(xiàng)目的平滑發(fā)布問(wèn)題來(lái)分析延遲暴露的使用案例
二、什么是dubbo延遲暴露
dubbo service默認(rèn)是在容器啟動(dòng)的時(shí)候暴露的旺罢,一旦暴露旷余,consumer端就可以發(fā)現(xiàn)這個(gè)service并且調(diào)用到這個(gè)provider绢记。所謂延遲暴露即在啟動(dòng)之后延遲一定時(shí)間再暴露,比如延遲3s正卧。
三蠢熄、為什么需要延遲暴露
3.1 場(chǎng)景一:組件初始化需要一定的時(shí)間
比如你提供的service需要初始化緩存數(shù)據(jù),這個(gè)數(shù)據(jù)需要讀取DB穗酥,然后進(jìn)行計(jì)算(假設(shè)這個(gè)時(shí)間需要10s)护赊。如果提早暴露了service,consumer在調(diào)用時(shí)就會(huì)穿透緩存砾跃,導(dǎo)致DB壓力變大。
這個(gè)時(shí)候設(shè)置一個(gè)延遲時(shí)間(>10s)來(lái)讓service晚一點(diǎn)暴露則是很關(guān)鍵的节吮。
3.2 場(chǎng)景二:平滑發(fā)布(本篇重點(diǎn))
某些外部容器(比如tomcat)在未完全啟動(dòng)完畢之前抽高,對(duì)于dubbo service的調(diào)用會(huì)存在阻塞,導(dǎo)致consumer端timeout透绩,這種情況在發(fā)布的時(shí)候有一定概率會(huì)發(fā)生翘骂。
為了避免這個(gè)問(wèn)題,設(shè)置一定的延時(shí)時(shí)間(保證在tomcat啟動(dòng)完畢之后)就可以做到平滑發(fā)布帚豪。
四碳竟、dubbo延遲暴露使用及原理
4.1 使用
老的spring工程(xml)和spring boot工程(properties)的用法不太一樣,下面針對(duì)這2種用法做介紹狸臣。
4.1.1 xml配置
provider級(jí)別的配置:
<!-- delay屬性莹桅,表示延遲時(shí)間,單位ms烛亦。這里延遲20s暴露 -->
<dubbo:provider delay="20000"/>
service級(jí)別的配置:
<!-- 關(guān)鍵就是delay屬性诈泼,這里延遲3s暴露 -->
<dubbo:service interface="com.xxx.xxxService" ref="xxxService" delay="3000"/>
思考題:會(huì)不會(huì)有method級(jí)別的delay配置?想想dubbo的注冊(cè)流程...
4.1.2 Spring Boot工程的配置
springboot工程的特色就是配置變少了煤禽,少量的properties配置+各種組件的xxx-spring-boot-autoconfigure就搞定了大部分的配置铐达。
dubbo延遲暴露在application.properties中的配置如下:
# 單位也是ms,這里表示延遲3s暴露
dubbo.provider.delay = 3000
注意:在properties中只能配置provider級(jí)別的延遲檬果,如果你想配置service級(jí)別的延遲瓮孙,可以通過(guò)xml或者注解的方式。
用注解的方式配置service級(jí)別的延遲如下:
import com.alibaba.dubbo.config.annotation.Service;
@Service(delay = 3000)
public class CategoryTreeServiceImpl implements CategoryTreeService {
...
}
注意:上面@Service注解import的是dubbo包的选脊,不是用的spring包的
4.2 原理
dubbo延遲暴露在源碼中主要體現(xiàn)在ServiceBean
類和它的父類ServiceConfig
中杭抠。
以下是我從dubbo源碼中把延遲暴露相關(guān)的代碼摳出來(lái)的精簡(jiǎn)代碼。
/**
* 這個(gè)類相當(dāng)于就是在xml中配置的<dubbo:service ... />所代表的一個(gè)bean
*/
public class ServiceBean<T> extends ServiceConfig<T> implements InitializingBean, DisposableBean, ApplicationContextAware, ApplicationListener<ContextRefreshedEvent>, BeanNameAware, ApplicationEventPublisherAware {
//...
//此方法是在spring容器初始化完成后觸發(fā)的一個(gè)事件回調(diào)
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
if (isDelay() && !isExported() && !isUnexported()) {
//...
export();
}
}
private boolean isDelay() {
Integer delay = getDelay();//這里取的是service中的delay配置
ProviderConfig provider = getProvider();
//如果service沒(méi)有配置delay則再取provider級(jí)別的delay配置
if (delay == null && provider != null) {
delay = provider.getDelay();
}
/*
* supportedApplicationListener你可以理解成肯定是true知牌,所以結(jié)果就看后面
* 1. 默認(rèn)不配置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();
//...
}
}
/**
* 這個(gè)類是真正處理service暴露的地方
*/
public class ServiceConfig<T> extends AbstractServiceConfig {
//...
public synchronized void export() {
if (provider != null) {
if (export == null) {
export = provider.getExport();
}
//這里優(yōu)先用的是service級(jí)別的delay配置, 如果為null則再取provider級(jí)別的delay配置
if (delay == null) {
delay = provider.getDelay();
}
}
if (export != null && !export) {
return;
}
//如果配置了delay, 則用延遲任務(wù)(延遲時(shí)間就是delay的配置)去執(zhí)行doExport()
if (delay != null && delay > 0) {
delayExportExecutor.schedule(new Runnable() {
@Override
public void run() {
doExport();
}
}, delay, TimeUnit.MILLISECONDS);
} else {//如果沒(méi)有配置delay, 則馬上執(zhí)行doExport()
doExport();//這是真正暴露服務(wù)的方法
}
}
}
從上面的代碼分析祈争,ServiceBean
作為spring bean時(shí)有2個(gè)關(guān)鍵的生命周期:
- 在初始化一個(gè)
ServiceBean
時(shí),會(huì)執(zhí)行afterPropertiesSet()
- 在spring容器初始化完成時(shí)角寸,會(huì)執(zhí)行
onApplicationEvent(ContextRefreshedEvent event)
而對(duì)dubbo服務(wù)的暴露時(shí)機(jī)也是基于上面這2個(gè)入口控制的菩混,中間穿插了對(duì)delay配置的判斷及延遲任務(wù)的控制忿墅。
在ServiceBean
類中的isDelay()
這個(gè)方法主要就是用來(lái)判斷服務(wù)是否需要延遲暴露的。
注意沮峡!注意疚脐!注意!下面這個(gè)點(diǎn)必須注意邢疙!
這里的isDelay()
方法從名字上會(huì)讓人理解成是配置delay則返回true棍弄,沒(méi)有配置delay則返回false。但事實(shí)剛好相反疟游,delay參數(shù)(比如delay=2000)時(shí)isDelay()
返回呼畸,delay參數(shù)時(shí)isDelay()
返回。
ServiceBean
類的afterPropertiesSet
和onApplicationEvent
方法中都有可能執(zhí)行export()來(lái)暴露服務(wù)颁虐,區(qū)別就是這2個(gè)方法中對(duì)isDelay()
的判斷是相反的蛮原,afterPropertiesSet
中是if(!isDelay())
,onApplicationEvent
中是if(isDelay())
另绩,所以最終只會(huì)在其中一個(gè)地方去執(zhí)行export()儒陨。
4.2.1 代碼執(zhí)行時(shí)序圖
下面是沒(méi)有配置延遲和配置了延遲這2種情況分別對(duì)應(yīng)的時(shí)序圖。
非延遲時(shí)序圖
延遲時(shí)序圖
小結(jié):
- 延遲(配了delay參數(shù))暴露服務(wù)是在
ServiceBean
的afterPropertiesSet
方法(bean初始化時(shí))中執(zhí)行export()笋籽,然后通過(guò)延時(shí)任務(wù)(ScheduledExecutor
)來(lái)觸發(fā)服務(wù)暴露的蹦漠。 - 非延遲(未配置delay參數(shù))暴露服務(wù)是在
ServiceBean
的onApplicationEvent
方法(spring容器初始化完成時(shí))中執(zhí)行export()來(lái)立即觸發(fā)服務(wù)暴露的。
說(shuō)明:dubbo 2.6.5之前版本和之后版本在延遲暴露策略有一些區(qū)別车海,這里不再展開(kāi)討論笛园,可以參考官方文檔http://dubbo.apache.org/zh-cn/docs/user/demos/delay-publish.html
五、平滑發(fā)布案例分析
5.1 老的Java工程為什么需要延遲暴露
5.1.1 當(dāng)rest協(xié)議和外置Tomcat結(jié)合時(shí)
rest協(xié)議其實(shí)就是http請(qǐng)求容劳,所以需要配合web server來(lái)使用喘沿。由于我司用的是Tomcat,所以我以Tomcat為例來(lái)說(shuō)竭贩。
當(dāng)用的是外置Tomcat作為容器時(shí)蚜印,rest協(xié)議配置的端口號(hào)(port)需要和Tomcat中server.xml的http端口號(hào)保持一致。
5.1.1.1 未配置延遲暴露的問(wèn)題
假設(shè)現(xiàn)在配置的rest協(xié)議端口號(hào)是8001留量,那么在非延遲暴露的情況下窄赋,整個(gè)啟動(dòng)的流程如下圖所示:
上圖的關(guān)鍵點(diǎn)有2個(gè):
- 一個(gè)服務(wù)暴露出去按照協(xié)議會(huì)注冊(cè)多個(gè)provider的URL(這里rest和dubbo協(xié)議會(huì)注冊(cè)2個(gè)URL),consumer端如果沒(méi)有指定reference的協(xié)議楼熄,那么負(fù)載均衡器有一定概率會(huì)走到rest協(xié)議對(duì)應(yīng)的URL(原理見(jiàn)下面的圖4)忆绰,這個(gè)時(shí)候就會(huì)通過(guò)Tomcat所監(jiān)聽(tīng)的8001端口。
- dubbo provider在暴露服務(wù)的時(shí)候可岂,Tomcat還沒(méi)有進(jìn)行組件start的步驟错敢,此時(shí)雖然8001端口已經(jīng)暴露出去,但是socket是不接受請(qǐng)求的。此時(shí)如果有8001端口的請(qǐng)求進(jìn)來(lái)稚茅,會(huì)wait直到Tomcat啟動(dòng)完畢纸淮。
基于以上2點(diǎn),我們?cè)诳碿onsumer端配置的timeout是多少亚享,假設(shè)rest請(qǐng)求到Tomcat啟動(dòng)完畢的時(shí)間超過(guò)了timeout咽块,那么consumer端就會(huì)throw Exception:timeout。這樣欺税,未配置延遲暴露所導(dǎo)致的平滑發(fā)布問(wèn)題就出現(xiàn)了侈沪。
5.1.1.2 配置延遲暴露來(lái)解決問(wèn)題
接下來(lái)我們?cè)倏聪?strong>配置了延遲暴露后的啟動(dòng)流程:
上圖的關(guān)鍵點(diǎn)就在于通過(guò)延時(shí)任務(wù)來(lái)進(jìn)行服務(wù)暴露,而延時(shí)任務(wù)的觸發(fā)是在Tomcat啟動(dòng)完成之后晚凿,這樣來(lái)保證rest請(qǐng)求過(guò)來(lái)時(shí)亭罪,Tomcat已經(jīng)準(zhǔn)備好并且可以正常處理請(qǐng)求了。以此解決了平滑發(fā)布的問(wèn)題歼秽。
注意:這里的延時(shí)任務(wù)的觸發(fā)時(shí)間是通過(guò)delay的具體值來(lái)保證的皆撩,如果delay配的特別小,那么延時(shí)任務(wù)的觸發(fā)并一定在Tomcat啟動(dòng)完成之后哲银。
5.1.2 dubbo協(xié)議會(huì)出問(wèn)題嗎
上面我們討論的都是基于rest協(xié)議的請(qǐng)求可能會(huì)出現(xiàn)平滑發(fā)布的問(wèn)題,那么如果consumer用的是dubbo協(xié)議呻惕,問(wèn)題還會(huì)出現(xiàn)嗎荆责?
其實(shí)dubbo協(xié)議是不會(huì)有問(wèn)題的。原因在于dubbo協(xié)議的請(qǐng)求在provider端是用NettyServer來(lái)處理的亚脆,而NettyServer在第一個(gè)服務(wù)暴露之前就會(huì)完全初始化完畢并等待連接了做院,NettyServer本身不依賴Tomcat,所以不存在Tomcat這種服務(wù)暴露和接受請(qǐng)求之間存在時(shí)間差的問(wèn)題濒持。
那么本質(zhì)上來(lái)講键耕,上面的問(wèn)題主要還是由于rest協(xié)議所引起的(PHP只能通過(guò)rest協(xié)議調(diào)用,有些Java的consumer也沒(méi)有指定協(xié)議)柑营,如果指定用dubbo協(xié)議去調(diào)用服務(wù)的話屈雄,這個(gè)問(wèn)題也就沒(méi)有了。
5.2 新的Spring Boot工程為什么就不用了
Spring Boot工程除了配置少官套,我個(gè)人覺(jué)得最大的好處就是集成了內(nèi)嵌的服務(wù)器(比如Tomcat)酒奶,部署特別簡(jiǎn)單,直接調(diào)main函數(shù)就行奶赔。那在dubbo服務(wù)暴露的問(wèn)題上惋嚎,Spring Boot工程和老的spring工程到底有什么區(qū)別呢?
5.2.1 當(dāng)rest協(xié)議和內(nèi)嵌Tomcat結(jié)合時(shí)
我們先來(lái)看一下Spring Boot工程基于內(nèi)嵌Tomcat的啟動(dòng)流程站刑,這里只是關(guān)注dubbo服務(wù)暴露的問(wèn)題另伍。
注意:上圖是基于未配置延遲暴露下的啟動(dòng)流程。
上圖的關(guān)鍵點(diǎn)就在于暴露服務(wù)前會(huì)先啟動(dòng)內(nèi)嵌的Tomcat绞旅,等待內(nèi)嵌Tomcat啟動(dòng)完畢之后再去做暴露動(dòng)作摆尝,這個(gè)時(shí)候Tomcat已經(jīng)具備了完整的處理能力温艇,在步驟1.5請(qǐng)求進(jìn)來(lái)時(shí),Tomcat就開(kāi)始馬上處理請(qǐng)求了结榄。
因?yàn)楫?dāng)Spring Boot工程結(jié)合內(nèi)嵌Tomcat部署時(shí)中贝,則不存在上面說(shuō)的平滑發(fā)布的問(wèn)題。
5.2.2 rest協(xié)議已不受待見(jiàn)
除了Spring Boot本身的原因以外臼朗,rest協(xié)議本身的使用場(chǎng)景已經(jīng)越來(lái)越少了邻寿,也就是說(shuō)以后這樣的平滑發(fā)布問(wèn)題其實(shí)就越來(lái)越少了。
因?yàn)閞est的短連接(http)請(qǐng)求對(duì)于高并發(fā)的接口調(diào)用場(chǎng)景是不太適合的视哑。而dubbo協(xié)議是基于長(zhǎng)連接绣否,避免了創(chuàng)建連接和銷毀連接的消耗,更適合互聯(lián)網(wǎng)的高并發(fā)場(chǎng)景挡毅。
那rest的存在還有什么意義蒜撮?
我理解rest的意義主要還是為了跨語(yǔ)言(比如給PHP調(diào)用),因?yàn)閞est協(xié)議本質(zhì)就是http跪呈。
但是現(xiàn)在公司都在各種Java化段磨,大部分后端業(yè)務(wù)用的都是Java語(yǔ)言,所以rest的跨語(yǔ)言優(yōu)勢(shì)就沒(méi)那么明顯了耗绿,包括公司現(xiàn)在的Java網(wǎng)關(guān)在進(jìn)行dubbo泛化調(diào)用時(shí)苹支,都指定了使用dubbo協(xié)議。
六误阻、總結(jié)
- dubbo服務(wù)默認(rèn)是在spring容器初始化完成時(shí)(
onApplicationEvent
)暴露债蜜,如果配置了delay參數(shù)且delay>0(單位ms),則會(huì)進(jìn)行延遲暴露(初始化bean時(shí)afterPropertiesSet
->export
->ScheduleExecutor
)究反。 - delay的配置有provider級(jí)別和service級(jí)別2種寻定,Spring工程可在xml中配置;Spring Boot工程可在properties中聲明provider級(jí)別配置精耐,在service實(shí)現(xiàn)類上通過(guò)注解聲明service級(jí)別配置狼速。
- 外置Tomcat部署dubbo應(yīng)用時(shí)的平滑發(fā)布問(wèn)題(consumer調(diào)用會(huì)timeout),本質(zhì)是因?yàn)閏onsumer端用rest協(xié)議請(qǐng)求的時(shí)候provider端的Tomcat還沒(méi)有完全啟動(dòng)所導(dǎo)致的黍氮,可以通過(guò)dubbo服務(wù)延遲暴露來(lái)解決唐含。
- Spring Boot工程結(jié)合內(nèi)嵌Tomcat不會(huì)有平滑發(fā)布的問(wèn)題,因?yàn)樵诜?wù)暴露前會(huì)等待內(nèi)嵌Tomcat完全啟動(dòng)沫浆。
- consumer端可以盡量指定使用dubbo協(xié)議來(lái)提升一點(diǎn)點(diǎn)的調(diào)用性能捷枯。