Dubbo 3 走向應用級服務發現

一、服務發現(Service Discovery) 概述

從 Internet 剛開始興起,如何動態感知後端服務的地址變化就是一個必須要面對的問題,爲此人們定義了 DNS 協議,基於此協議,調用方只需要記住由固定字符串組成的域名,就能輕鬆完成對後端服務的訪問,而不用擔心流量最終會訪問到哪些機器 IP,因爲有代理組件會基於 DNS 地址解析後的地址列表,將流量透明的、均勻的分發到不同的後端機器上。

在使用微服務構建複雜的分佈式系統時,如何感知 backend 服務實例的動態上下線,也是微服務框架最需要關心並解決的問題之一。業界將這個問題稱之爲 -  微服務的地址發現(Service Discovery),業界比較有代表性的微服務框架如 SpringCloud、Microservices、Dubbo 等都抽象了強大的動態地址發現能力,並且爲了滿足微服務業務場景的需求,絕大多數框架的地址發現都是基於自己設計的一套機制來實現,因此在能力、靈活性上都要比傳統 DNS 豐富得多。如 SpringCloud 中常用的 Eureka, Dubbo 中常用的 Zookeeper、Nacos 等,這些註冊中心實現不止能夠傳遞地址(IP + Port),還包括一些微服務的 Metadata 信息,如實例序列化類型、實例方法列表、各個方法級的定製化配置等。

下圖是微服務中 Service Discovery 的基本工作原理圖,微服務體系中的實例大概可分爲三種角色:服務提供者(Provider)、服務消費者(Consumer)和註冊中心(Registry)。而不同框架實現間最主要的區別就體現在註冊中心數據的組織:地址如何組織、以什麼粒度組織、除地址外還同步哪些數據?

我們今天這篇文章就是圍繞這三個角色展開,重點看下 Dubbo 中對於服務發現方案的設計,包括之前老的服務發現方案的優勢和缺點,以及 Dubbo 3.0 中正在設計、開發中的全新的面向應用粒度的地址發現方案,我們期待這個新的方案能做到:

  • 支持幾十萬/上百萬級集羣實例的地址發現

  • 與不同的微服務體系(如 Spring Cloud)實現在地址發現層面的互通

二、Dubbo 地址發現機制解析

我們先以一個 DEMO 應用爲例,來快速的看一下 Dubbo “接口粒度”服務發現與“應用粒度”服務發現體現出來的區別。這裏我們重點關注 Provider 實例是如何向註冊中心註冊的,並且,爲了體現註冊中心數據量變化,我們觀察的是兩個 Provider 實例的場景。

應用 DEMO 提供的服務列表如下:

<dubbo:service interface="org.apache.dubbo.samples.basic.api.DemoService" ref="demoService"/>

<dubbo:service interface="org.apache.dubbo.samples.basic.api.GreetingService" ref="greetingService"/>

我們示例註冊中心實現採用的是 Zookeeper ,啓動 192.168.0.103 和 192.168.0.104 兩個實例後,以下是兩種模式下注冊中心的實際數據。

2.1 “接口粒度” 服務發現

192.168.0.103  實例註冊的數據:

dubbo://192.168.0.103:20880/org.apache.dubbo.samples.basic.api.DemoService?anyhost=true&application=demo-provider&default=true&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.samples.basic.api.DemoService&methods=testVoid,sayHello&pid=995&release=2.7.7&side=provider×tamp=1596988171266

dubbo://192.168.0.103:20880/org.apache.dubbo.samples.basic.api.GreetingService?anyhost=true&application=demo-provider&default=true&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.samples.basic.api.GreetingService&methods=greeting&pid=995&release=2.7.7&side=provider×tamp=1596988170816

192.168.0.104  實例註冊的數據:

dubbo://192.168.0.104:20880/org.apache.dubbo.samples.basic.api.DemoService?anyhost=true&application=demo-provider&default=true&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.samples.basic.api.DemoService&methods=testVoid,sayHello&pid=995&release=2.7.7&side=provider×tamp=1596988171266

