新聞中心
你好呀,我是歪歪。

關(guān)于 RPC 調(diào)用,大家肯定都是比較熟悉的了,就是在微服務(wù)架構(gòu)下解決系統(tǒng)間通信問題的一個玩意。
其中的典型代表之一就是 Dubbo 了:
圖片
在微服務(wù)架構(gòu)下,我們針對某個 RPC 接口,我們一般有兩個角色。
- 服務(wù)消費者 (Dubbo Consumer),發(fā)起業(yè)務(wù)調(diào)用或 RPC 通信的 Dubbo 進程
- 服務(wù)提供者 (Dubbo Provider),接收業(yè)務(wù)調(diào)用或 RPC 通信的 Dubbo 進程
假設(shè)我是服務(wù)消費者,想要調(diào)用某個服務(wù),只要我們鏈接到的是同一個服務(wù)注冊中心,那么找對應(yīng)服務(wù)要到 API 包對應(yīng)的 Maven 坐標,引入到項目中,就類似于這樣的東西:
org.apache.dubbo
dubbo-spring-boot-demo-interface
${project.parent.version}
那么對于這個 API 包中的接口,雖然我們沒有具體的實現(xiàn)類,但是我們還是能像調(diào)用本地方法一樣調(diào)用該服務(wù)提供的接口。
這些都是常規(guī)的東西了,你肯定是門清。
那我現(xiàn)在問你一個問題?。?/p>
我是服務(wù)消費者,我要調(diào)用一個服務(wù)提供者的 RPC 接口,但是我又不想引入它的 API 包,或者我根本就拉取不到它的 API 包,那么我應(yīng)該怎么辦?
如果你要非給我說:這不可能,既然是要消費別人的接口,那么肯定要拿到 API 包才對,你不拿就是你偷懶。
那我再給你舉個歪師傅在實際開發(fā)過程中遇到的具體的例子:網(wǎng)關(guān)服務(wù)。
網(wǎng)關(guān)是個什么玩意?
是你對外請求的統(tǒng)一入口,做接受請求、分發(fā)請求用的,作為鏈接各個微服務(wù)的角色,你勢必要使用到下游的若干個 RPC 服務(wù)。
你怎么辦?
引入所有的服務(wù)提供方的 API 包,然后發(fā)起調(diào)用嗎?
圖片
可以是可以,但是不夠優(yōu)雅。
你想,如果有一個服務(wù)提供方發(fā)布了新的 API 包,你也需要更新版本,重新發(fā)版?
或者新來一個服務(wù)提供者 E,你需要引入其 API 包,然后重新發(fā)版?
網(wǎng)關(guān)應(yīng)該是一個穩(wěn)定的基礎(chǔ)服務(wù),它提供的是聚攏 API 接口、轉(zhuǎn)發(fā)調(diào)用的基礎(chǔ)功能,不應(yīng)該頻繁發(fā)版,不應(yīng)該主動去關(guān)注下游的服務(wù)接口變化。平臺本身不應(yīng)該依賴于服務(wù)提供方的接口 API。
不主動,才能更加優(yōu)雅,也能讓自己更加輕松。
那么怎么才能做到不主動關(guān)注呢?
這個事情,總有一方要主動的,所以網(wǎng)關(guān)層不主動,那么服務(wù)提供者就需要主動起來。
我們可以搞成這樣:
圖片
網(wǎng)關(guān)層提供一個 API 接口發(fā)布平臺,當服務(wù)提供者的接口有新增或者發(fā)生變化的時候,由對應(yīng)系統(tǒng)的接口管理人員把接口信息,比如接口路徑、方法、入?yún)?、出參、方法功能說明、方法負責團隊、接口對接人等等這些消息維護到 API 接口發(fā)布平臺上。
這樣網(wǎng)關(guān)層就可以從 API 接口發(fā)布平臺獲取到所有服務(wù)的所有接口,并不需要引入任何服務(wù)提供者的 API 包。
這樣就解決了“主動”的問題,如果接口有變化,請在 API 接口發(fā)布平臺進行登記,從而解決了網(wǎng)關(guān)頻繁發(fā)布的問題。
在官網(wǎng)上,除了網(wǎng)關(guān)的場景外,還提到一個測試平臺的場景,道理是一樣的,我就不贅述了:
https://cn.dubbo.apache.org/zh-cn/overview/mannual/java-sdk/advanced-features-and-usage/service/generic-reference/
圖片
解決了“主動”的問題,那么下一個問題就隨之而來了:知道所有服務(wù)的所有接口然后呢,怎么發(fā)起調(diào)用呢?
這個時候泛化調(diào)用,啪的一下就站出來了:鋪墊了這么多,終于該老子上場了。
泛化調(diào)用
啥是泛化調(diào)用呢?
在 Dubbo 官網(wǎng)上是這樣介紹的:
圖片
首先需要強調(diào)的是“泛化調(diào)用”不是 Dubbo 特有的,它是一個功能,很多的框架都支持泛化調(diào)用,只是我這里用的 Dubbo 做演示而已。
老規(guī)矩,先花五分鐘時間搭個 Demo 出來再說。
這個 Demo 我也是跟著網(wǎng)上的 quick start 搞的:
https://cn.dubbo.apache.org/zh-cn/overview/quickstart/java/spring-boot/
圖片
可以說寫的非常詳細了,你就跟著官網(wǎng)的步驟一步步的搞就行了。
我這個 Demo 稍微不一樣的是我在消費者模塊里面搞了一個 Http 接口:
圖片
在接口里面發(fā)起了 RPC 調(diào)用,模擬從前端頁面發(fā)起請求的場景,更加符合我們的開發(fā)習慣。
為了起到強調(diào)作用,我再次把這個部分給你框起來:
圖片
DemoService 是 RPC 接口,它的實現(xiàn)類是這樣的:
圖片
在我的消費者模塊里面為什么能注入這個 DemoService 并調(diào)用它的 sayHello 方法呢?
因為我引入了對應(yīng)的依賴包。
那么,如果我把這個依賴包去掉,也就是模擬我們前面說的“不主動”的動作,這個 DemoService 肯定會報錯,找不到這個類:
圖片
那么我們應(yīng)該怎么去修改一下這個 Demo,讓它泛化起來呢?
非常簡單:
圖片
注入 DemoService 修改為注入 GenericService。
有的小伙伴可能會問 GenericService 是怎么冒出來的?
你先別管它是怎么冒出來的,我現(xiàn)在是在給你鋪墊 Demo,后面要撕給你看。你現(xiàn)在只需要知道它是 Dubbo 框架里面的包,并不會讓你引用額外的包就行了:
圖片
現(xiàn)在 Demo 就算是搭好了,本地啟動一個 zk,然后把服務(wù)提供者啟動起來,再把消費者啟動起來,最后輕輕的發(fā)起一個調(diào)用:
圖片
朋友,它不就跑起來了嗎?
我沒有引用接口的 api 包,我不也正常發(fā)起了調(diào)用,然后拿到了返回值嗎?
啥原理
你就想,遠程調(diào)用,你把一些花里胡哨的東西都拿掉之后,它的本質(zhì)是什么?
本質(zhì)就是幫助解決微服務(wù)組件之間的通信問題,不管是基于 HTTP、HTTP/2、TCP 還是什么其他的通信協(xié)議,解決的是網(wǎng)絡(luò)連接管理、數(shù)據(jù)傳輸?shù)然A(chǔ)問題。
雖然我沒有引用 API 的對應(yīng)的包,但是我前面我不是說了嗎,我們有一個 API 接口發(fā)布平臺,這個平臺里面有接口維護人員提供的接口路徑、方法、入?yún)?、出參這些關(guān)鍵信息。
所以我在調(diào)用的時候可以拿到相關(guān)的信息,以一種通用的方式,比如字符串的方式告訴 RPC 框架,我要調(diào)用的是 DemoService 接口的 sayHello 方法,入?yún)⑹?String 類型的 world 字符串:
如果是你來開發(fā)一個 RPC 框架,調(diào)用方都把這些關(guān)鍵信息給你了,無非就是你幫忙多做幾步類似于反射、序列化之類的處理。而處理的這個過程,就是泛化調(diào)用的過程。
泛化調(diào)用不是 Dubbo 特有的,但是具體到 Dubbo 這個框架里面,具體是這樣的。
首先,Dubbo 里面有一層 Filter,這些 Filter 構(gòu)成了一個 Filter 鏈條:
圖片
Filter 用來對每次服務(wù)調(diào)用做一些預(yù)處理、后處理動作,使用 Filter 可以完成訪問日志、加解密、流量統(tǒng)計、參數(shù)驗證等任務(wù)。
一次請求過程中可以植入多個 Filter,F(xiàn)ilter 之間相互獨立沒有依賴。
圖片
從消費端視角,它在請求發(fā)起前基于請求參數(shù)等做一些預(yù)處理工作,在接收到響應(yīng)后,對響應(yīng)結(jié)果做一些后置處理。
從提供者視角,在接收到訪問請求后,在返回響應(yīng)結(jié)果前做一些預(yù)處理。
所以我們的泛化調(diào)用,也是通過下面這兩個 Filter 來搞事情的:
- org.apache.dubbo.rpc.filter.GenericFilter
- org.apache.dubbo.rpc.filter.GenericImplFilter
那么問題就來了?
為什么要兩個 Filter 呢?
因為要完成一次泛化調(diào)用,消費端和服務(wù)提供者都需要感知到并做相關(guān)的處理,所以一個是消費端的 Fliter,一個是服務(wù)提供者的 Fliter:
圖片
圖片
知道了對應(yīng)的 Filter,關(guān)于泛化調(diào)用的所有秘密都藏在 Filter 對應(yīng)的源碼里面。
歪師傅帶著你簡單的看一眼。
GenericImplFilter.invoke
首先,我們在方法的消費者對應(yīng)的 Fliter 的入口處打上斷點:
org.apache.dubbo.rpc.filter.GenericImplFilter#invoke
可以看到分為了三個模塊。
- isCallingGenericImpl:calling a generic impl service,判斷是否調(diào)用的是一個實現(xiàn)了泛化接口的接口。
- isMakingGenericCall:making a generic call to a normal service,把泛化調(diào)用轉(zhuǎn)換為一個常規(guī)調(diào)用。
- invoker.invoke(invocation):常規(guī)調(diào)用。
我們研究的情況屬于 isMakingGenericCall 這個分支。
既然是要把泛化調(diào)用轉(zhuǎn)換為一個常規(guī)調(diào)用,那么 Dubbo 是怎么判斷這是一個泛化調(diào)用的呢?
org.apache.dubbo.rpc.filter.GenericImplFilter#isMakingGenericCall
圖片
- 判斷本次調(diào)用的方法名稱是否是 或者invokeAsync
- 判斷本次調(diào)用的入?yún)€數(shù)是否是 3 個
- 判斷容器上下文中的 generic 參數(shù)是否對應(yīng)著泛化調(diào)用的序列化方法。
我們一個個的看。
或者invokeAsync 方法是 GenericService 這個接口里面的方法。而這兩個方法的入?yún)€數(shù)都是三個。
然后有個 generic 參數(shù),在我的 Demo 里面這個參數(shù)是 true:
圖片
當我啪的一下跟進到 isGeneric 方法中,才發(fā)現(xiàn)這里面別有洞天:
圖片
原來 generic 這個參數(shù)不只是可以為 “true”,它不同的值,代表著不同的序列化方式。
圖片
通過這部分源碼可以看出來,泛化調(diào)用對于客戶端,即在 GenericImplFilter 里面,并沒有做什么特別的操作,注意還是參數(shù)校驗。
如果入?yún)⒑蛯?yīng)的序列化方法不能匹配起來,即使的拋出異常,這樣符合 Dubbo 框架的 fast-fail 思想。
但是其實看到這里的時候,我有一個小疑問,如果我寫一個這樣的類:
public interface WhyService {
Object $invoke(String a,String b,String c);
}和 GenericService 類一樣,有 $invoke 方法,而且也是三個參數(shù)。
然后在上下文中塞個 generic=true 進去,那么是不是也能騙過這段代碼呢,也能進入到 isMakingGenericCall 方法里面呢?
從代碼上看確實是這樣的,那么 Dubbo 到底是怎么規(guī)避這些“惡意”冒充者的呢?
我也不知道。
先存?zhèn)€疑吧,接著往下看。
GenericFilter.invoke
我們同樣在服務(wù)端打上斷點,當這個請求來到服務(wù)端的時候,我們再看看服務(wù)端的情況。
org.apache.dubbo.rpc.filter.GenericFilter#invoke
可以看到這個方法邏輯都在 if 判斷為 true 的時候。
而這個判斷我們剛剛在客戶端已經(jīng)解析過了,只是多了一個判斷:
!GenericService.class.isAssignableFrom(invoker.getInterface())
看看發(fā)起調(diào)用的接口類是不是 GenericService 類的子類,如果是,則進入到 if 分支里面。
朋友,這就有點意思了。幾秒鐘之前我們還在存疑,然后啪的一下疑問就解開了。
直接就是恍然大悟了。
我這個類:
public interface WhyService {
Object $invoke(String a,String b,String c);
}過不了服務(wù)提供者的 GenericFilter 里面的這個判斷:
!GenericService.class.isAssignableFrom(invoker.getInterface())
在 invoke 方法里面,可以看到經(jīng)過了一個 findMethodByMethodSignature 方法,獲取了我們想要調(diào)用的 method 方法:
圖片
這個方法,從名字上也可以看出,是根據(jù)方法簽名反射出具體的方法:
圖片
在服務(wù)端,是有 DemoService 接口對應(yīng)的類的,所以可以通過反射找到它。
然后再解析出入?yún)⒌木唧w值:
圖片
這樣你就有了構(gòu)建一個 RpcInvocation 對象,即發(fā)起 RPC 調(diào)用的對象的所有關(guān)鍵消息。
直接就是發(fā)動一招“貍貓換太子”的大動作,重新構(gòu)建一個 RpcInvocation 對象,然后自己發(fā)起一個 invoke 調(diào)用。
圖片
這樣整體看起來似乎一次泛化調(diào)用也是很簡單的,當你去看服務(wù)提供端的源碼的時候,你會發(fā)現(xiàn)這里面的源碼特別多。
不過是因為 Dubbo 支持了多種不同的序列化方式而已,本質(zhì)是一樣的:
圖片
onResponse 方法也是同理,就不贅述了:
org.apache.dubbo.rpc.filter.GenericFilter#onResponse
圖片
到這里就算是扯下了泛化調(diào)用的神秘面紗,和我們預(yù)想的一樣,無非是拿到接口調(diào)用的關(guān)鍵信息之后,重新構(gòu)建一個請求而已,整體邏輯并不復雜。
復雜的邏輯是什么?
我演示的是最簡單的,入?yún)⑹且粋€ String 類型的情況。如果我是一個復雜對象呢,對象里面的成員變量特別多,對象里面套對象,對象里面有 List 或者 Map 的情況呢?
復雜的地方在于怎么處理這些復雜對象,把復雜對象搞成服務(wù)提供者的 Java 對象入?yún)ⅰ?/p>
我這里只是一個導讀而已,如果你對這部分有興趣的話,自己搞個復雜對象去研究研究吧,老有意思了。
就當是家庭作業(yè)了。
意外收獲
歪師傅在扯面紗的時候,沒想到還有意外收獲。
給你看一段代碼,也是前面出現(xiàn)過的一個方法,我把完整的代碼都截圖放出來:
org.apache.dubbo.common.utils.ReflectUtils#findMethodByMethodSignature
圖片
你瞅瞅我框起來部分的 signature 字段,是不是沒有任何卵用?
自信一點,不要懷疑,確實沒有任何用處,signature 只是賦了個值而已,后續(xù)的代碼中并沒有使用。
所以,我小腦瓜子一轉(zhuǎn),立刻察覺到這又是一個水 pr 的好機會。
于是...
https://github.com/apache/dubbo/pull/13382
圖片
晚上 10 點半的時候,直接就是一個貢獻源碼的大動作,小手一揮,帶走四行代碼:
圖片
當時我沒細想,但是后來躺在床上的時候我突然想起來:不應(yīng)該啊,這個地方為什么會留著幾行看起來是沒有刪除不干凈的代碼呢?
隱隱覺得這里面應(yīng)該是有故事的。
于是看了這個類的提交記錄,主要找兩個地方:這個 signature 是什么時候有的,又是什么時候沒的。
在 2012 年 6 月 15 日,針對這個類做了一次性能優(yōu)化:
圖片
優(yōu)化的具體內(nèi)容就是用 Map 把方法緩存起來,以免每次都需要去走反射的邏輯。
圖片
圖片
看完這個提交之后我覺得很合理啊,使用 Map 緩存一下確實屬于性能優(yōu)化。
那么為什么又把這個 Map 拿走了呢?
于是我在 2021 年 9 月 6 日的提交中找到了拿走 Map 對應(yīng)的提交記錄:
圖片
圖片
這次提交的內(nèi)容非常的多,而從提交記錄的 log 中并沒有找到為什么要移除這個 Map 的原因:
圖片
怎么辦?
很簡單,社區(qū)提問就行了。
于是我在我的 pr 下面拋出了自己的問題:
圖片
我查看了該類的提交歷史,發(fā)現(xiàn) #8684 刪除了 ReflectUtils.java 中的所有 Map 緩存,遺留了對 signature 字段的處理。
但是我不明白為什么要刪除緩存,在我看來應(yīng)該保留緩存。能說一下官方是怎么考慮的嗎?
很快我就得到了官方的回復:
圖片
刪除緩存的原因是因為這些 Map 緩存是全局變量,這會導致從 Dubbo 的類(通常是 GC root)到對應(yīng)類的引用,而這些類在 ClassLoader 被閑置后無法釋放。
啥意思呢?
我大概的解釋一下。
首先,我們看一下這個 Map 的定義是怎么樣的:
private static final ConcurrentMap
它是個 static 對象,那么它是不是會被作為一個 GC root?
如果它作為一個 GC root,它里面緩存的這些方法,是不是都是“可達的”?
方法是可達的,那么這些方法對應(yīng)的 Class 類是不是也是“可達的”?
但是在這些方法對應(yīng)的 Class 類的 ClassLoader 完成自己的使命,被回收之后,那么這個 Class 類是不是理論上也可以被回收了?
但是實際情況是什么呢?
實際情況是因為這個 static 對象還持有其引用,導致它不會被回收。
基于這個考慮,官方?jīng)Q定移除這個 Map。
其實我個人覺得,如果我上面的理解沒有錯的話,那么討論這個 Map 的效果,可以得兩個分情況:
如果一個泛化調(diào)用的調(diào)用頻率非常低,那么你把對應(yīng)的方法緩存起來,導致 GC 一直回收不了,確實沒啥意思。
如果一個泛化調(diào)用的調(diào)用頻率比較高,那么你把對應(yīng)的方法緩存起來,確實能起到“性能優(yōu)化”的效果。
那么 Dubbo 作為一個框架怎么知道你的這個方法調(diào)用的頻率高不高呢?
它也不知道,所以干脆不要替用戶多做這一步,做多了,反而容易出錯。
其實它也是可以知道的,比如可以提供一個參數(shù)給用戶進行配置,把選擇權(quán)給到用戶,讓用戶通過配置來告訴你。甚至它可以不用用戶提供信息,可以自己來做數(shù)據(jù)收集,來評判這個方法是否應(yīng)該被緩存起來。
但是,這玩意收益也不高啊。
本來泛化調(diào)用就不是 RPC 調(diào)用里面非常核心的東西,在這上面搞這么多心思,投入產(chǎn)出比不高啊。
有這時間,還不如想想主鏈路上還有沒有什么地方可以優(yōu)化優(yōu)化,在主鏈路上干事情,才是收益最大的事情。
就像是你在公司里面,在邊緣部門里面干得再出色,也很少能讓人注意到。但是如果你在核心部門里面,做出一點稍微亮眼的成績,大家都能看到。
所以,你以為你敲的只是代碼嗎?
不是的,你敲的,是人情世故。
最后,這個 pr 也合并到源碼中去了,再次查看這個類的提交記錄,你會發(fā)現(xiàn)一個熟悉的名稱:
圖片
說真的,刪除這三行代碼沒有任何技術(shù)含量,這部分代碼讓任何一個有 Java 基礎(chǔ)的人來看,都會發(fā)現(xiàn)這個問題。
我不過是在調(diào)試源碼的過程中撿了個漏而已。
但是為什么這部分代碼存在了很久時間了,是我撿到了這個漏呢?
我想,大概是我真的搭了個 Demo 然后一行行的跟了一下源碼吧。
所以,朋友,別只是看,要動手,說不定有意外收獲。
好了,價值也上完了,本文的技術(shù)部分就到這里啦。
當前名稱:我試圖通過這篇文章告訴你,什么是神奇的泛化調(diào)用
當前地址:http://m.fisionsoft.com.cn/article/cdiijjc.html


咨詢
建站咨詢
