09.30 攜程的 Dubbo 之路

本篇文章整理自董藝荃在 Dubbo 社區開發者日上海站的演講。

緣起

攜程當初為什麼要引入 Dubbo 呢?實際上從 2013 年底起,攜程內主要使用的就是基於 HTTP 協議的 SOA 微服務框架。這個框架是攜程內部自行研發的,整體架構在這近6年中沒有進行大的重構。受到當初設計的限制,框架本身的擴展性不是很好,使得用戶要想自己擴展一些功能就會比較困難。另外,由於 HTTP 協議一個連接同時只能處理一個請求。在高併發的情況下,服務端的連接數和線程池等資源都會比較緊張,影響到請求處理的性能。而 Dubbo 作為一個高性能的 RPC 框架,不僅是一款業界知名的開源產品,它整體優秀的架構設計和數據傳輸方式也可以解決上面提到的這些問題。正好在 2017 年下半年,阿里宣佈重啟維護 Dubbo 。基於這些原因,我們團隊決定把 Dubbo 引入攜程。

Dubbo 落地第一步

要在公司落地 Dubbo 這個新服務框架,第一步就是解決服務治理和監控這兩個問題。

服務治理

在服務治理這方面,攜程現有的 SOA 框架已經有了一套完整的服務註冊中心和服務治理系統。對於服務註冊中心,大家比較常用的可能是 Apache Zookeeper 。而我們使用的是參考 Netflix 開源的 Eureka 自行研發的註冊中心 Artemis 。Artemis 的架構是一個去中心的對等集群。各個節點的地位相同,沒有主從之分。服務實例與集群中的任意一個節點保持長連接,發送註冊和心跳信息。收到信息的節點會將這些信息分發給其他節點,確保集群間數據的一致性。客戶端也會通過一個長連接來接受註冊中心推送的服務實例列表信息。

攜程的 Dubbo 之路

在服務數據模型方面,我們直接複用了現有 SOA 服務的數據模型。如圖所示,最核心的服務模型對應的是 Dubbo 中的一個 interface 。一個應用程序內可以包含多個服務,一個服務也可以部署在多個服務器上。我們將每個服務器上運行的服務應用稱為服務實例。

攜程的 Dubbo 之路

所有的服務在上線前都需要在治理系統中進行註冊。註冊後,系統會為其分配一個唯一的標識,也就是 ServiceID 。這個 ServiceID 將會在服務實例註冊時發送至註冊中心用來標識實例的歸屬,客戶端也需要通過這個ID來獲取指定服務的實例列表。

攜程的 Dubbo 之路

由於 Dubbo 本身並沒有 ServiceID 的設計,這裡的問題就是如何向註冊中心傳遞一個 interface 所對應的 ServiceID 信息。我們的方法是在 Service 和 Reference 配置中增加一個 serviceId 參數。ArtemisServiceRegistry 的實現會讀取這個參數,並傳遞給註冊中心。這樣就可以正常的與註冊中心進行交互了。

攜程的 Dubbo 之路

服務監控

在服務監控這方面我們主要做了兩部分工作:統計數據層面的監控和調用鏈層面的監控。

統計數據指的是對各種服務調用數據的定期彙總,比如調用量、響應時間、請求體和響應體的大小以及請求出現異常的情況等等。這部分數據我們分別在客戶端和服務端以分鐘粒度進行了彙總,然後輸出到 Dashboard 看板上。同時我們也對這些數據增加了一些標籤,例如:Service ID、服務端 IP 、調用的方法等等。用戶可以很方便的查詢自己需要的監控數據。

攜程的 Dubbo 之路

在監控服務調用鏈上,我們使用的是 CAT 。CAT 是美團點評開源的一個實時的應用監控平臺。它通過樹形的 Transaction 和 Event 節點,可以將整個請求的處理過程記錄下來。我們在 Dubbo 的客戶端和服務端都增加了 CAT 的 Transaction 和 Event 埋點,記錄了調用的服務、 SDK 的版本、服務耗時、調用方的標識等信息,並且通過 Dubbo 的 Attachment 把 CAT 服務調用的上下文信息傳遞到了服務端,使得客戶端和服務端的監控數據可以連接起來。在排障的時候就可以很方便的進行查詢。在圖上,外面一層我們看到的是客戶端記錄的監控數據。在調用發起處展開後,我們就可以看到對應的在服務端的監控數據。

初版發佈

在解決了服務治理和監控對接這兩個問題後,我們就算完成了 Dubbo 在攜程初步的一個本地化,在 2018 年 3 月,我們發佈了 Dubbo 攜程定製版的首個可用版本。在正式發佈前我們需要給這個產品起個新名字。既然是攜程(Ctrip)加 Dubbo ,我們就把這個定製版本稱為 CDubbo 。

