用隧道協議實現不同dubbo叢集間的透明通訊
前言
筆者最近完成了一個非常有意思的隧道機制(已在產線執行),可以讓註冊到不同zookeeper之間的dubbo叢集之間能夠正常進行通訊。如下圖所示:
例如圖中A/B兩個網路隔離的叢集,兩者只能通過專線進行通訊。但是對於在裡面的應用來說,呼叫另外一個叢集的dubbo服務(例如app1呼叫app3)依舊和原來的方式一模一樣,無需做任何修改。這個特性對於新建單元(機房),業務網路隔離等場景非常有用。
本文就稍稍聊一下這個機制。
場景
這個dubbo叢集通訊機制,可被用在下面的場景中。
新建機房
在我們新建一個機房的過程中。正常情況下,需要將一整條鏈路的所有應用以及相關設施全部部署到新的機房中。如下圖所示:
而在筆者新的機制中,如果本叢集沒有對應的介面,會去尋找有對應介面的叢集,就算其中缺失了一些系統,整個機房依舊能夠work,將新建機房變為可迭代式的。大幅度減少了新建機房的複雜性。
新建業務單元
由於單機房機架位的限制或者一些其它原因,有一些業務希望剝離到一個單獨的單元(機房裡面)。但是業務確需要一大堆原來單元的基礎服務。而不同單元之間的網路又無法打通(安全性要求)。
如果按照傳統的模式,勢必要對業務系統做改造,例如建立一個業務閘道器來負責和基礎系統的通訊,這個閘道器明顯費時費力而且沒什麼技術含量,例如在業務程式碼中將dubbo呼叫強行轉換為對業務閘道器的http呼叫,如下圖所示:
而且,每增加一個介面呼叫,都得在業務閘道器中轉換一把,新增對應的介面包,然後釋出。這樣的閘道器維護起來肯定是個天坑!隨著日益嚴格的安全性要求,不同業務間的網路隔離要求會與日俱增。
筆者是搞中介軟體的,堅信做的基礎服務能夠對業務透明,讓其感知不到才是一個好的設計。一旦需要業務大量配合這種由基礎架構變更而引起的改造,無疑是非常的不友好,甚至是個失敗的設計。
故障隔離
事實上,筆者搞這一套隧道機制的初衷還有很大一部分原因是故障隔離。例如,筆者遇到數次由於業務系統使用zookeeper不當,往zookeeper寫了一大堆資料,從而讓整個叢集陷入不可用的風險。而新的機制,可以讓不同的業務註冊到不同的zookeeper,zookeeper掛了,也只是這個業務宕了,其它業務則不受影響。
事實上不僅為zookeeper,由於筆者對訊息(例如activemq)也做了這一套類似的隧道機制。使得我們的整個業務能夠更好的進行故障隔離!
隧道機制
筆者這個機制的最大便利性在於對業務的侵入性很少。對於基礎叢集的應用甚至完全不需要做修改。為了達成這個需求,筆者引入了在網路上非常常用的隧道概念(Tunnel),這個大家可能平時都接觸過,VPN/Vxlan這些網路協議都用了隧道。
隧道穿透
我們先來看一下最基本的原理,在系統A通過Dubbo呼叫系統B的時候,在同一個叢集中走的是dubbo協議。而跨叢集的時候,筆者將dubbo原始位元流承載在http協議上,在專線上發出去。
由於在B系統看來,接收到的都是相同的byte流,其無法(也不用)區分到底是走了一層專線還是直接呼叫。所以B系統無需更改任何程式碼。
隧道實現
那麼,這個隧道具體是如何實現,系統A又是如何知道需要本叢集沒有對應的介面,需要通過http隧道呼叫到另一個叢集的呢?這就引入了我們的隧道閘道器。
這裡的概念也是和網路上的預設閘道器類似,如果本叢集內找不到對應的接受者就投遞到一個預設的閘道器,由這個隧道閘道器來替我們傳遞呼叫。
如何發現這個閘道器
為了充分利用dubbo介面的註冊發現機制,筆者將隧道閘道器也暴露為一個dubbo介面,其輸入和輸出分別如下所示:
// 隧道閘道器介面請求體
class TunnelInterfaceReq {
// dubbo元資訊,例如具體呼叫介面資訊
MetaData dubbo
// 原始請求A呼叫序列化後的位元流
byte[] body;
}
// 隧道閘道器介面返回體
class TunnelInterfaceResp{
// dubbo元資訊
MetaData dubbo
// 返回值呼叫序列化後的位元流,由另一個叢集的對應系統返回
byte[] resp;
}
有了這個dubbo介面,我們就可以很容易的將資料傳送給預設閘道器了。
注意,這裡其實也是做了一層隧道協議,即用dubbo協議承載dubbo協議,用這種類似套娃的方法有效的利用了dubbo本身的註冊發現機制。
閘道器和閘道器之間通過http通訊
由於不同叢集之間通過專線進行通訊,所以筆者採用了http通訊來進行。在App1的請求到達隧道閘道器後,閘道器會將原始body位元流從TunnelInterfaceRequest中取出。然後放到一個http的請求中進行傳遞。如下圖所示:
值得注意的是,由於傳遞的是byte流,沒有攜帶任何業務資訊(例如型別資訊等),所以我們的隧道閘道器可以對任意dubbo請求進行隧道傳輸,而不像傳統的閘道器那樣需要新增各種業務對應的jar包並不停釋出-_-!
在圖中,投遞到另一端的隧道閘道器後,其從http協議中取出呼叫元資訊和原始呼叫byte流,通過呼叫元資訊找到App2。然後給App2重放byte流,這樣就可以進行dubbo呼叫了。事實上,App2從隧道閘道器看到的byte流和從叢集內其它機器呼叫的byte流完全一致。如下圖所示:
返回值也通過隧道機制
很明顯的,我們的返回值也需要通過隧道機制。和Request一樣,其也會走兩次隧道協議,如下圖所示:
那麼App1真正接收到的其實是Tunnel Response,怎麼讓其透明的接收原始response位元流呢?這就需要呼叫方接入筆者研發的輕量級jar包(其實,一開始的request的隧道也需要這樣的jar包)
對dubbo進行擴充套件
由於dubbo有非常優秀的filter機制,可以在各種地方可以擴充套件。為了這個隧道機制,筆者就擴充套件了其中的invoke呼叫邏輯。如下圖所示:
只要引入筆者寫的jar包,就能夠非常輕鬆的進行自動擴充套件,除了pom.xml加兩行,其它業務程式碼完全無需修改。
隧道閘道器的介面發現
那麼隧道閘道器A是怎麼知道介面在叢集B,從而投遞給隧道閘道器B的呢?很明顯的,我們需要隧道閘道器間的叢集通訊機制。
例如,由隧道閘道器向其它不同的隧道閘道器詢問是否有此介面,並按一定策略做快取即可。
dubbo叢集的發現
最後的問題就是隧道閘道器怎麼知道其它的dubbo叢集的了,由於相對於dubbo介面數量,叢集的數量是很少且不經常改變。我們只需要找個地方簡單的記錄下即可,例如放到資料庫裡面。然後由於是http呼叫,直接通過DNS解析域名即可做負載均衡。
效能
由於筆者的這套機制序列化和反序列化完全在Provider/Consumer端,完全沒有對閘道器形成任何壓力,所以閘道器的CPU消耗很低。在單個呼叫延遲上,由於多了兩跳,不可避免的有所損耗,大概每個介面多了2ms左右。
總結
這套機制從一開始構想,到完全能夠在產線執行,並且效能損耗還很小,筆者還是花費了不少的精力。看到這樣的結果,還是非常有成就感的。事實上,這套隧道機制在非常多的地方借鑑了網路上的概念。可謂它山之石可以攻玉!不同技術之間確實可以相互遷移,他們只是在不同的層級上解決了本質相通的問題!
歡迎大家關注我公眾號,裡面有各種乾貨,還有大禮包相送哦!