dubbo://192.168.0.104:20880/org.apache.dubbo.samples.basic.api.GreetingService?anyhost=true&application=demo-provider&default=true&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.samples.basic.api.GreetingService&methods=greeting&pid=995&release=2.7.7&side=provider×tamp=1596988170816

2.2 “應用粒度” 服務發現

192.168.0.103 實例數據:

{

    "name": "demo-provider",

    "id": "192.168.0.103:20880",

    "address": "192.168.0.103",

    "port": 20880,

  "metadata": {

    "dubbo.endpoints": "[{\"port\":20880,\"protocol\":\"dubbo\"}]",

    "dubbo.metadata.storage-type": "local",

    "dubbo.revision": "6785535733750099598"

  },

    "time": 1583461240877

}

192.168.0.104  實例數據:

{

"name": "demo-provider",

"id": "192.168.0.104:20880",

"address": "192.168.0.104",

"port": 20880,

"metadata": {

"dubbo.endpoints": "[{"port":20880,"protocol":"dubbo"}]",

"dubbo.metadata.storage-type": "local",

"dubbo.revision": "7829635812370099387"

},

"time": 1583461240947

}

對比以上兩種不同粒度的服務發現模式,從 “接口粒度” 升級到 “應用粒度” 後我們可以總結出最大的區別是:註冊中心數據量不再與接口數成正比,不論應用提供有多少接口,註冊中心只有一條實例數據。

那麼接下來我們詳細看下這個變化給 Dubbo 帶來了哪些好處。

三、Dubbo 應用級服務發現的意義

我們先說結論,應用級服務發現給 Dubbo 帶來以下優勢:

  • 與業界主流微服務模型對齊,比如 SpringCloud、Kubernetes Native Service 等

  • 提升性能與可伸縮性。註冊中心數據的重新組織(減少),能最大幅度的減輕註冊中心的存儲、推送壓力,進而減少 Dubbo Consumer 側的地址計算壓力;集羣規模也開始變得可預測、可評估(與 RPC 接口數量無關,只與實例部署規模相關)

3.1 對齊主流微服務模型

自動、透明的實例地址發現(負載均衡)是所有微服務框架需要解決的事情,這能讓後端的部署結構對上游微服務透明,上游服務只需要從收到的地址列表中選取一個,發起調用就可以了。要實現以上目標,涉及兩個關鍵點的自動同步:

  • 實例地址,服務消費方需要知道地址以建立鏈接

  • RPC 方法定義,服務消費方需要知道 RPC 服務的具體定義,不論服務類型是 rest 或 rmi 等

對於 RPC 實例間藉助註冊中心的數據同步,REST 定義了一套非常有意思的成熟度模型,感興趣的朋友可以參考這裏:點擊查看, 按照文章中的 4 級成熟度定義,Dubbo 當前基於接口粒度的模型可以對應到 L4 級別。

接下來,我們看看 Dubbo、SpringCloud 以及 Kubernetes 分別是怎麼圍繞自動化的實例地址發現這個目標設計的。

a) Spring Cloud

Spring Cloud 通過註冊中心只同步了應用與實例地址,消費方可以基於實例地址與服務提供方建立鏈接,但是消費方對於如何發起 http 調用(SpringCloud 基於 rest 通信)一無所知,比如對方有哪些 http endpoint,需要傳入哪些參數等。

RPC 服務這部分信息目前都是通過線下約定或離線的管理系統來協商的。這種架構的優缺點總結如下。

優勢:部署結構清晰、地址推送量小。

缺點:地址訂閱需要指定應用名, provider 應用變更(拆分)需消費端感知;RPC 調用無法全自動同步。

b) Dubbo

Dubbo 通過註冊中心同時同步了實例地址和 RPC 方法,因此其能實現 RPC 過程的自動同步,面向 RPC 編程、面向 RPC 治理,對後端應用的拆分消費端無感知,其缺點則是地址推送數量變大,和 RPC 方法成正比。

c) Dubbo + Kubernetes