CDubbo 功能擴展

除了基本的系統對接,我們還對 CDubbo 進行了一系列的功能擴展,主要包括以下這 5 點: Callback 增強、序列化擴展、熔斷和請求測試工具。下面我來逐一給大家介紹一下。

Callback 增強

首先,我們看一下這段代碼。請問代碼裡有沒有什麼問題呢?

攜程的 Dubbo 之路

這段代碼裡有一個 DemoService 。其中的 callbackDemo 方法的參數是一個接口。下面的 Demo 類中分別在 foo 和 bar 兩個方法中調用了這個 callbackDemo 方法。相信用過 Callback 的朋友們應該知道,foo 這個方法的調用方式是正確的,而 bar 這個方法在重複調用的時候是會報錯的。因為對於同一個 Callback 接口,客戶端只能創建一個實例。

但這又有什麼問題呢?我們來看一下這樣一個場景。

攜程的 Dubbo 之路

一個用戶在頁面上發起了一個查詢機票的請求。站點服務器接收到請求之後調用了後端的查詢機票服務。考慮到這個調用可能會耗時較長,接口上使用了 callback 來回傳實際的查詢結果。然後再由站點服務器通過類似 WebSocket 的技術推送給客戶端。那麼問題來了。站點服務器接受到回調數據時需要知道它對應的是哪個用戶的哪次調用請求,這樣才能把數據正確的推送給用戶。但對於全局唯一的callback接口實例,想要拿到這個請求上下文信息就比較困難了。需要在接口定義和實現上預先做好準備。可能需要額外引入一些全局的對象來保存這部分上下文信息。

針對這個問題,我們在 CDubbo 中增加了 Stream 功能。跟前面一樣,我們先來看代碼。

攜程的 Dubbo 之路

這段代碼與前面的代碼有什麼區別?首先, callback 接口的參數替換為了一個 StreamContext 。還有接受回調的地方不是之前的全局唯一實例,而是一個匿名類,並且也不再是單單一個方法,而是有3個方法,onNext、onError和onCompleted 。這樣調用方在匿名類裡就可以通過閉包來獲取原本請求的上下文信息了。是不是體驗就好一些了?

那麼 Stream 具體是怎麼實現的呢?我們來看一下這張圖。

攜程的 Dubbo 之路

在客戶端,客戶端發起帶 Stream 的調用時,需要通過 StreamContext.create 方法創建一個StreamContext。雖然說是創建,但實際是在一個全局的 StreamContext 一個唯一的 StreamID 和對應回調的實際處理邏輯。在發送請求時,這個 StreamID 會被髮送到服務端。服務端在發起回調的時候也會帶上這個 StreamID 。這樣客戶端就可以知道這次回調對應的是哪個 StreamContext 了。

序列化擴展

攜程的一些業務部門,在之前開發 SOA 服務的時候,使用的是 Google Protocol Buffer 的契約編寫的請求數據模型。 Google PB 的要求就是通過契約生成的數據模型必須使用PB的序列化器進行序列化。為了便於他們將 SOA 服務遷移到Dubbo ,我們也在 Dubbo 中增加了 GooglePB 序列化方式的支持。後續為了便於用戶自行擴展,我們在PB序列化器的實現上增加了擴展接口,允許用戶在外圍繼續增加數據壓縮的功能。整體序列化器的實現並不是很難,倒是有一點需要注意的是,由於 Dubbo 服務對外只能暴露一種序列化方式,這種序列化方式應該兼容所有的 Java 數據類型。而 PB 碰巧就是那種只能序列化自己契約生成的數據類型的序列化器。所以在遇到不支持的數據類型的時候,我們還是會 fallback 到使用默認的 hessian 來進行序列化操作的。

請求熔斷

相信大家對熔斷應該不陌生吧。當客戶端或服務端出現大範圍的請求出錯或超時的時候,系統會自動執行 fail-fast 邏輯,不再繼續發送和接受請求,而是直接返回錯誤信息。這裡我們使用的是業界比較成熟的解決方案:Netflix 開源的 Hystrix 。它不僅包含熔斷的功能,還支持併發量控制、不同的調用間隔離等功能。單個調用的出錯不會對其他的調用造成影響。各項功能都支持按需進行自定義配置。CDubbo的服務端和客戶端通過集成 Hystrix 來做請求的異常情況進行處理,避免發生雪崩效應。

服務測試工具

Dubbo 作為一個使用二進制數據流進行傳輸的 RPC 協議,服務的測試就是一個比較難操作的問題。要想讓測試人員在無需編寫代碼的前提下測試一個 Dubbo 服務,我們要解決的有這樣三個問題:如何編寫測試請求、如何發送測試請求和如何查看響應數據。

