我試圖透過這篇文章告訴你,什麼是神奇的泛化呼叫。
來源:why技術
你好呀,我是歪歪。
關於 RPC 呼叫,大家肯定都是比較熟悉的了,就是在微服務架構下解決系統間通訊問題的一個玩意。
其中的典型代表之一就是 Dubbo 了:
在微服務架構下,我們針對某個 RPC 介面,我們一般有兩個角色。
服務消費者 (Dubbo Consumer),發起業務呼叫或 RPC 通訊的 Dubbo 程式 服務提供者 (Dubbo Provider),接收業務呼叫或 RPC 通訊的 Dubbo 程式
假設我是服務消費者,想要呼叫某個服務,只要我們連結到的是同一個服務註冊中心,那麼找對應服務要到 API 包對應的 Maven 座標,引入到專案中,就類似於這樣的東西:
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-demo-interface</artifactId>
<version>${project.parent.version}</version>
</dependency>
那麼對於這個 API 包中的介面,雖然我們沒有具體的實現類,但是我們還是能像呼叫本地方法一樣呼叫該服務提供的介面。
這些都是常規的東西了,你肯定是門清。
那我現在問你一個問題啊:
我是服務消費者,我要呼叫一個服務提供者的 RPC 介面,但是我又不想引入它的 API 包,或者我根本就拉取不到它的 API 包,那麼我應該怎麼辦?
如果你要非給我說:這不可能,既然是要消費別人的介面,那麼肯定要拿到 API 包才對,你不拿就是你偷懶。
那我再給你舉個歪師傅在實際開發過程中遇到的具體的例子:閘道器服務。
閘道器是個什麼玩意?
是你對外請求的統一入口,做接受請求、分發請求用的,作為連結各個微服務的角色,你勢必要使用到下游的若干個 RPC 服務。
你怎麼辦?
引入所有的服務提供方的 API 包,然後發起呼叫嗎?
可以是可以,但是不夠優雅。
你想,如果有一個服務提供方釋出了新的 API 包,你也需要更新版本,重新發版?
或者新來一個服務提供者 E,你需要引入其 API 包,然後重新發版?
閘道器應該是一個穩定的基礎服務,它提供的是聚攏 API 介面、轉發呼叫的基礎功能,不應該頻繁發版,不應該主動去關注下游的服務介面變化。平臺本身不應該依賴於服務提供方的介面 API。
不主動,才能更加優雅,也能讓自己更加輕鬆。
那麼怎麼才能做到不主動關注呢?
這個事情,總有一方要主動的,所以閘道器層不主動,那麼服務提供者就需要主動起來。
我們可以搞成這樣:
閘道器層提供一個 API 介面釋出平臺,當服務提供者的介面有新增或者發生變化的時候,由對應系統的介面管理人員把介面資訊,比如介面路徑、方法、入參、出參、方法功能說明、方法負責團隊、介面對接人等等這些訊息維護到 API 介面釋出平臺上。
這樣閘道器層就可以從 API 介面釋出平臺獲取到所有服務的所有介面,並不需要引入任何服務提供者的 API 包。
這樣就解決了“主動”的問題,如果介面有變化,請在 API 介面釋出平臺進行登記,從而解決了閘道器頻繁釋出的問題。
在官網上,除了閘道器的場景外,還提到一個測試平臺的場景,道理是一樣的,我就不贅述了:
解決了“主動”的問題,那麼下一個問題就隨之而來了:知道所有服務的所有介面然後呢,怎麼發起呼叫呢?
這個時候泛化呼叫,啪的一下就站出來了:鋪墊了這麼多,終於該老子上場了。
泛化呼叫
啥是泛化呼叫呢?
在 Dubbo 官網上是這樣介紹的:
首先需要強調的是“泛化呼叫”不是 Dubbo 特有的,它是一個功能,很多的框架都支援泛化呼叫,只是我這裡用的 Dubbo 做演示而已。
老規矩,先花五分鐘時間搭個 Demo 出來再說。
這個 Demo 我也是跟著網上的 quick start 搞的:
可以說寫的非常詳細了,你就跟著官網的步驟一步步的搞就行了。
我這個 Demo 稍微不一樣的是我在消費者模組裡面搞了一個 Http 介面:
在介面裡面發起了 RPC 呼叫,模擬從前端頁面發起請求的場景,更加符合我們的開發習慣。
為了起到強調作用,我再次把這個部分給你框起來:
DemoService 是 RPC 介面,它的實現類是這樣的:
在我的消費者模組裡面為什麼能注入這個 DemoService 並呼叫它的 sayHello 方法呢?
因為我引入了對應的依賴包。
那麼,如果我把這個依賴包去掉,也就是模擬我們前面說的“不主動”的動作,這個 DemoService 肯定會報錯,找不到這個類:
那麼我們應該怎麼去修改一下這個 Demo,讓它泛化起來呢?
非常簡單:
注入 DemoService 修改為注入 GenericService。
有的小夥伴可能會問 GenericService 是怎麼冒出來的?
你先別管它是怎麼冒出來的,我現在是在給你鋪墊 Demo,後面要撕給你看。你現在只需要知道它是 Dubbo 框架裡面的包,並不會讓你引用額外的包就行了:
現在 Demo 就算是搭好了,本地啟動一個 zk,然後把服務提供者啟動起來,再把消費者啟動起來,最後輕輕的發起一個呼叫:
朋友,它不就跑起來了嗎?
我沒有引用介面的 api 包,我不也正常發起了呼叫,然後拿到了返回值嗎?
啥原理
你就想,遠端呼叫,你把一些花裡胡哨的東西都拿掉之後,它的本質是什麼?
本質就是幫助解決微服務元件之間的通訊問題,不管是基於 HTTP、HTTP/2、TCP 還是什麼其他的通訊協議,解決的是網路連線管理、資料傳輸等基礎問題。
雖然我沒有引用 API 的對應的包,但是我前面我不是說了嗎,我們有一個 API 介面釋出平臺,這個平臺裡面有介面維護人員提供的介面路徑、方法、入參、出參這些關鍵資訊。
所以我在呼叫的時候可以拿到相關的資訊,以一種通用的方式,比如字串的方式告訴 RPC 框架,我要呼叫的是 DemoService 介面的 sayHello 方法,入參是 String 型別的 world 字串:
如果是你來開發一個 RPC 框架,呼叫方都把這些關鍵資訊給你了,無非就是你幫忙多做幾步類似於反射、序列化之類的處理。而處理的這個過程,就是泛化呼叫的過程。
泛化呼叫不是 Dubbo 特有的,但是具體到 Dubbo 這個框架裡面,具體是這樣的。
首先,Dubbo 裡面有一層 Filter,這些 Filter 構成了一個 Filter 鏈條:
Filter 用來對每次服務呼叫做一些預處理、後處理動作,使用 Filter 可以完成訪問日誌、加解密、流量統計、引數驗證等任務。
一次請求過程中可以植入多個 Filter,Filter 之間相互獨立沒有依賴。
從消費端視角,它在請求發起前基於請求引數等做一些預處理工作,在接收到響應後,對響應結果做一些後置處理。
從提供者視角,在接收到訪問請求後,在返回響應結果前做一些預處理。
所以我們的泛化呼叫,也是透過下面這兩個 Filter 來搞事情的:
org.apache.dubbo.rpc.filter.GenericFilter org.apache.dubbo.rpc.filter.GenericImplFilter
那麼問題就來了?
為什麼要兩個 Filter 呢?
因為要完成一次泛化呼叫,消費端和服務提供者都需要感知到並做相關的處理,所以一個是消費端的 Fliter,一個是服務提供者的 Fliter:
知道了對應的 Filter,關於泛化呼叫的所有秘密都藏在 Filter 對應的原始碼裡面。
歪師傅帶著你簡單的看一眼。
GenericImplFilter.invoke
首先,我們在方法的消費者對應的 Fliter 的入口處打上斷點:
org.apache.dubbo.rpc.filter.GenericImplFilter#invoke
可以看到分為了三個模組。
isCallingGenericImpl:calling a generic impl service,判斷是否呼叫的是一個實現了泛化介面的介面。 isMakingGenericCall:making a generic call to a normal service,把泛化呼叫轉換為一個常規呼叫。 invoker.invoke(invocation):常規呼叫。
我們研究的情況屬於 isMakingGenericCall 這個分支。
既然是要把泛化呼叫轉換為一個常規呼叫,那麼 Dubbo 是怎麼判斷這是一個泛化呼叫的呢?
org.apache.dubbo.rpc.filter.GenericImplFilter#isMakingGenericCall
判斷本次呼叫的方法名稱是否是 invokeAsync 判斷本次呼叫的入參個數是否是 3 個 判斷容器上下文中的 generic 引數是否對應著泛化呼叫的序列化方法。
我們一個個的看。
invokeAsync 方法是 GenericService 這個介面裡面的方法。而這兩個方法的入參個數都是三個。
然後有個 generic 引數,在我的 Demo 裡面這個引數是 true:
當我啪的一下跟進到 isGeneric 方法中,才發現這裡面別有洞天:
原來 generic 這個引數不只是可以為 “true”,它不同的值,代表著不同的序列化方式。
透過這部分原始碼可以看出來,泛化呼叫對於客戶端,即在 GenericImplFilter 裡面,並沒有做什麼特別的操作,注意還是引數校驗。
如果入參和對應的序列化方法不能匹配起來,即使的丟擲異常,這樣符合 Dubbo 框架的 fast-fail 思想。
但是其實看到這裡的時候,我有一個小疑問,如果我寫一個這樣的類:
public interface WhyService {
Object $invoke(String a,String b,String c);
}
和 GenericService 類一樣,有 $invoke 方法,而且也是三個引數。
然後在上下文中塞個 generic=true 進去,那麼是不是也能騙過這段程式碼呢,也能進入到 isMakingGenericCall 方法裡面呢?
從程式碼上看確實是這樣的,那麼 Dubbo 到底是怎麼規避這些“惡意”冒充者的呢?
我也不知道。
先存個疑吧,接著往下看。
GenericFilter.invoke
我們同樣在服務端打上斷點,當這個請求來到服務端的時候,我們再看看服務端的情況。
org.apache.dubbo.rpc.filter.GenericFilter#invoke
可以看到這個方法邏輯都在 if 判斷為 true 的時候。
而這個判斷我們剛剛在客戶端已經解析過了,只是多了一個判斷:
!GenericService.class.isAssignableFrom(invoker.getInterface())
看看發起呼叫的介面類是不是 GenericService 類的子類,如果是,則進入到 if 分支裡面。
朋友,這就有點意思了。幾秒鐘之前我們還在存疑,然後啪的一下疑問就解開了。
直接就是恍然大悟了。
我這個類:
public interface WhyService {
Object $invoke(String a,String b,String c);
}
過不了服務提供者的 GenericFilter 裡面的這個判斷:
!GenericService.class.isAssignableFrom(invoker.getInterface())
在 invoke 方法裡面,可以看到經過了一個 findMethodByMethodSignature 方法,獲取了我們想要呼叫的 method 方法:
這個方法,從名字上也可以看出,是根據方法簽名反射出具體的方法:
在服務端,是有 DemoService 介面對應的類的,所以可以透過反射找到它。
然後再解析出入參的具體值:
這樣你就有了構建一個 RpcInvocation 物件,即發起 RPC 呼叫的物件的所有關鍵訊息。
直接就是發動一招“狸貓換太子”的大動作,重新構建一個 RpcInvocation 物件,然後自己發起一個 invoke 呼叫。
這樣整體看起來似乎一次泛化呼叫也是很簡單的,當你去看服務提供端的原始碼的時候,你會發現這裡面的原始碼特別多。
不過是因為 Dubbo 支援了多種不同的序列化方式而已,本質是一樣的:
onResponse 方法也是同理,就不贅述了:
org.apache.dubbo.rpc.filter.GenericFilter#onResponse
到這裡就算是扯下了泛化呼叫的神秘面紗,和我們預想的一樣,無非是拿到介面呼叫的關鍵資訊之後,重新構建一個請求而已,整體邏輯並不複雜。
複雜的邏輯是什麼?
我演示的是最簡單的,入參是一個 String 型別的情況。如果我是一個複雜物件呢,物件裡面的成員變數特別多,物件裡面套物件,物件裡面有 List 或者 Map 的情況呢?
複雜的地方在於怎麼處理這些複雜物件,把複雜物件搞成服務提供者的 Java 物件入參。
我這裡只是一個導讀而已,如果你對這部分有興趣的話,自己搞個複雜物件去研究研究吧,老有意思了。
就當是家庭作業了。
意外收穫
歪師傅在扯麵紗的時候,沒想到還有意外收穫。
給你看一段程式碼,也是前面出現過的一個方法,我把完整的程式碼都截圖放出來:
org.apache.dubbo.common.utils.ReflectUtils#findMethodByMethodSignature
你瞅瞅我框起來部分的 signature 欄位,是不是沒有任何卵用?
自信一點,不要懷疑,確實沒有任何用處,signature 只是賦了個值而已,後續的程式碼中並沒有使用。
所以,我小腦瓜子一轉,立刻察覺到這又是一個水 pr 的好機會。
於是...
晚上 10 點半的時候,直接就是一個貢獻原始碼的大動作,小手一揮,帶走四行程式碼:
當時我沒細想,但是後來躺在床上的時候我突然想起來:不應該啊,這個地方為什麼會留著幾行看起來是沒有刪除不乾淨的程式碼呢?
隱隱覺得這裡面應該是有故事的。
於是看了這個類的提交記錄,主要找兩個地方:這個 signature 是什麼時候有的,又是什麼時候沒的。
在 2012 年 6 月 15 日,針對這個類做了一次效能最佳化:
最佳化的具體內容就是用 Map 把方法快取起來,以免每次都需要去走反射的邏輯。
看完這個提交之後我覺得很合理啊,使用 Map 快取一下確實屬於效能最佳化。
那麼為什麼又把這個 Map 拿走了呢?
於是我在 2021 年 9 月 6 日的提交中找到了拿走 Map 對應的提交記錄:
這次提交的內容非常的多,而從提交記錄的 log 中並沒有找到為什麼要移除這個 Map 的原因:
怎麼辦?
很簡單,社群提問就行了。
於是我在我的 pr 下面丟擲了自己的問題:
我檢視了該類的提交歷史,發現 #8684 刪除了 ReflectUtils.java 中的所有 Map 快取,遺留了對 signature 欄位的處理。
但是我不明白為什麼要刪除快取,在我看來應該保留快取。能說一下官方是怎麼考慮的嗎?
很快我就得到了官方的回覆:
刪除快取的原因是因為這些 Map 快取是全域性變數,這會導致從 Dubbo 的類(通常是 GC root)到對應類的引用,而這些類在 ClassLoader 被閒置後無法釋放。
啥意思呢?
我大概的解釋一下。
首先,我們看一下這個 Map 的定義是怎麼樣的:
private static final ConcurrentMap<String, Method> SIGNATURE_METHODS_CACHE = new ConcurrentHashMap<String, Method>();
它是個 static 物件,那麼它是不是會被作為一個 GC root?
如果它作為一個 GC root,它裡面快取的這些方法,是不是都是“可達的”?
方法是可達的,那麼這些方法對應的 Class 類是不是也是“可達的”?
但是在這些方法對應的 Class 類的 ClassLoader 完成自己的使命,被回收之後,那麼這個 Class 類是不是理論上也可以被回收了?
但是實際情況是什麼呢?
實際情況是因為這個 static 物件還持有其引用,導致它不會被回收。
基於這個考慮,官方決定移除這個 Map。
其實我個人覺得,如果我上面的理解沒有錯的話,那麼討論這個 Map 的效果,可以得兩個分情況:
如果一個泛化呼叫的呼叫頻率非常低,那麼你把對應的方法快取起來,導致 GC 一直回收不了,確實沒啥意思。
如果一個泛化呼叫的呼叫頻率比較高,那麼你把對應的方法快取起來,確實能起到“效能最佳化”的效果。
那麼 Dubbo 作為一個框架怎麼知道你的這個方法呼叫的頻率高不高呢?
它也不知道,所以乾脆不要替使用者多做這一步,做多了,反而容易出錯。
其實它也是可以知道的,比如可以提供一個引數給使用者進行配置,把選擇權給到使用者,讓使用者透過配置來告訴你。甚至它可以不用使用者提供資訊,可以自己來做資料收集,來評判這個方法是否應該被快取起來。
但是,這玩意收益也不高啊。
本來泛化呼叫就不是 RPC 呼叫裡面非常核心的東西,在這上面搞這麼多心思,投入產出比不高啊。
有這時間,還不如想想主鏈路上還有沒有什麼地方可以最佳化最佳化,在主鏈路上幹事情,才是收益最大的事情。
就像是你在公司裡面,在邊緣部門裡面幹得再出色,也很少能讓人注意到。但是如果你在核心部門裡面,做出一點稍微亮眼的成績,大家都能看到。
所以,你以為你敲的只是程式碼嗎?
不是的,你敲的,是人情世故。
最後,這個 pr 也合併到原始碼中去了,再次檢視這個類的提交記錄,你會發現一個熟悉的名稱:
說真的,刪除這三行程式碼沒有任何技術含量,這部分程式碼讓任何一個有 Java 基礎的人來看,都會發現這個問題。
我不過是在除錯原始碼的過程中撿了個漏而已。
但是為什麼這部分程式碼存在了很久時間了,是我撿到了這個漏呢?
我想,大概是我真的搭了個 Demo 然後一行行的跟了一下原始碼吧。
所以,朋友,別隻是看,要動手,說不定有意外收穫。
好了,價值也上完了,本文的技術部分就到這裡啦。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024923/viewspace-2997567/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 我試圖透過這篇文章告訴你,這行原始碼有多牛逼。原始碼
- 面試中圖論都考什麼?這篇文章告訴你!面試圖論
- 為什麼要學習自動化測試?這篇文章告訴你答案
- 什麼是雲資料庫?這篇文章詳細告訴你!資料庫
- 我試圖透過這篇文章,教會你一種閱讀原始碼的方式。原始碼
- 一張圖告訴你什麼是GraphQL?
- MQTT 協議是個啥?這篇文章告訴你!MQQT協議
- 告訴你什麼是TestOps測試運維運維
- Java,你告訴我 fail-fast 是什麼鬼?JavaAIAST
- 使用 MacBook Pro 時如何防止過熱,這篇文章告訴你Mac
- "instanceof 的原理是什麼"?大聲告訴面試官,我知道!面試
- 告訴你什麼是Pixelmator Pro for Mac!Mac
- 如何看懂DOE分析報告?這篇文章告訴你
- 圖解|12張圖告訴你MySQL的主鍵查詢為什麼這麼快圖解MySql
- 一篇告訴你什麼是SpringSpring
- 【轉】kafka-告訴你什麼是kafkaKafka
- BI系統要自研還是採購?這篇文章告訴你
- 怎麼GET大牛的框架思維?透過EventBus框架告訴你框架
- 爆肝30小時後,我想告訴你《原神》是什麼
- 用大白話告訴你什麼是Event LoopOOP
- 一座島告訴你,什麼是智慧!
- 用最簡單的話告訴你什麼是ElasticSearchElasticsearch
- 如何實施標準作業?這篇文章告訴你
- 什麼是智慧數字經營系統?這三點告訴你答案
- 企業使用CRM系統的前景如何?這篇文章告訴你
- 機器學習中特徵選擇怎麼做?這篇文章告訴你機器學習特徵
- 用大白話告訴你,Java到底是什麼Java
- 告訴你MySQL主鍵查詢為什麼這麼快MySql
- 馬爾可夫鏈是個什麼鬼?圖文詳解告訴你!馬爾可夫
- 看完這篇文章你就可以告訴領導你精通Zookeeper了
- 【重點】圖解:告訴面試官什麼是 JS 原型和原型鏈?圖解面試JS原型
- 一文告訴你什麼是NAT,為什麼需要NAT
- 如何對前端圖片主題色進行提取?這篇文章詳細告訴你前端
- 關於Flutter初始化,我必須告訴你的是...(乾貨)Flutter
- BI小白不能錯過|一文告訴你什麼是商務智慧
- 我來告訴你程式碼重構有什麼好處
- 讓我來告訴你為什麼做女程式媛很好
- 一文告訴你大資料是什麼大資料