Dubbo 要支持 Kubernetes native service,相比之前自建註冊中心的服務發現體系來說,在工作機制上主要有兩點變化:

  • 服務註冊由平臺接管,provider 不再需要關心服務註冊

  • consumer 端服務發現將是 Dubbo 關注的重點,通過對接平臺層的 API-Server、DNS 等,Dubbo client 可以通過一個 Service Name(通常對應到 Application Name)查詢到一組 Endpoints(一組運行 provider 的 pod),通過將 Endpoints 映射到 Dubbo 內部地址列表,以驅動 Dubbo 內置的負載均衡機制工作

Kubernetes Service 作爲一個抽象概念,怎麼映射到 Dubbo 是一個值得討論的點:

  • Service Name - > Application Name,Dubbo 應用和 Kubernetes 服務一一對應,對於微服務運維和建設環節透明,與開發階段解耦

apiVersion: v1

kind: Service

metadata:

  
name: provider-app-namespec:

  
selector:

    
app: provider-app-name

  
ports:

    
- protocol: TCP

      
port:

      
targetPort: 9376
  • Service Name - > Dubbo RPC Service,Kubernetes 要維護調度的服務與應用內建 RPC 服務綁定,維護的服務數量變多

---

apiVersion: v1

kind: Service

metadata:

  name: rpc-service-1

spec:

  selector:

    app: provider-app-name

  ports: ##

...

---

apiVersion: v1

kind: Service

metadata:

  name: rpc-service-2

spec:

  selector:

    app: provider-app-name

  ports: ##

...

---

apiVersion: v1

kind: Service

metadata:

  name: rpc-service-N

spec:

  selector:

    app: provider-app-name

  ports: ##

...

結合以上幾種不同微服務框架模型的分析,我們可以發現,Dubbo 與 SpringCloud、Kubernetes 等不同產品在微服務的抽象定義上還是存在很大不同的。SpringCloud 和 Kubernetes 在微服務的模型抽象上還是比較接近的,兩者基本都只關心實例地址的同步,如果我們去關心其他的一些服務框架產品,會發現它們絕大多數也是這麼設計的,即 REST 成熟度模型中的 L3 級別。

對比起來 Dubbo 則相對是比較特殊的存在,更多的是從 RPC 服務的粒度去設計的。對應 REST 成熟度模型中的 L4 級別。

如我們上面針對每種模型做了詳細的分析,每種模型都有其優勢和不足。而我們最初決定 Dubbo 要做出改變,往其他的微服務發現模型上的對齊,是我們最早在確定  Dubbo 的雲原生方案時,我們發現要讓 Dubbo 去支持 Kubernetes Native Service,模型對齊是一個基礎條件;另一點是來自用戶側對 Dubbo 場景化的一些工程實踐的需求,得益於 Dubbo 對多註冊、多協議能力的支持,使得 Dubbo 聯通不同的微服務體系成爲可能,而服務發現模型的不一致成爲其中的一個障礙,這部分的場景描述請參見以下文章:點擊查看。

3.2 更大規模的微服務集羣 - 解決性能瓶頸

這部分涉及到和註冊中心、配置中心的交互,關於不同模型下注冊中心數據的變化,之前原理部分我們簡單分析過。爲更直觀的對比服務模型變更帶來的推送效率提升,我們來通過一個示例看一下不同模型註冊中心的對比:

圖中左邊是微服務框架的一個典型工作流程,Provider 和  Consumer 通過註冊中心實現自動化的地址通知。其中,Provider 實例的信息如圖中表格所示:應用 DEMO 包含三個接口 DemoService 1 2 3,當前實例的 ip 地址爲 10.210.134.30。

  • 對於 Spring Cloud 和 Kubernetes 模型,註冊中心只會存儲一條 DEMO - 10.210.134.30+metadata 的數據

  • 對於老的 Dubbo 模型,註冊中心存儲了三條接口粒度的數據,分別對應三個接口 DemoService 1 2 3,並且很多的址數據都是重複的

可以總結出,基於應用粒度的模型所存儲和推送的數據量是和應用、實例數成正比的,只有當我們的應用數增多或應用的實例數增長時,地址推送壓力纔會上漲。

而對於基於接口粒度的模型,數據量是和接口數量正相關的,鑑於一個應用通常發佈多個接口的現狀,這個數量級本身比應用粒度是要乘以倍數的;另外一個關鍵點在於,接口粒度導致的集羣規模評估的不透明,相對於實i例、應用增長都通常是在運維側的規劃之中,接口的定義更多的是業務側的內部行爲,往往可以繞過評估給集羣帶來壓力。

以 Consumer 端服務訂閱舉例,根據我對社區部分 Dubbo 中大規模頭部用戶的粗略統計,根據受統計公司的實際場景,一個 Consumer 應用要消費(訂閱)的 Provier 應用數量往往要超過 10 個,而具體到其要消費(訂閱)的的接口數量則通常要達到 30 個,平均情況下 Consumer 訂閱的 3 個接口來自同一個 Provider 應用,如此計算下來,如果以應用粒度爲地址通知和選址基本單位,則平均地址推送和計算量將下降 60% 還要多。

而在極端情況下,也就是當 Consumer 端消費的接口更多的來自同一個應用時,這個地址推送與內存消耗的佔用將會進一步得到降低,甚至可以超過 80% 以上。

一個典型的幾段場景即是 Dubbo 體系中的網關型應用,有些網關應用消費(訂閱)達 100+ 應用,而消費(訂閱)的服務有 1000+ ,平均有 10 個接口來自同一個應用,如果我們把地址推送和計算的粒度改爲應用,則地址推送量從原來的 n 1000 變爲 n 100,地址數量降低可達近 90%。

四、應用級服務發現工作原理

4.1 設計原則

上面一節我們從服務模型支撐大規模集羣的角度分別給出了 Dubbo 往應用級服務發現靠攏的好處或原因,但這麼做的同時接口粒度的服務治理能力還是要繼續保留,這是 Dubbo 框架編程模型易用性、服務治理能力優勢的基礎。

以下是我認爲我們做服務模型遷移仍要堅持的設計原則:

  • 新的服務發現模型要實現對原有 Dubbo 消費端開發者的無感知遷移,即 Dubbo 繼續面向 RPC 服務編程、面向 RPC 服務治理,做到對用戶側完全無感知

  • 建立 Consumer 與 Provider 間的自動化 RPC 服務元數據協調機制,解決傳統微服務模型無法同步 RPC 級接口配置的缺點

4.2 基本原理詳解

應用級服務發現作爲一種新的服務發現機制,和以前 Dubbo 基於 RPC 服務粒度的服務發現在覈心流程上基本上是一致的:即服務提供者往註冊中心註冊地址信息,服務消費者從註冊中心拉取&訂閱地址信息。

這裏主要的不同有以下兩點:

4.2.1 註冊中心數據以“應用 - 實例列表”格式組織,不再包含 RPC 服務信息

以下是每個 Instance metadata 的示例數據,總的原則是 metadata 只包含當前 instance 節點相關的信息,不涉及 RPC 服務粒度的信息。

總體信息概括如下:實例地址、實例各種環境標、metadata service 元數據、其他少量必要屬性。

{

    "name": "provider-app-name",

    "id": "192.168.0.102:20880",

    "address": "192.168.0.102",

    "port": 20880,

    "sslPort": null,

    "payload": {

        "id": null,

        "name": "provider-app-name",

        "metadata": {

            "metadataService": "{\"dubbo\":{\"version\":\"1.0.0\",\"dubbo\":\"2.0.2\",\"release\":\"2.7.5\",\"port\":\"20881\"}}",

            "endpoints": "[{\"port\":20880,\"protocol\":\"dubbo\"}]",

            "storage-type": "local",

            "revision": "6785535733750099598",

        }

    },

    "registrationTimeUTC": 1583461240877,

    "serviceType": "DYNAMIC",

    "uriSpec": null

}

4.2.2 Client – Server 自行協商 RPC 方法信息

在註冊中心不再同步 RPC 服務信息後,服務自省在服務消費端和提供端之間建立了一條內置的 RPC 服務信息協商機制,這也是“服務自省”這個名字的由來。服務端實例會暴露一個預定義的 MetadataService RPC 服務,消費端通過調用 MetadataService 獲取每個實例 RPC 方法相關的配置信息。

當前 MetadataService 返回的數據格式如下:

[

  "dubbo://192.168.0.102:20880/org.apache.dubbo.demo.DemoService?anyhost=true&application=demo-provider&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.demo.DemoService&methods=sayHello&pid=9585&release=2.7.5&side=provider×tamp=1583469714314",

 "
dubbo://192.168.0.102:20880/org.apache.dubbo.demo.HelloService?anyhost=true&application=demo-provider&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.demo.DemoService&methods=sayHello&pid=9585&release=2.7.5&side=provider×tamp=1583469714314",

  "
dubbo://192.168.0.102:20880/org.apache.dubbo.demo.WorldService?anyhost=true&application=demo-provider&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.demo.DemoService&methods=sayHello&pid=9585&release=2.7.5&side=provider×tamp=1583469714314"

]

熟悉 Dubbo 基於 RPC 服務粒度的服務發現模型的開發者應該能看出來,服務自省機制機制將以前註冊中心傳遞的 URL 一拆爲二:

  • 一部分和實例相關的數據繼續保留在註冊中心,如 ip、port、機器標識等

  • 另一部分和 RPC 方法相關的數據從註冊中心移除,轉而通過 MetadataService 暴露給消費端

理想情況下是能達到數據按照實例、RPC 服務嚴格區分開來,但明顯可以看到以上實現版本還存在一些數據冗餘,有些也數據還未合理劃分。尤其是 MetadataService 部分,其返回的數據還只是簡單的 URL 列表組裝,這些 URL其實是包含了全量的數據。

以下是服務自省的一個完整工作流程圖,詳細描述了服務註冊、服務發現、MetadataService、RPC 調用間的協作流程。

  • 服務提供者啓動,首先解析應用定義的“普通服務”並依次註冊爲 RPC 服務,緊接着註冊內建的 MetadataService 服務,最後打開 TCP 監聽端口

  • 啓動完成後,將實例信息註冊到註冊中心(僅限 ip、port 等實例相關數據),提供者啓動完成

  • 服務消費者啓動,首先依據其要“消費的 provider 應用名”到註冊中心查詢地址列表,並完成訂閱(以實現後續地址變更自動通知)

  • 消費端拿到地址列表後,緊接着對 MetadataService 發起調用,返回結果中包含了所有應用定義的“普通服務”及其相關配置信息

  • 至此,消費者可以接收外部流量,並對提供者發起 Dubbo RPC 調用

在以上流程中,我們只考慮了一切順利的情況,但在更詳細的設計或編碼實現中,我們還需要嚴格約定一些異常場景下的框架行爲。比如,如果消費者 MetadataService 調用失敗,則在重試知道成功之前,消費者將不可以接收外部流量。

4.3 服務自省中的關鍵機制

4.3.1 元數據同步機制

Client 與 Server 間在收到地址推送後的配置同步是服務自省的關鍵環節,目前針對元數據同步有兩種具體的可選方案,分別是:

  • 內建 MetadataService

  • 獨立的元數據中心,通過中細化的元數據集羣協調數據

內建 MetadataService

MetadataService 通過標準的 Dubbo 協議暴露,根據查詢條件,會將內存中符合條件的“普通服務”配置返回給消費者。這一步發生在消費端選址和調用前。

元數據中心

複用 2.7 版本中引入的元數據中心,provider 實例啓動後,會嘗試將內部的 RPC 服務組織成元數據的格式到元數據中心,而 consumer 則在每次收到註冊中心推送更新後,主動查詢元數據中心。

注意 consumer 端查詢元數據中心的時機,是等到註冊中心的地址更新通知之後。也就是通過註冊中心下發的數據,我們能明確的知道何時某個實例的元數據被更新了,此時才需要去查元數據中心。

4.3.2 RPC 服務 < - > 應用映射關係

回顧上文講到的註冊中心關於“應用 - 實例列表”結構的數據組織形式,這個變動目前對開發者並不是完全透明的,業務開發側會感知到查詢/訂閱地址列表的機制的變化。具體來說,相比以往我們基於 RPC 服務來檢索地址,現在 consumer 需要通過指定 provider 應用名才能實現地址查詢或訂閱。

老的 Consumer 開發與配置示例:

<!-- 框架直接通過 RPC Service 1/2/N 去註冊中心查詢或訂閱地址列表 -->

<dubbo:registry address="zookeeper://127.0.0.1:2181"/>

<dubbo:reference interface="RPC Service 1" />

<dubbo:reference interface="RPC Service 2" />

<dubbo:reference interface="RPC Service N" />

新的 Consumer 開發與配置示例:

<!-- 框架需要通過額外的 provided-by="provider-app-x" 才能在註冊中心查詢或訂閱到地址列表 -->

<dubbo:registry address="zookeeper://127.0.0.1:2181?registry-type=service"/>

<dubbo:reference interface="RPC Service 1" provided-by="provider-app-x"/>

<dubbo:reference interface="RPC Service 2" provided-by="provider-app-x" />

<dubbo:reference interface="RPC Service N" provided-by="provider-app-y" />

以上指定 provider 應用名的方式是 Spring Cloud 當前的做法,需要 consumer 端的開發者顯示指定其要消費的 provider 應用。

以上問題的根源在於註冊中心不知道任何 RPC 服務相關的信息,因此只能通過應用名來查詢。

爲了使整個開發流程對老的 Dubbo 用戶更透明,同時避免指定 provider 對可擴展性帶來的影響(參見下方說明),我們設計了一套 RPC 服務到應用名的映射關係,以嘗試在 consumer 自動完成 RPC 服務到 provider 應用名的轉換。

Dubbo 之所以選擇建立一套“接口-應用”的映射關係,主要是考慮到 service - app 映射關係的不確定性。一個典型的場景即是應用/服務拆分,如上面提到的配置<dubbo:reference interface="RPC Service 2" provided-by="provider-app-x" />,PC Service 2 是定義於 provider-app-x 中的一個服務,未來它隨時可能會被開發者分拆到另外一個新的應用如 provider-app-x-1 中,這個拆分要被所有的 PC Service 2 消費方感知到,並對應用進行修改升級,如改爲<dubbo:reference interface="RPC Service 2" provided-by="provider-app-x-1" />,這樣的升級成本不可否認還是挺高的。

到底是 Dubbo 框架幫助開發者透明的解決這個問題,還是交由開發者自己去解決,當然這只是個策略選擇問題,並且 Dubbo 2.7.5+ 版本目前是都提供了的。其實我個人更傾向於交由業務開發者通過組織上的約束來做,這樣也可進一步降低 Dubbo 框架的複雜度,提升運行態的穩定性。

五、總結與展望

應用級服務發現機制是 Dubbo 面向雲原生走出的重要一步,它幫 Dubbo 打通了與其他微服務體系之間在地址發現層面的鴻溝,也成爲 Dubbo 適配 Kubernetes Native Service 等基礎設施的基礎。我們期望 Dubbo 在新模型基礎上,能繼續保留在編程易用性、服務治理能力等方面強大的優勢。但是我們也應該看到應用粒度的模型一方面帶來了新的複雜性,需要我們繼續去優化與增強;另一方面,除了地址存儲與推送之外,應用粒度在幫助 Dubbo 選址層面也有進一步挖掘的潛力。

END -

「技術分享」某種程度上,是讓作者和讀者,不那麼孤獨的東西。歡迎關注我的微信公衆號:Kirito的技術分享」


本文分享自微信公衆號 - Kirito的技術分享(cnkirito)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章