本篇文章整理自董藝荃在 Dubbo 社區開發者日上海站的演講。後端
攜程當初爲何要引入 Dubbo 呢?實際上從 2013 年末起,攜程內主要使用的就是基於 HTTP 協議的 SOA 微服務框架。這個框架是攜程內部自行研發的,總體架構在這近6年中沒有進行大的重構。受到當初設計的限制,框架自己的擴展性不是很好,使得用戶要想本身擴展一些功能就會比較困難。另外,因爲 HTTP 協議一個鏈接同時只能處理一個請求。在高併發的狀況下,服務端的鏈接數和線程池等資源都會比較緊張,影響到請求處理的性能。而 Dubbo 做爲一個高性能的 RPC 框架,不只是一款業界知名的開源產品,它總體優秀的架構設計和數據傳輸方式也能夠解決上面提到的這些問題。正好在 2017 年下半年,阿里宣佈重啓維護 Dubbo 。基於這些緣由,咱們團隊決定把 Dubbo 引入攜程。服務器
要在公司落地 Dubbo 這個新服務框架,第一步就是解決服務治理和監控這兩個問題。閉包
在服務治理這方面,攜程現有的 SOA 框架已經有了一套完整的服務註冊中心和服務治理系統。對於服務註冊中心,你們比較經常使用的多是 Apache Zookeeper 。而咱們使用的是參考 Netflix 開源的 Eureka 自行研發的註冊中心 Artemis 。Artemis 的架構是一個去中心的對等集羣。各個節點的地位相同,沒有主從之分。服務實例與集羣中的任意一個節點保持長鏈接,發送註冊和心跳信息。收到信息的節點會將這些信息分發給其餘節點,確保集羣間數據的一致性。客戶端也會經過一個長鏈接來接受註冊中心推送的服務實例列表信息。架構
在服務數據模型方面,咱們直接複用了現有 SOA 服務的數據模型。如圖所示,最核心的服務模型對應的是 Dubbo 中的一個 interface 。一個應用程序內能夠包含多個服務,一個服務也能夠部署在多個服務器上。咱們將每一個服務器上運行的服務應用稱爲服務實例。併發
全部的服務在上線前都須要在治理系統中進行註冊。註冊後,系統會爲其分配一個惟一的標識,也就是 ServiceID 。這個 ServiceID 將會在服務實例註冊時發送至註冊中心用來標識實例的歸屬,客戶端也須要經過這個ID來獲取指定服務的實例列表。框架
因爲 Dubbo 自己並無 ServiceID 的設計,這裏的問題就是如何向註冊中心傳遞一個 interface 所對應的 ServiceID 信息。咱們的方法是在 Service 和 Reference 配置中增長一個 serviceId 參數。ArtemisServiceRegistry 的實現會讀取這個參數,並傳遞給註冊中心。這樣就能夠正常的與註冊中心進行交互了。微服務
在服務監控這方面咱們主要作了兩部分工做:統計數據層面的監控和調用鏈層面的監控。高併發
統計數據指的是對各類服務調用數據的按期彙總,好比調用量、響應時間、請求體和響應體的大小以及請求出現異常的狀況等等。這部分數據咱們分別在客戶端和服務端以分鐘粒度進行了彙總,而後輸出到 Dashboard 看板上。同時咱們也對這些數據增長了一些標籤,例如:Service ID、服務端 IP 、調用的方法等等。用戶能夠很方便的查詢本身須要的監控數據。工具
在監控服務調用鏈上,咱們使用的是 CAT 。CAT 是美團點評開源的一個實時的應用監控平臺。它經過樹形的 Transaction 和 Event 節點,能夠將整個請求的處理過程記錄下來。咱們在 Dubbo 的客戶端和服務端都增長了 CAT 的 Transaction 和 Event 埋點,記錄了調用的服務、 SDK 的版本、服務耗時、調用方的標識等信息,而且經過 Dubbo 的 Attachment 把 CAT 服務調用的上下文信息傳遞到了服務端,使得客戶端和服務端的監控數據能夠鏈接起來。在排障的時候就能夠很方便的進行查詢。在圖上,外面一層咱們看到的是客戶端記錄的監控數據。在調用發起處展開後,咱們就能夠看到對應的在服務端的監控數據。性能
在解決了服務治理和監控對接這兩個問題後,咱們就算完成了 Dubbo 在攜程初步的一個本地化,在 2018 年 3 月,咱們發佈了 Dubbo 攜程定製版的首個可用版本。在正式發佈前咱們須要給這個產品起個新名字。既然是攜程(Ctrip)加 Dubbo ,咱們就把這個定製版本稱爲 CDubbo 。
除了基本的系統對接,咱們還對 CDubbo 進行了一系列的功能擴展,主要包括如下這 5 點: Callback 加強、序列化擴展、熔斷和請求測試工具。下面我來逐一給你們介紹一下。
首先,咱們看一下這段代碼。請問代碼裏有沒有什麼問題呢?
這段代碼裏有一個 DemoService 。其中的 callbackDemo 方法的參數是一個接口。下面的 Demo 類中分別在 foo 和 bar 兩個方法中調用了這個 callbackDemo 方法。相信用過 Callback 的朋友們應該知道,foo 這個方法的調用方式是正確的,而 bar 這個方法在重複調用的時候是會報錯的。由於對於同一個 Callback 接口,客戶端只能建立一個實例。
但這又有什麼問題呢?咱們來看一下這樣一個場景。
一個用戶在頁面上發起了一個查詢機票的請求。站點服務器接收到請求以後調用了後端的查詢機票服務。考慮到這個調用可能會耗時較長,接口上使用了 callback 來回傳實際的查詢結果。而後再由站點服務器經過相似 WebSocket 的技術推送給客戶端。那麼問題來了。站點服務器接受到回調數據時須要知道它對應的是哪一個用戶的哪次調用請求,這樣才能把數據正確的推送給用戶。但對於全局惟一的callback接口實例,想要拿到這個請求上下文信息就比較困難了。須要在接口定義和實現上預先作好準備。可能須要額外引入一些全局的對象來保存這部分上下文信息。
針對這個問題,咱們在 CDubbo 中增長了 Stream 功能。跟前面同樣,咱們先來看代碼。
這段代碼與前面的代碼有什麼區別?首先, callback 接口的參數替換爲了一個 StreamContext 。還有接受回調的地方不是以前的全局惟一實例,而是一個匿名類,而且也再也不是單單一個方法,而是有3個方法,onNext、onError和onCompleted 。這樣調用方在匿名類裏就能夠經過閉包來獲取本來請求的上下文信息了。是否是體驗就好一些了?
那麼 Stream 具體是怎麼實現的呢?咱們來看一下這張圖。
在客戶端,客戶端發起帶 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 提供了一個很好的工具,就是泛化調用, GenericService 。咱們把請求體經過泛化調用發送給服務端,再把服務端返回的Map序列化成JSON顯示給測試人員。整個測試流程就完成了。順便還解決了如何查看響應數據的問題。
爲了方便用戶使用,咱們開發了一個服務測試平臺。用戶能夠在上面直接選擇服務和實例,編寫和發送測試請求。另外爲了方便用戶進行自動化測試,咱們也把這部分功能封裝成了 jar 包發佈了出去。
其實在作測試工具的過程當中,還遇到了一點小問題。經過從 JSON 轉化 Map 再轉化爲 POJO 這條路是能走通的。但前面提到了,有一些對象是經過相似 Google Protobuf 的契約生成的。它們不是單純的 POJO ,沒法直接轉換。因此,咱們對泛化調用進行了擴展。首先對於這種自定義的序列化器,咱們容許用戶自行定義從數據對象到 JSON 的格式轉換實現。其次,在服務端處理泛化調用時,咱們給 Dubbo 增長了進行 JSON 和 Google PB 對象之間的互相轉換的功能。如今這兩個擴展功能有已經合併入了 Dubbo 的代碼庫,並隨着 2.7.3 版本發佈了。
說完了單純針對服務的測試,有些時候咱們還但願在生產的實際使用環境下對服務進行測試,尤爲是在應用發佈的時候。在攜程,有一個叫堡壘測試的測試方法,指的是在應用發佈過程當中,發佈系統會先挑出一臺服務器做爲堡壘機,並將新版本的應用發佈到堡壘機上。而後用戶經過特定的測試方法將請求發送到堡壘機上來驗證新版本應用的功能是否能夠正常工做。因爲進行堡壘測試時,堡壘機還沒有拉入集羣,這裏就須要讓客戶端能夠識別出一個堡壘測試請求並把請求轉發給指定的堡壘服務實例。雖然咱們能夠經過路由來實現這一點,但這就須要客戶端了解不少轉發的細節信息,並且整合入 SDK 的功能對於後續的升級維護會形成必定的麻煩。因此咱們開發了一個專門用於堡壘測試的服務網關。當一個客戶端識別到當前請求的上下文中包含堡壘請求標識時,它就會把 Dubbo 請求轉發給預先配置好的測試網關。網關會先解析這個服務請求,判斷它對應的是哪一個服務而後再找出這個服務的堡壘機並將請求轉發過去。在服務完成請求處理後,網關也會把響應數據轉發回調用方。
與通常的 HTTP 網關不一樣, Dubbo 的服務網關須要考慮一個額外的請求方式,就是前面所提到的 callback 。因爲 callback 是從服務端發起的請求,整個處理流程都與客戶端的正常請求不一樣。網關上會將客戶端發起的鏈接和網關與服務端之間的鏈接進行綁定,並記錄最近待返回的請求 ID 。這樣在接收到 callback 的請求和響應時就能夠準確的路由了。
截止到今天, CDubbo 一共發佈了27個版本。攜程的不少業務部門都已經接入了 Dubbo 。在將來, CDubbo 還會擴展更多的功能,好比請求限流和認證受權等等。咱們但願之後能夠貢獻更多的新功能出來,回饋開源社區。
本文做者:董藝荃,攜程框架架構研發部技術專家。目前負責攜程服務化框架的研發工做。
本文爲雲棲社區原創內容,未經容許不得轉載。