建設(shè)一個大型網(wǎng)站大概費用磁力王
你好,我是何輝。今天我們來聊一聊Dubbo的大廠高頻面試題。
大廠面試,一般重點考察對技術(shù)理解的深度,和中小廠的區(qū)別在于,不僅要你精于實戰(zhàn),還要你深懂原理,勤于思考并針對功能進(jìn)行合理的設(shè)計。
網(wǎng)上一直流傳著“面試造火箭,工作擰螺絲”的大廠面試要求,其實原因也很簡單,一來面試競爭者多,需要設(shè)置門檻,二來是期望盡可能挑選出綜合素質(zhì)能力出眾的面試者,在對應(yīng)崗位上能把事情做精做細(xì),更加智能簡單,最好每一次的功能迭代都是一次性的、穩(wěn)定的、高效的、靠譜的,沒有反反復(fù)復(fù)的 BUG 修改。
因為這樣無形中可以節(jié)省很多成本(修復(fù)BUG成本、溝通成本、人力成本、時間成本等等),簡單來講,企業(yè)希望大家用最少的時間,干最多的活,而且一出手就是王炸級別的高可用、高可擴(kuò)展、高性能,穩(wěn)定靠譜地在產(chǎn)線運行。
現(xiàn)在,你還會不會覺得大廠在面試環(huán)節(jié)故意問各種底層原理來刁難你呢?其實面試官也犯不著為難你,只要你能力出眾,對于老江湖的伯樂面試官而言是可遇不可求的。但也不乏有點小心思的面試官,擔(dān)心強(qiáng)者會逐漸取代自己,不過,如果你有技術(shù)、有思想、有能力,即便這一家沒面上,換一家就是了,天下之大,總有你的一席之地。
如果大廠在面試中問到Dubbo,一般會問框架的整體架構(gòu)、常用技術(shù)點背后的底層邏輯、生僻的底層技術(shù)點,或者讓你談?wù)剬σ恍﹩栴}的看法,總之,希望能從中看到你對 Dubbo 框架的掌握程度,以此來評判你是否具有駕馭 Dubbo 框架的深厚功底。
如果遇到對 Dubbo 特別精通的面試者,大概率會作為重要候選人,入司的話會考慮把一些核心的系統(tǒng)或功能,或偏底層的通用功能開發(fā)交付,給這樣的人選來處理。
我也整理了常見的14個Dubbo大廠面試問題,你可以先嘗試自己回答一下。
1.Dubbo 源碼分層模塊是怎樣的? 2.Dubbo 是如何掃描含有 @DubboService 這種注解的類的? 3.Dubbo SPI 解決了 JDK SPI 的什么問題? 4.簡要描述下 Dubbo SPI 與 Spring SPI 的加載原理? 5.LinkedHashMap 可以設(shè)計成 LRU 么? 6.利用 Dubbo 框架怎么做分布式限流呢? 7.Wrapper 是怎么降低調(diào)用開銷的? 8.使用 Javassist 編譯的有哪些關(guān)鍵要素環(huán)節(jié)? 9.使用 ASM 編譯有哪些基本步驟? 10.Dubbo 是怎么完成實例注入與切面攔截的? 11.服務(wù)發(fā)布的流程是怎樣的? 12.服務(wù)訂閱的流程是怎樣的? 13.消費方調(diào)用流程是怎樣的? 14.你有研究過 Dubbo 的協(xié)議幀格式么?
這些問題,都是我們課程中講過的知識點,所以你也可以考核一下自己的學(xué)習(xí)情況。我們來看看每個知識點你掌握得如何。
問題一
- Dubbo 源碼分層模塊是怎樣的?
這個問題是想對 Dubbo 整體代碼分層結(jié)構(gòu)的熟悉程度,判斷你有沒有深入研究過 Dubbo 框架體系,一般答到有 Config、Proxy、Cluster 層就差不多及格了,如果能至上而下縱向詳細(xì)說出 Dubbo 的十個層次模塊,就更好了,會讓面試官刮目相看。
我們在“源碼框架”中就通過一個簡單的消費方調(diào)用逐步分析過 Dubbo 的十層模塊,如果不太記得,你可以復(fù)習(xí)鞏固一下。
主要分為三大塊,第一大塊是和business緊密相關(guān)的 Service 層,第二大塊是和RPC緊密相關(guān)的Config、Proxy、Registry、Cluster、Monitor和Protocol,剩下的第三大塊是和Remoting 緊密相關(guān)的Exchange、Transport、Serialize。
- Service,與業(yè)務(wù)邏輯關(guān)聯(lián)緊密的一層稱為服務(wù)層。
- Config,專門存儲與讀取配置打交道的層次稱為配置層。
- Proxy,代理接口發(fā)起遠(yuǎn)程調(diào)用,或代理接收請求進(jìn)行實例分發(fā)處理的層次,稱為服務(wù)代理層。
- Registry,與注冊中心打交道的層次,稱為注冊中心層。
- Cluster,封裝多個提供者并承擔(dān)路由過濾和負(fù)載均衡的層次,稱為路由層。
- Monitor,同步調(diào)用結(jié)果的層次稱為監(jiān)控層。
- Protocol,封裝調(diào)用過程的層次稱為遠(yuǎn)程調(diào)用層。
- Exchange,封裝請求并根據(jù)同步異步模式獲取響應(yīng)結(jié)果的層次,稱為信息交換層。
- Transport,將數(shù)據(jù)通過網(wǎng)絡(luò)發(fā)送至對端服務(wù)的層次稱為網(wǎng)絡(luò)傳輸層。
- Serialize,把對象與二進(jìn)制進(jìn)行相互轉(zhuǎn)換的正反序列化的層次稱為數(shù)據(jù)序列化層。
問題二
- Dubbo 如何掃描含有 @DubboService 這種注解的類?
這個問題是想看你對 Dubbo 掃描自定義注解的掌握程度,一般說到“生成了代理”就基本沾邊及格了,如果能詳細(xì)說出掃描器的源頭是利用了Spring掃描特性,就更好,因為懂了這些原理,之后公司有需求,要你根據(jù)業(yè)務(wù)功能抽象插件,或者在系統(tǒng)中通過無侵入性進(jìn)行技術(shù)改造,對你來說就是小菜一碟。
我們在“集成框架”中仿照 Spring 類掃描機(jī)制對 integration 層代碼進(jìn)行改造時,提到過這些。
Dubbo 利用了一個 DubboClassPathBeanDefinitionScanner 類繼承了 ClassPathBeanDefinitionScanner,充分利用 Spring 自身已有的擴(kuò)展特性來掃描自己需要關(guān)注的三個注解類,org.apache.dubbo.config.annotation.DubboService、org.apache.dubbo.config.annotation.Service、com.alibaba.dubbo.config.annotation.Service,然后完成 BeanDefinition 對象的創(chuàng)建。
在 BeanDefinition 對象的實例化完成后,在容器觸發(fā)刷新的事件過程中,通過回調(diào)了 ServiceConfig 的 export 方法完成了服務(wù)導(dǎo)出,即完成 Proxy 代理對象的創(chuàng)建,最后在運行時就可以直接被拿來使用了。
問題三
- Dubbo SPI 解決了 JDK SPI 的什么問題?
這個問題是想看你對 Dubbo SPI 在算法性能層面的掌握,一般說出“利用了緩存功能”就及格了,如果能說出 JDK SPI 的不足,以及 Dubbo SPI 怎么解決不足,怎樣通過算法改善的,就更好了。能這么詳細(xì)地描述前后對比情況,說明平常你在編寫代碼時,會主動思考功能的性能優(yōu)化,這是一種難能可貴的自我思考意識,比較受面試官的喜愛。
我們在“SPI 機(jī)制”中通過運行 JDK SPI 的程序,分析過 JDK SPI 的不足問題,也分析如何改善。
JDK SPI 使用一次,就會一次性實例化所有實現(xiàn)類。為了彌補(bǔ)我們分析的 JDK SPI 的不足,Dubbo 也定義出了自己的一套 SPI 機(jī)制邏輯,既要通過 O(1) 的時間復(fù)雜度來獲取指定的實例對象,還要控制緩存創(chuàng)建出來的對象,做到按需加載獲取指定實現(xiàn)類。
Dubbo SPI 在實現(xiàn)的過程中,采用了兩種方式來優(yōu)化。
- 方式一,增加緩存,來降低磁盤IO訪問以及減少對象的生成。
- 方式二,使用Map的hash查找,來提升檢索指定實現(xiàn)類的性能。
通過兩種方式的優(yōu)化后,在面對大量高頻調(diào)用時,JDK SPI 可能會出現(xiàn)磁盤 IO 吞吐下降、大量對象產(chǎn)生和查詢指定實現(xiàn)類的 O(n) 復(fù)雜度等問題,而 Dubbo SPI 采用緩存+Map的組合方式更加友好地避免了這些情況,即使大量調(diào)用,也問題不大。
問題四
- 簡要描述下 Dubbo SPI 與 Spring SPI 的加載原理?
在問題三的基礎(chǔ)上,這是想繼續(xù)看你對 Dubbo SPI 加載實現(xiàn)的底層原理的掌握程度,一般說出“通過加載資源文件目錄的文件”就及格了,如果能詳細(xì)說出有哪些資源目錄,然后根據(jù)自己的理解,說明這些目錄一般存放什么類型的 SPI 接口,就更好。
能充分說明你不但知曉 Dubbo SPI 的優(yōu)勢點,還能知道 Dubbo SPI 為了提供這樣的優(yōu)勢,是如何完成底層實現(xiàn)的,并且還能橫向掌握 Spring SPI 的加載原理。足以說明你不但對 Dubbo SPI 有過研究,Spring SPI 這種高級特性的底層原理,也有過深入研究,那么針對工作中一些技術(shù)類改造的需求,你應(yīng)該比較擅長靈活擴(kuò)展。
我們在“SPI 機(jī)制”的思考題答案中有講過,如果不太記得,可以復(fù)習(xí)鞏固一下。
Dubbo SPI 的核心加載原理,就是加載了以下三個資源路徑下的文件內(nèi)容,資源分別為。
- META-INF/dubbo/internal/
- META-INF/dubbo/
- META-INF/services/
我們自己設(shè)計的 SPI 接口,放到這 3 個資源路徑下都可以,不過從路徑的名稱上可以看出,META-INF/dubbo/internal/ 存放的是 Dubbo 內(nèi)置的一些擴(kuò)展點,META-INF/services/ 存放的是 Dubbo 自身的一些業(yè)務(wù)邏輯所需要的一些擴(kuò)展點,而 META-INF/dubbo/ 存放的是上層業(yè)務(wù)系統(tǒng)自身的一些定制 Dubbo 的相關(guān)擴(kuò)展點。
而相比于 JDK 原生的SPI,Spring 中的 SPI 功能也很強(qiáng)大,主是通過 org.springframework.core.io.support.SpringFactoriesLoader#loadFactories 方法讀取所有 jar 包的“META-INF/spring.factories”資源文件,并從文件中讀取一堆的類似 EnableAutoConfiguration 標(biāo)識的類路徑,把這些類創(chuàng)建對應(yīng)的 Spring Bean 對象注入到容器中,就完成了 SpringBoot 的自動裝配。
問題五
- LinkedHashMap 可以設(shè)計成 LRU 么?
這個問題是想看你對 Map 工具類的掌握程度,說出通過自定義工具類并內(nèi)部組合使用 Map,在 put 操作時進(jìn)行 LRU 功能設(shè)計,在設(shè)計層面一般及格了。
如果能充分挖掘 Map 的底層特性,通過繼承 Map 并重寫 removeEldestEntry 方法來靈活擴(kuò)展為 LRU,并且還能說出 Dubbo 框架在什么功能也對比進(jìn)行擴(kuò)展過,就更好了,能充分說明你對 Map 工具類的掌握非常深,對 Map 的底層原理了解非常透徹,還能知道該特性在 Dubbo 框架中的應(yīng)用,難能可貴。
LinkedHashMap 可以設(shè)計成 LRU,在“緩存操作”那講中,我們跟蹤了一個所謂的 LRU2Cache 緩存類,它的底層實現(xiàn)原理就是繼承了 LinkedHashMap 的類,然后重寫了父類 LinkedHashMap 中的 removeEldestEntry 方法,當(dāng) LRU2Cache 存儲的數(shù)據(jù)個數(shù)大于設(shè)置的容量后,會刪除最先存儲的數(shù)據(jù),讓最新的數(shù)據(jù)能夠保存進(jìn)來。
細(xì)節(jié)處的源碼跟蹤,我也展示在這里。
// LRU2Cache 的帶參構(gòu)造方法,在 LruCache 構(gòu)造方法中,默認(rèn)傳入的大小是 1000
org.apache.dubbo.common.utils.LRU2Cache#LRU2Cache(int)
public LRU2Cache(int maxCapacity) {super(16, DEFAULT_LOAD_FACTOR, true);this.maxCapacity = maxCapacity;this.preCache = new PreCache<>(maxCapacity);
}
// 若繼續(xù)放數(shù)據(jù)時,若發(fā)現(xiàn)現(xiàn)有數(shù)據(jù)個數(shù)大于 maxCapacity 最大容量的話
// 則會考慮拋棄掉最古老的一個,也就是會拋棄最早進(jìn)入緩存的那個對象
@Override
protected boolean removeEldestEntry(java.util.Map.Entry<K, V> eldest) {return size() > maxCapacity;
}↓
// JDK 中的 LinkedHashMap 源碼在發(fā)生節(jié)點插入后
// 給了子類一個擴(kuò)展刪除最舊數(shù)據(jù)的機(jī)制
java.util.LinkedHashMap#afterNodeInsertion
void afterNodeInsertion(boolean evict) { // possibly remove eldestLinkedHashMap.Entry<K,V> first;if (evict && (first = head) != null && removeEldestEntry(first)) {K key = first.key;removeNode(hash(key), key, null, false, true);}
}
問題六
- 利用 Dubbo 框架怎么來做分布式限流呢?
這個問題是想看你利用 Dubbo 過濾器的特性來處理分布式功能的掌握程度,一般答到 Filter、Redis 等關(guān)鍵詞就算是及格了,如果能總結(jié)出使用過濾器進(jìn)行限流改造的方法流程,就更好了,至少可以說明你不但對過濾器十分了解,平時還注重方法流程的總結(jié),這些總結(jié)出來的方法流程,你應(yīng)該可以很好地平移到其他框架的學(xué)習(xí)上,快速上手新項目。
關(guān)鍵一點是需要利用 Dubbo 框架的過濾器特性,結(jié)合方法層面的參數(shù)設(shè)置,就可以很好的做到分布式限流的控制。具體可以參考“流量控制”的分布式限流方案改造。
這里也提煉下控制流量的三個關(guān)鍵環(huán)節(jié)。
- 第一,尋找請求流經(jīng)的必經(jīng)之路,并在必經(jīng)之路上找到可擴(kuò)展的接口。
- 第二,找到該接口的眾多實現(xiàn)類,研究在觸發(fā)調(diào)用的入口可以拿到哪些數(shù)據(jù),再研究關(guān)于方法的入?yún)?shù)據(jù)、方法本身信息以及方法歸屬類的信息可以通過哪些 API 拿到。
- 第三,根據(jù)限流的核心計算模塊,逐漸橫向擴(kuò)展,從單個方法到多個方法,從單個服務(wù)到多個服務(wù),從單個節(jié)點到集群節(jié)點,盡可能周全地考慮通用處理方式,同時站在使用者的角度,做到簡單易用的效果。
問題七
- Wrapper 是怎么降低調(diào)用開銷的?
這個問題是想看你對 Dubbo 底層處理調(diào)用時的性能開銷掌握程度,一般能答到生成代理類就沾邊了,如果能一針見血說出 Wrapper 代理類,并且描述代理類的執(zhí)行邏輯,那就更完美了。
能描述出代理類的執(zhí)行邏輯,基本上都對 Wrapper 機(jī)制有過深入研究,因為 Wrapper 代理類是運行時的對象,如果不刻意去斷點調(diào)試生成文件查看,是很難挖掘出 Wrapper 的核心內(nèi)幕的,所以,能把調(diào)用開銷講明白的人,能迅速研究透各種底層框架的核心調(diào)用流程。
我們在“Wrapper 機(jī)制”中詳細(xì)講過。
首先得搞清楚,Wrapper 用在哪里了??匆欢未a,提供方是這么使用 Wrapper 來生成代理類的。
// org.apache.dubbo.rpc.proxy.javassist.JavassistProxyFactory#getInvoker
// 創(chuàng)建一個 Invoker 的包裝類
@Override
public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {// 這里就是生成 Wrapper 代理對象的核心一行代碼final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ? proxy.getClass() : type);// 包裝一個 Invoker 對象return new AbstractProxyInvoker<T>(proxy, type, url) {@Overrideprotected Object doInvoke(T proxy, String methodName,Class<?>[] parameterTypes,Object[] arguments) throws Throwable {// 使用 wrapper 代理對象調(diào)用自己的 invokeMethod 方法// 以此來避免反射調(diào)用引起的性能開銷// 通過強(qiáng)轉(zhuǎn)來實現(xiàn)統(tǒng)一方法調(diào)用return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments);}};
}
Wrapper 最終調(diào)用了 getWrapper 方法來生成一個代理類。
- 以源對象的類屬性為維度,與生成的代理類建立緩存映射關(guān)系,避免頻繁創(chuàng)建代理類影響性能。
- 生成了一個繼承 Wrapper 的動態(tài)類,并且暴露了一個公有 invokeMethod 方法來調(diào)用源對象的方法。
- 在invokeMethod 方法中,通過生成的 if...else 邏輯代碼來識別調(diào)用源對象的不同方法。
這里也總結(jié)一下原理細(xì)節(jié)的代碼流程。
總之,Wrapper 降低開銷的主要有 2 個關(guān)鍵要素的原因。
- 原因一,生成了代理類緩存起來,避免頻繁創(chuàng)建對象。
- 原因二,代理類中的邏輯,是通過 if...else 的普通代碼進(jìn)行了強(qiáng)轉(zhuǎn)操作,轉(zhuǎn)為原始對象后繼續(xù)調(diào)用方法,而不是采用反射方式來調(diào)用方法的。
問題八
- 使用 Javassist 編譯的有哪些關(guān)鍵要素環(huán)節(jié)?
這個問題是想看你兩方面的知識,一有沒有了解過 Javassist,二有沒有研究過如何使用 Javassist 生成過代理對象,一般答出可以用 Javassist 生成代理對象就算是及格了,如果能詳細(xì)說出使用 Javassist 來生成代理類的流程步驟,說明你確實有底層框架的開發(fā)經(jīng)驗,具備比較深厚的抽象能力,是開發(fā)底層功能的好苗子。
在“Compile 編譯”中,我們利用 Javassist 編寫了實戰(zhàn)案例,也總結(jié)了方法流程。
- 首先,設(shè)計一個代碼模板。
- 然后,使用 Javassist 的相關(guān) API,通過 ClassPool.makeClass 得到一個操控類的 CtClass 對象,然后針對 CtClass 進(jìn)行 addField 添加字段、addMethod 添加方法、addConstructor 添加構(gòu)造方法等等。
- 最后,調(diào)用 CtClass.toClass 方法并編譯得到一個類信息,有了類信息,就可以實例化對象處理業(yè)務(wù)邏輯了。
問題九
- 使用 ASM 編譯有哪些基本步驟?
在問題八的基礎(chǔ)上,問這個問題是想再看看,你能否用更為底層的 ASM 進(jìn)行開發(fā),大多數(shù)情況下,項目組很少有人會去深入研究這塊,一般能說出可以利用非常底層的 ASM 進(jìn)行字節(jié)碼操作就算是不錯的了,如果能詳細(xì)講出如何利用 ASM 來生成代理類,那非常厲害了。
我們在“Compile 編譯”中也花了大量篇幅學(xué)習(xí) ASM 的開發(fā)步驟流程,如果不太記得,可以復(fù)習(xí)鞏固一下。
使用 ASM 編譯,流程操作和 Javassist 有點類似。
- 首先,還是設(shè)計一個代碼模板。
- 其次,通過 IDEA 的協(xié)助得到代碼模板的字節(jié)碼指令內(nèi)容。
- 然后,使用 Asm 的相關(guān) API 依次將字節(jié)碼指令翻譯為 Asm 對應(yīng)的語法,比如創(chuàng)建 ClassWriter 相當(dāng)于創(chuàng)建了一個類,繼續(xù)調(diào)用 ClassWriter.visitMethod 方法相當(dāng)于創(chuàng)建了一個方法等等,對于生僻的字節(jié)碼指令實在找不到對應(yīng)的官方文檔的話,可以通過“MethodVisitor + 字節(jié)碼指令”來快速查找對應(yīng)的 Asm API。
- 最后,調(diào)用 ClassWriter.toByteArray 得到字節(jié)碼的字節(jié)數(shù)組,傳遞到 ClassLoader.defineClass 交給 JVM 虛擬機(jī)得出一個 Class 類信息。
問題十
- Dubbo 是怎么完成實例注入與切面攔截的?
這個問題考察 Dubbo 框架中,實例對象創(chuàng)建過程中的擴(kuò)展機(jī)制。一般答到具有 setter 注入的特性原理就算是及格了,如果能更為詳細(xì)地描述前置、后置等擴(kuò)展機(jī)制,以及如何進(jìn)行構(gòu)造方法注入變?yōu)榘b對象等底層原理,就非常漂亮了。
如果你能把這種對象創(chuàng)建過程的原理弄得非常透徹,其他框架的對象創(chuàng)建過程都是類似的,研究起來就會輕而易舉。我們在“實例注入”中詳細(xì)跟蹤了源碼。
Dubbo 完成實例注入,主要是當(dāng) ExtensionLoader 的 getExtension 方法被調(diào)用時,才會酌情考慮是取緩存對象,還是直接創(chuàng)建對象并進(jìn)行實例注入。
直接創(chuàng)建對象的過程,主要是通過反射創(chuàng)建出來一個嬰兒對象,然后經(jīng)歷前置初始化前置處理(postProcessBeforeInitialization)、注入擴(kuò)展點(injectExtension)、初始化后置處理(postProcessAfterInitialization)三個階段,經(jīng)過三段處理的對象,我們暫且稱為“原始對象”。
然后,這個原始對象,會根據(jù) getExtension 傳入的 wrap 變量,決定是否需要將原始對象再次進(jìn)行包裹處理,若需要包裹,會將該 SPI 接口的所有包裝類排序好,以套娃的形式,將原始對象層層包裹。而包裝類上可以設(shè)置 @Wrapper 注解,結(jié)合注解,有 3 種情況決定是否需要包裝。
- 無 @Wrapper 注解,則需要包裝。
- 有 @Wrapper 注解,但是注解中的 matches 字段值為空,則需要包裝。
- 有 @Wrapper 注解,但是注解中的 matches 字段值包含入?yún)⒌臄U(kuò)展點名稱,并且 mismatches 字段值不包含入?yún)⒌臄U(kuò)展點名稱,則需要包裝。
不過重點要關(guān)注 injectExtension 方法,該方法中會從根據(jù)對象的 setter 方法獲取擴(kuò)展點名稱,然后直接從容器中找到對應(yīng)的實例,完成實例注入。
總而言之,創(chuàng)建擴(kuò)展點對象的時候,不但會通過 setter 方法進(jìn)行實例注入,而且還會通過包裝類層層包裹,就像這樣:
問題十一
- 服務(wù)發(fā)布的流程是怎樣的?
這個問題考察你平常編寫提供方服務(wù)時,對提供服務(wù)能力流程的掌握,一般能提到服務(wù)導(dǎo)出開啟了Netty服務(wù)就算是及格了,如果能從所使用的注解開始,劃分流程步驟分別講解,那樣就更好,因為你能把服務(wù)發(fā)布流程研究得非常透徹,說明你對 Dubbo 怎么提供服務(wù)能力的體系流程比較關(guān)注,想必在日常開發(fā)編碼時,對整個需求實現(xiàn)的流程體系把控也不會差。
在“發(fā)布流程”中,我們針對提供方的架構(gòu)示意圖,展開了詳細(xì)的源碼分析。
服務(wù)發(fā)布的流程,主要可以從配置、導(dǎo)出、注冊三方面描述。
- 配置流程,通過掃描指定包路徑下含有 @DubboService 注解的 Bean 定義,把掃描出來的 Bean 定義屬性,全部轉(zhuǎn)移至新創(chuàng)建的 ServiceBean 類型的 Bean 定義中,為后續(xù)導(dǎo)出做準(zhǔn)備。
- 導(dǎo)出流程,主要有兩塊,一塊是 injvm 協(xié)議的本地導(dǎo)出,一塊是暴露協(xié)議的遠(yuǎn)程導(dǎo)出,遠(yuǎn)程導(dǎo)出與本地導(dǎo)出有著實質(zhì)性的區(qū)別,遠(yuǎn)程導(dǎo)出會使用協(xié)議端口,通過 Netty 綁定來提供端口服務(wù)。
- 注冊流程,其實是遠(yuǎn)程導(dǎo)出的一個分支流程,會將提供方的服務(wù)接口信息,通過 Curator 客戶端,寫到 Zookeeper 注冊中心服務(wù)端去。
問題十二
- 服務(wù)訂閱的流程是怎樣的?
在上一個問題“提供方服務(wù)發(fā)布流程”的基礎(chǔ)上,再看看你對消費方服務(wù)訂閱流程的掌握程度,一般能提到從注冊中心獲取提供方信息就算及格了,如果能詳細(xì)說出具體的流程,并與服務(wù)發(fā)布建立流程對比,就更好了,起碼能說明你是非常細(xì)心的,在提供方和消費方之間發(fā)現(xiàn)了可對比的流程環(huán)節(jié),在對比中,發(fā)現(xiàn)問題,研究問題,這種學(xué)習(xí)方式是值得稱贊的。
我們在“訂閱流程”中通過與服務(wù)發(fā)布建立對比關(guān)系,邊猜測邊驗證,進(jìn)行了詳細(xì)的流程分析。
相比于服務(wù)的發(fā)布流程,服務(wù)訂閱流程大體類似,概括起來有 4 個步驟。
- 首先,在程序上,往往會通過 @DubboReference 注解來標(biāo)識需要訂閱哪些接口的服務(wù),并使用這些服務(wù)進(jìn)行調(diào)用。在源碼跟蹤上,可以通過該注解一路探索出背后的核心類 ReferenceConfig。
- 緊接著,在ReferenceConfig 的 get 方法會先后進(jìn)行本地引用與遠(yuǎn)程引用的兩大主干流程。
- 然后,在本地引用環(huán)節(jié)中使用的 invoker 對象是從 InjvmProtocol 中 exporterMap 獲取到的。而在遠(yuǎn)程引用環(huán)節(jié)中,創(chuàng)建 invoker 的核心邏輯是在 RegistryProtocol 的 doCreateInvoker 方法中完成的。
- 最后,在這段 doCreateInvoker 邏輯中,還進(jìn)行了消費者注冊和接口訂閱邏輯,訂閱邏輯的本質(zhì)就是啟動環(huán)節(jié)從注冊中心拉取一遍接口的所有提供方信息,然后為這些接口添加監(jiān)聽操作,以便在后續(xù)的環(huán)節(jié)中,提供方有任何變化,消費方這邊也能通過監(jiān)聽策略,及時感知到提供方節(jié)點的變化。
消費方訂閱的整體流程,也給你總結(jié)了圖片。
問題十三
- 消費方調(diào)用流程是怎樣的?
在掌握服務(wù)發(fā)布、服務(wù)訂閱的基礎(chǔ)上,考核你對消費方調(diào)用流程的掌握程度。
通過生成的 Proxy 代理,在 Cluster 集群擴(kuò)展器中,負(fù)載均衡通過 Transport 層的 Netty 通信框架發(fā)送數(shù)據(jù),說到這里就基本及格了,如果能像講述十層模塊一樣,自上而下說出調(diào)用環(huán)節(jié)設(shè)計的層次模塊和關(guān)鍵核心方法或類,就更好了,可以說明你平時非常注重底層原理流程的研究,是個不可多得的人才。
我們在“調(diào)用流程”中,從消費方的簡單調(diào)用代碼開始,詳細(xì)分析了,如果不太記得,可以復(fù)習(xí)鞏固一下。
這里我就用一張圖總結(jié)。消費方的調(diào)用流程,比較簡單,只要順著消費方代碼調(diào)用的地方,順著一路往下追蹤,就能抓出整個調(diào)用流程。
問題十四
- 你有研究過 Dubbo 的協(xié)議幀格式么?
這個問題是想看你對數(shù)據(jù)收發(fā)底層協(xié)議格式的掌握程度,一般能提到“定長+變長的協(xié)議格式”就及格了,如果能詳細(xì)說出 Dubbo 協(xié)議幀格式的細(xì)節(jié)字段,那就更好,說明你在數(shù)據(jù)收發(fā)粘包、半包這塊了解深入,操控底層 Socket 進(jìn)行數(shù)據(jù)收發(fā)基本上沒什么問題。
我們在“協(xié)議編解碼”中通過在 NettyCodecAdapter 中進(jìn)行了斷點,詳細(xì)研究了編解碼代碼,如果不太記得,可以復(fù)習(xí)鞏固一下。
Dubbo 的協(xié)議幀本質(zhì)就是“定長 + 變長”的整個報文格式。
- magic high:魔術(shù)高位,占用 8 bit,也就是 1 byte。該值固定為 0xda,是一種標(biāo)識符。
- magic low:魔術(shù)低位,占用 8 bit,也就是 1 byte。該值固定為 0xbb,也是一種標(biāo)識符。
魔術(shù)低位和魔術(shù)高位合并起來就是 0xdabb,代表著 dubbo 數(shù)據(jù)協(xié)議報文的開始。如果從通信 socket 收到的報文不是以 0xdabb 開始的,可以認(rèn)為是非法報文。
request flag and serialization id:請求類型和序列化方式,占用 8 bit,也就是 1 byte。前面 4 bit 是請求類型,后面 4 bit 是序列化方式,合起來用 1 個 byte 來表示。
response status:響應(yīng)碼,占用 8 bit,也是 1 byte。因為已經(jīng)明確是響應(yīng)碼了,所以一個請求發(fā)送出去的時候,不用填充這個值,響應(yīng)回來的時候,這里就有值了。
但是這個響應(yīng)碼,并不是那些真實業(yè)務(wù)數(shù)據(jù)功能的響應(yīng)碼,而是 Dubbo 通信層面的錯誤碼*,比如通信響應(yīng)成功碼、消費方超時碼、服務(wù)方超時碼、請求格式錯誤碼等等,都是一些 Dubbo 框架自己易于通信識別錯誤的碼,并非那些真正上層業(yè)務(wù)功能的錯誤碼。
request id:請求唯一ID,占用 64 bit,也就是 8 byte。標(biāo)識請求的唯一性,用來證明你收到的響應(yīng),就是你曾經(jīng)發(fā)出去的請求返回來的數(shù)據(jù)。
body length:報文體長度,占用 32 bit,也就是 4 byte。體現(xiàn)真正的業(yè)務(wù)報文數(shù)據(jù)到底有多長。
因為真實的業(yè)務(wù)數(shù)據(jù)有大有小,如果報文里不告知業(yè)務(wù)數(shù)據(jù)的長度,服務(wù)方就不知道要讀取多長的字節(jié),所以,就需要知道業(yè)務(wù)報文數(shù)據(jù)到底有多長。當(dāng)客戶端發(fā)送數(shù)據(jù)時,把要發(fā)送的業(yè)務(wù)數(shù)據(jù)報文計算一下長度后,放到這個位置,服務(wù)方看到該長度后,就會讀取指定長度的字節(jié),讀完就結(jié)束,也就收到了一個完整的報文數(shù)據(jù)。
- body content:報文體數(shù)據(jù),占用的 bit 未知,占用的 byte 字節(jié)個數(shù)也未知。這里是我們真正業(yè)務(wù)數(shù)據(jù)的內(nèi)容,至于真正的業(yè)務(wù)數(shù)據(jù)的長度有多長,完全由報文體長度決定。
Dubbo 的協(xié)議幀格式,整體不難,我們只需要按照上面的幀格式一步步構(gòu)建出這樣的字節(jié)數(shù)組出來,然后利用 socket,write 出去就可以了。服務(wù)方在接收數(shù)據(jù)的時候,也是一樣,嚴(yán)格按照報文格式進(jìn)行解析,不是 oxdabb 開頭的就直接丟棄,是的話就繼續(xù)往后讀取,按照數(shù)據(jù)幀格式,直到讀完整個報文為止。
面試小技巧
這 14 個大廠常見的面試題,總的來說,都是比較偏底層的知識點,你在完成日常需求開發(fā)后,如果能再研究一下框架,提升自己駕馭框架的能力,回答這些問題應(yīng)該比較輕松。
像這種偏底層面試問題,切記,撿重要環(huán)節(jié)分步驟回答,千萬不要想自己平常是怎么看源碼的,就憑記憶從上到下,一把梭哈講,這樣會讓面試官覺得你毫無主次,沒有邏輯概念。
針對這種底層原理分析類的問題,我給你個小建議:
- 拿到問題,先沉思幾秒,快速在腦海中回憶自己曾經(jīng)研究的整個過程。
- 回答時,把剛剛回憶的流程分為幾個步驟,一般不超過4步,太多反而會顯得啰嗦。
- 針對每個步驟,撿關(guān)鍵的講,能體現(xiàn)源碼的關(guān)鍵類也最好說出來,顯得你不但有過深入研究,還非常專業(yè)細(xì)致。
- 回答后,記得談一下自己對這個問題的看法,實在沒有啥看法不說也行。 好,中小廠和大廠的高頻面試題和面試技巧,我們就都講完了。
期待你在技術(shù)這條道路上精益求精,既要精通技術(shù)實戰(zhàn),也要深懂底層原理,更要做思考的引領(lǐng)者,讓自己在技術(shù)路上越走越遠(yuǎn),越走越香。
文章來源:極客時間《Dubbo 源碼剖析與實戰(zhàn)》