首先就是怎麼構造請求。這個問題實際分為兩個部分。一個是用戶在不寫代碼的前提下用什麼格式去構造這個請求。考慮到很多測試人員對 Restful Service 的測試比較熟悉,所以我們最終決定使用 JSON 格式表示請求數據。那麼讓一個測試人員從一個空白的 JSON 開始構造一個請求是不是有點困難呢?所以我們還是希望能夠讓用戶瞭解到請求的數據模型。雖然我們使用的是 Dubbo 2.5.10 ,但這部分功能在 Dubbo 2.7.3 中已經有了。所以我們將這部分代碼複製了過來,然後對它進行了擴展,把服務的元數據信息保存在一個全局上下文中。並且我們在 CDubbo 中通過 Filter 增加了一個內部的操作,$serviceMeta,把服務的元數據信息暴露出來。這部分元數據信息包括方法列表、各個方法的參數列表和參數的數據模型等等。這樣用戶通過調用內部操作拿到這個數據模型之後,可以生成出一個基本的JSON結構。之後用戶只需要在這個結構中填充實際的測試數據就可以很容易的構造出一個測試請求來。

攜程的 Dubbo 之路

然後,怎麼把編輯好的請求發送給服務端呢?因為沒有模型代碼,無法直接發起調用。而 Dubbo 提供了一個很好的工具,就是泛化調用, GenericService 。我們把請求體通過泛化調用發送給服務端,再把服務端返回的Map序列化成JSON顯示給測試人員。整個測試流程就完成了。順便還解決了如何查看響應數據的問題。

攜程的 Dubbo 之路

為了方便用戶使用,我們開發了一個服務測試平臺。用戶可以在上面直接選擇服務和實例,編寫和發送測試請求。另外為了方便用戶進行自動化測試,我們也把這部分功能封裝成了 jar 包發佈了出去。

攜程的 Dubbo 之路

其實在做測試工具的過程中,還遇到了一點小問題。通過從 JSON 轉化 Map 再轉化為 POJO 這條路是能走通的。但前面提到了,有一些對象是通過類似 Google Protobuf 的契約生成的。它們不是單純的 POJO ,無法直接轉換。所以,我們對泛化調用進行了擴展。首先對於這種自定義的序列化器,我們允許用戶自行定義從數據對象到 JSON 的格式轉換實現。其次,在服務端處理泛化調用時,我們給 Dubbo 增加了進行 JSON 和 Google PB 對象之間的互相轉換的功能。現在這兩個擴展功能有已經合併入了 Dubbo 的代碼庫,並隨著 2.7.3 版本發佈了。

堡壘測試網關

說完了單純針對服務的測試,有些時候我們還希望在生產的實際使用環境下對服務進行測試,尤其是在應用發佈的時候。在攜程,有一個叫堡壘測試的測試方法,指的是在應用發佈過程中,發佈系統會先挑出一臺服務器作為堡壘機,並將新版本的應用發佈到堡壘機上。然後用戶通過特定的測試方法將請求發送到堡壘機上來驗證新版本應用的功能是否可以正常工作。由於進行堡壘測試時,堡壘機尚未拉入集群,這裡就需要讓客戶端可以識別出一個堡壘測試請求並把請求轉發給指定的堡壘服務實例。雖然我們可以通過路由來實現這一點,但這就需要客戶端了解很多轉發的細節信息,而且整合入 SDK 的功能對於後續的升級維護會造成一定的麻煩。所以我們開發了一個專門用於堡壘測試的服務網關。當一個客戶端識別到當前請求的上下文中包含堡壘請求標識時,它就會把 Dubbo 請求轉發給預先配置好的測試網關。網關會先解析這個服務請求,判斷它對應的是哪個服務然後再找出這個服務的堡壘機並將請求轉發過去。在服務完成請求處理後,網關也會把響應數據轉發回調用方。

攜程的 Dubbo 之路

與一般的 HTTP 網關不同, Dubbo 的服務網關需要考慮一個額外的請求方式,就是前面所提到的 callback 。由於 callback 是從服務端發起的請求,整個處理流程都與客戶端的正常請求不同。網關上會將客戶端發起的連接和網關與服務端之間的連接進行綁定,並記錄最近待返回的請求 ID 。這樣在接收到 callback 的請求和響應時就可以準確的路由了。

後續功能規劃

截止到今天, CDubbo 一共發佈了27個版本。攜程的很多業務部門都已經接入了 Dubbo 。在未來, CDubbo 還會擴展更多的功能,比如請求限流和認證授權等等。我們希望以後可以貢獻更多的新功能出來,回饋開源社區。

本文作者:董藝荃,攜程框架架構研發部技術專家。目前負責攜程服務化框架的研發工作。


分享到:


相關文章: