不管是服務導出還是服務引入,都發生在應用啓動過程中,比如:在啓動類上加上 @EnableDubbo 時,該註解上有一個 @DubboComponentScan 註解,@DubboComponentScan 註解 Import 了一個 DubboComponentScanRegistrar,DubboComponentScanRegistrar 中會調用 DubboSpringInitializer.initialize(),該方法中會註冊一個 DubboDeployApplicationListener,而 DubboDeployApplicationListener 會監聽 Spring 容器啓動完成事件 ContextRefreshedEvent,一旦接收到這個事件後,就會開始 Dubbo 的啓動流程,就會執行 DefaultModuleDeployer 的 start() 進行服務導出與服務引入。
在啓動過程中,在做完服務導出與服務引入後,還會做幾件非常重要的事情:
服務導出
當在某個接口的實現類上加上 @DubboService 後,就表示定義了一個 Dubbo 服務,應用啓動時 Dubbo 只要掃描到了 @DubboService,就會解析對應的類,得到服務相關的配置信息,比如:
解析完服務的配置信息後,就會把這些配置信息封裝成爲一個 ServiceConfig 對象,並調用其 export() 進行服務導出,此時一個 ServiceConfig 對象就表示一個Dubbo 服務。
而所謂的服務導出,主要就是完成三件事情:
確定服務參數
一個 Dubbo 服務,除開服務的名字,也就是接口名,還會有很多其他的屬性,比如:超時時間、版本號、服務所屬應用名、所支持的協議及綁定的端口等衆多信息。
但是,通常這些信息並不會全部在 @DubboService 中進行定義,比如:一個 Dubbo 服務肯定是屬於某個應用的,而一個應用下可以有多個 Dubbo 服務,所以可以在應用級別定義一些通用的配置,比如協議。
在 application.yml 中定義:
dubbo:
application:
name: dubbo-springboot-demo-provider
protocol:
name: tri
port: 20880
表示當前應用下所有的 Dubbo 服務都支持通過 tri 協議進行訪問,並且訪問端口爲 20880,所以在進行某個服務的服務導出時,就需要將應用中的這些配置信息合併到當前服務的配置信息中。
另外,除開可以通過 @DubboService 來配置服務,也可以在配置中心對服務進行配置,比如:在配置中心中配置:
dubbo.service.org.apache.dubbo.samples.api.DemoService.timeout=5000
表示當前服務的超時時間爲 5s。
所以,在服務導出時,也需要從配置中心獲取當前服務的配置,如果在 @DubboService 中也定義了 timeout,那麼就用配置中心的覆蓋掉,配置中心的配置優先級更高。
最終確定出服務的各種參數,這塊內容和 Dubbo2.7 一致。
服務註冊
當確定好了最終的服務配置後,Dubbo 就會根據這些配置信息生成對應的服務 URL,比如:
tri://192.168.65.221:20880/org.apache.dubbo.springboot.demo.DemoService?application=dubbo-springboot-demo-provider&timeout=3000
這個 URL 就表示了一個 Dubbo 服務,服務消費者只要能獲得到這個服務 URL,就知道了關於這個 Dubbo 服務的全部信息,包括服務名、支持的協議、ip、port、各種配置。
確定了服務 URL 之後,服務註冊要做的事情就是把這個服務 URL 存到註冊中心(比如:Zookeeper)中去,說的再簡單一點,就是把這個字符串存到 Zookeeper中去,這個步驟其實是非常簡單的,實現這個功能的源碼在 RegistryProtocol 中的 export() 方法中,最終服務 URL 存在了 Zookeeper 的 /dubbo/ 接口名 /providers 目錄下。
但是服務註冊並不僅僅就這麼簡單,既然上面的這個 URL 表示一個服務,並且還包括了服務的一些配置信息,那這些配置信息如果改變了呢?比如:利用 Dubbo管理臺中的動態配置功能(注意,並不是配置中心)來修改服務配置,動態配置可以應用運行過程中動態的修改服務的配置,並實時生效。
如果利用動態配置功能修改了服務的參數,那此時就要重新生成服務 URL 並重新註冊到註冊中心,這樣服務消費者就能及時的獲取到服務配置信息。
而對於服務提供者而言,在服務註冊過程中,還需要能監聽到動態配置的變化,一旦發生了變化,就根據最新的配置重新生成服務 URL,並重新註冊到中心。
應用級註冊
在 Dubbo3.0 之前,Dubbo 是接口級註冊,服務註冊就是把接口名以及服務配置信息註冊到註冊中心中,註冊中心存儲的數據格式大概爲:
接口名1:tri://192.168.1.221:20880/接口名1?application=應用名
接口名2:tri://192.168.1.221:20880/接口名2?application=應用名
接口名3:tri://192.168.1.221:20880/接口名3?application=應用名
key 是接口名,value 就是服務 URL,上面的內容就表示現在有一個應用,該應用下有 3 個接口,應用實例部署在 192.168.1.221,此時,如果給該應用增加一個實例,實例 ip 爲192.168.1.222,那麼新的實例也需要進行服務註冊,會向註冊中心新增 3 條數據:
接口名1:tri://192.168.1.221:20880/接口名1?application=應用名
接口名2:tri://192.168.1.221:20880/接口名2?application=應用名
接口名3:tri://192.168.1.221:20880/接口名3?application=應用名
接口名1:tri://192.168.1.222:20880/接口名1?application=應用名
接口名2:tri://192.168.1.222:20880/接口名2?application=應用名
接口名3:tri://192.168.1.222:20880/接口名3?application=應用名
可以發現,如果一個應用中有 3 個 Dubbo 服務,那麼每增加一個實例,就會向註冊中心增加 3 條記錄,那如果一個應用中有 10 個 Dubbo 服務,那麼每增加一個實例,就會向註冊中心增加 10 條記錄,註冊中心的壓力會隨着應用實例的增加而劇烈增加。
反過來,如果一個應用有 3 個 Dubbo 服務,5 個實例,那麼註冊中心就有 15 條記錄,此時增加一個 Dubbo 服務,那麼註冊中心就會新增 5 條記錄,註冊中心的壓力也會劇烈增加。
註冊中心的數據越多,數據就變化的越頻繁,比如:修改服務的 timeout,那麼對於註冊中心和應用都需要消耗資源用來處理數據變化。
所以爲了降低註冊中心的壓力,Dubbo3.0 支持了應用級註冊,同時也兼容接口級註冊,用戶可以逐步遷移成應用級註冊,而一旦採用應用級註冊,最終註冊中心的數據存儲就變成爲:
應用名:192.168.1.221:20880
應用名:192.168.1.222:20880
表示在註冊中心中,只記錄應用所對應的實例信息(IP + 綁定的端口),這樣只有一個應用的實例增加了,那麼註冊中心的數據纔會增加,而不關心一個應用中到底有多少個 Dubbo 服務。
這樣帶來的好處就是,註冊中心存儲的數據變少了,註冊中心中數據的變化頻率變小了,並且使用應用級註冊,使得 Dubbo3 能實現與異構微服務體系如:Spring Cloud、Kubernetes Service 等在地址發現層面更容易互通, 爲連通 Dubbo 與其他微服務體系提供可行方案。
應用級註冊帶來了好處,但是對於 Dubbo 來說又出現了一些新的問題,比如:原本,服務消費者可以直接從註冊中心就知道某個 Dubbo 服務的所有服務提供者以及相關的協議、ip、port、配置等信息,那現在註冊中心上只有 ip、port,那對於服務消費者而言:服務消費者怎麼知道現在它要用的某個 Dubbo 服務,也就是某個接口對應的應用是哪個呢?
對於這個問題,在進行服務導出的過程中,會在 Zookeeper 中存一個映射關係,在服務導出的最後一步,在 ServiceConfig 的 exported() 方法中,會保存這個映射關係:接口名:應用名
。這個映射關係存在 Zookeeper 的 /dubbo/mapping 目錄下,存了這個信息後,消費者就能根據接口名找到所對應的應用名了。
消費者知道了要使用的 Dubbo 服務在哪個應用,那也就能從註冊中心中根據應用名查到應用的所有實例信息( ip + port ),也就是可以發送方法調用請求了,但是在真正發送請求之前,還得知道服務的配置信息,對於消費者而言,它得知道當前要調用的這個 Dubbo 服務支持什麼協議、timeout 是多少,那服務的配置信息從哪裏獲取呢?
之前的服務配置信息是直接從註冊中心就可以獲取到的,就是服務 URL 後面,但是現在不行了,現在需要從服務提供者的元數據服務獲取,前面提到過,在應用啓動過程中會進行服務導出和服務引入,然後就會暴露一個應用元數據服務,其實這個應用元數據服務就是一個 Dubbo 服務(Dubbo 框架內置的,自己實現的 ),消費者可以調用這個服務來獲取某個應用中所提供的所有 Dubbo 服務以及服務配置信息,這樣也就能知道服務的配置信息了。
知道了應用註冊的好處,以及相關問題的解決方式,那麼來看它到底是如何實現的。
首先,我們可以通過配置 dubbo.application.register-mode 來控制:
不管是什麼註冊,都需要存數據到註冊中心,而 Dubbo3 的源碼實現中會根據所配置的註冊中心生成兩個 URL(不是服務 URL,可以理解爲註冊中心 URL,用來訪問註冊中心的):
這兩個 URL 只有 schema 不一樣,一個是 service-discovery-registry,一個是 registry,而 registry 是 Dubbo3 之前就存在的,也就代表接口級服務註冊,而service-discovery-registry 就表示應用級服務註冊。
在服務註冊相關的源碼中,當調用 RegistryProtocol 的 export() 方法處理 registry:// 時,會利用 ZookeeperRegistry 把服務 URL 註冊到 Zookeeper 中去,這就是接口級註冊。
而類似,當調用 RegistryProtocol 的 export() 方法處理 service-discovery-registry:// 時,會利用 ServiceDiscoveryRegistry 來進行相關邏輯的處理,那是不是就是在這裏把應用信息註冊到註冊中心去呢?並沒有這麼簡單。
所以 ServiceDiscoveryRegistry 在註冊一個服務 URL 時,並不會往註冊中心存數據,而只是把服務 URL 存到到一個 MetadataInfo 對象中,MetadataInfo 對象中就保存了當前應用中所有的 Dubbo 服務信息:服務名、支持的協議、綁定的端口、timeout 等。
前面提到過,在應用啓動的最後,纔會進行應用級註冊,而應用級註冊就是當前的應用實例上相關的信息存入註冊中心,包括:
比如:
{
"name":"dubbo-springboot-demo-provider",
"id":"192.168.65.221:20882",
"address":"192.168.65.221",
"port":20882,
"sslPort":null,
"payload":{
"@class":"org.apache.dubbo.registry.zookeeper.ZookeeperInstance",
"id":"192.168.65.221:20882",
"name":"dubbo-springboot-demo-provider",
"metadata":{
"dubbo.endpoints":"[{\"port\":20882,\"protocol\":\"dubbo\"},{\"port\":50051,\"protocol\":\"tri\"}]",
"dubbo.metadata-service.url-params":"{\"connections\":\"1\",\"version\":\"1.0.0\",\"dubbo\":\"2.0.2\",\"side\":\"provider\",\"port\":\"20882\",\"protocol\":\"dubbo\"}",
"dubbo.metadata.revision":"65d5c7b814616ab10d32860b54781686",
"dubbo.metadata.storage-type":"local"
}
},
"registrationTimeUTC":1654585977352,
"serviceType":"DYNAMIC",
"uriSpec":null
}
一個實例上可能支持多個協議以及多個端口,那如何確定實例的 ip 和端口呢?
答案是:獲取 MetadataInfo 對象中保存的所有服務 URL,優先取 dubbo 協議對應 ip 和 port,沒有 dubbo 協議則所有服務 URL 中的第一個 URL 的 ip 和 port。
另外一個協議一般只會對應一個端口,但是如何就是對應了多個,比如:
dubbo:
application:
name: dubbo-springboot-demo-provider
protocols:
p1:
name: dubbo
port: 20881
p2:
name: dubbo
port: 20882
p3:
name: tri
port: 50051
如果是這樣,最終存入 endpoint 中的會保證一個協議只對應一個端口,另外那個將被忽略,最終服務消費者在進行服務引入時將會用到這個 endpoint 信息。
確定好實例信息後之後,就進行最終的應用註冊了,就把實例信息存入註冊中心的 /services/應用名,目錄下:
可以看出 services 節點下存的是應用名,應用名的節點下存的是實例 ip 和實例 port,而 ip 和 port 這個節點中的內容就是實例的一些基本信息。
額外,我們可以配置 dubbo.metadata.storage-type,默認時 local,可以通過配置改爲 remote:
dubbo:
application:
name: dubbo-springboot-demo-provider
metadata-type: remote
這個配置其實跟應用元數據服務有關係:
在 Dubbo2.7 中就有了元數據中心,它其實就是用來減輕註冊中心的壓力的,Dubbo 會把服務信息完整的存一份到元數據中心,元數據中心也可以用 Zookeeper來實現,在暴露完元數據服務之後,在註冊實例信息到註冊中心之前,就會把 MetadataInfo 存入元數據中心,比如:
節點內容爲:
{
"app":"dubbo-springboot-demo-provider",
"revision":"64e68950e300068e6b5f8632d9fd141d",
"services":{
"org.apache.dubbo.springboot.demo.HelloService:tri":{
"name":"org.apache.dubbo.springboot.demo.HelloService",
"protocol":"tri",
"path":"org.apache.dubbo.springboot.demo.HelloService",
"params":{
"side":"provider",
"release":"",
"methods":"sayHello",
"deprecated":"false",
"dubbo":"2.0.2",
"interface":"org.apache.dubbo.springboot.demo.HelloService",
"service-name-mapping":"true",
"generic":"false",
"metadata-type":"remote",
"application":"dubbo-springboot-demo-provider",
"background":"false",
"dynamic":"true",
"anyhost":"true"
}
},
"org.apache.dubbo.springboot.demo.DemoService:tri":{
"name":"org.apache.dubbo.springboot.demo.DemoService",
"protocol":"tri",
"path":"org.apache.dubbo.springboot.demo.DemoService",
"params":{
"side":"provider",
"release":"",
"methods":"sayHelloStream,sayHello,sayHelloServerStream",
"deprecated":"false",
"dubbo":"2.0.2",
"interface":"org.apache.dubbo.springboot.demo.DemoService",
"service-name-mapping":"true",
"generic":"false",
"metadata-type":"remote",
"application":"dubbo-springboot-demo-provider",
"background":"false",
"dynamic":"true",
"anyhost":"true"
}
}
}
}
這裏面就記錄了當前實例上提供了哪些服務以及對應的協議,注意並沒有保存對應的端口......,所以後面服務消費者得利用實例信息中的 endpoint,因爲endpoint 中記錄了協議對應的端口....
其實元數據中心和元數據服務提供的功能是一樣的,都可以用來獲取某個實例的 MetadataInfo,上面中的 UUID 表示實例編號,只不過元數據中心是集中式的,元數據服務式分散在各個提供者實例中的,如果整個微服務集羣壓力不大,那麼效果差不多,如果微服務集羣壓力大,那麼元數據中心的壓力就大,此時單個元數據服務就更適合,所以默認也是採用的元數據服務。
至此,應用級服務註冊的原理就分析完了,總結一下:
服務暴露
服務暴露就是根據不同的協議啓動不同的 Server,比如:dubbo 和 tri 協議啓動的都是 Netty,像 Dubbo2.7 中的 http 協議啓動的就是 Tomcat,這塊在服務調用的時候再來分析。
服務引入
@DubboReference
private DemoService demoService;
需要利用 @DubboReference 註解來引入某一個 Dubbo 服務,應用在啓動過程中,進行完服務導出之後,就會進行服務引入,屬性的類型就是一個 Dubbo 服務接口,而服務引入最終要做到的就是給這個屬性賦值一個接口代理對象。
在 Dubbo2.7 中,只有接口級服務註冊,服務消費者會利用接口名從註冊中心找到該服務接口所有的服務 URL,服務消費者會根據每個服務 URL 的 protocol、ip、port 生成對應的 Invoker 對象,比如生成 TripleInvoker、DubboInvoker 等,調用這些 Invoker 的 invoke() 方法就會發送數據到對應的 ip、port,生成好所有的 Invoker 對象之後,就會把這些 Invoker 對象進行封裝並生成一個服務接口的代理對象,代理對象調用某個方法時,會把所調用的方法信息生成一個 Invocation 對象,並最終通過某一個 Invoker 的 invoke() 方法把 Invocation 對象發送出去,所以代理對象中的 Invoker 對象是關鍵,服務引入最核心的就是要生成這些 Invoker 對象。
Invoker 是非常核心的一個概念,也有非常多種類,比如:
像 TripleInvoker 和 DubboInvoker 對應的就是具體服務提供者,包含了服務提供者的 ip 地址和端口,並且會負責跟對應的 ip 和 port 建立 Socket 連接,後續就可以基於這個 Socket 連接並按協議格式發送 Invocation 對象。
比如現在引入了 DemoService 這個服務,那如果該服務支持:
那麼在服務消費端這邊,就會生成兩個 TripleInvoker 和一個 DubboInvoker,代理對象執行方法時就會進行負載均衡選擇其中一個 Invoker 進行調用。
接口級服務引入
在服務導出時,Dubbo3.0 默認情況下即會進行接口級註冊,也會進行應用級註冊,目的就是爲了兼容服務消費者應用,用的還是 Dubbo2.7,用 Dubbo2.7 就只能老老實實的進行接口級服務引入。
接口級服務引入核心就是要找到當前所引入的服務有哪些服務 URL,然後根據每個服務 URL 生成對應的 Invoker,流程爲:
應用級服務引入
在 Dubbo 中,應用級服務引入,並不是指引入某個應用,這裏和 SpringCloud 是有區別的,在 SpringCloud 中,服務消費者只要從註冊中心找到要調用的應用的所有實例地址就可以了,但是在 Dubbo 中找到應用的實例地址還遠遠不夠,因爲在 Dubbo 中是直接使用的接口,所以在 Dubbo 中就算是應用級服務引入,最終還是得找到服務接口有哪些服務提供者。
所以,對於服務消費者而言,不管是使用接口級服務引入,還是應用級服務引入,最終的結果應該得是一樣的,也就是某個服務接口的提供者 Invoker 是一樣的,不可能使用應用級服務引入得到的 Invoker 多一個或少一個,但是!!!,目前會有情況不一致,就是一個協議有多個端口時,比如在服務提供者應用這邊支持:
dubbo:
application:
name: dubbo-springboot-demo-provider
protocols:
p1:
name: dubbo
port: 20881
p2:
name: tri
port: 20882
p3:
name: tri
port: 50051
那麼在消費端進行服務引入時,比如:引入 DemoService 時,接口級服務引入會生成 3 個 Invoker(2個 TripleInvoker,1個DubboInvoker),而應用級服務引入只會生成 2 個 Invoker(1個TripleInvoker,1個DubboInvoker),原因就是在進行應用級註冊時是按照一個協議對應一個port存的。
那既然接口級服務引入和應用級服務引入最終的結果差不多,可能就不理解了,那應用級服務引入有什麼好處呢?要知道應用級服務引入和應用級服務註冊是對應,服務提供者應用如果只做應用級註冊,那麼對應的服務消費者就只能進行應用級服務引入,好處就是前面所說的,減輕了註冊中心的壓力等,那麼帶來的影響就是服務消費者端尋找服務 URL 的邏輯更復雜了。
只要找到了當前引入服務對應的服務 URL,然後生成對應的 Invoker,並最終生成一個 ClusterInvoker。
在進行應用級服務引入時:
MigrationInvoker 的生成
上面分析了接口級服務引入和應用級服務引入,最終都是得到某個服務對應的服務提供者 Invoker,那最終進行服務調用時,到底該怎麼選擇呢?
所以在 Dubbo3.0 中,可以配置:
# dubbo.application.service-discovery.migration 僅支持通過 -D 以及 全局配置中心 兩種方式進行配置。
dubbo.application.service-discovery.migration=APPLICATION_FIRST
# 可選值
# FORCE_INTERFACE,強制使用接口級服務引入
# FORCE_APPLICATION,強制使用應用級服務引入
# APPLICATION_FIRST,智能選擇是接口級還是應用級,默認就是這個
對於前兩種強制的方式,沒什麼特殊,就是上面走上面分析的兩個過程,沒有額外的邏輯,那對於 APPLICATION_FIRST 就需要有額外的邏輯了,也就是 Dubbo 要判斷,當前所引入的這個服務,應該走接口級還是應用級,這該如何判斷呢?
事實上,在進行某個服務的服務引入時,會統一利用 InterfaceCompatibleRegistryProtocol 的 refer 來生成一個 MigrationInvoker 對象,在 MigrationInvoker 中有三個屬性:
private volatile ClusterInvoker<T> invoker; // 用來記錄接口級ClusterInvoker
private volatile ClusterInvoker<T> serviceDiscoveryInvoker; // 用來記錄應用級的ClusterInvoker
private volatile ClusterInvoker<T> currentAvailableInvoker; // 用來記錄當前使用的ClusterInvoker,要麼是接口級,要麼應用級
一開始構造出來的 MigrationInvoker 對象中三個屬性都爲空,接下來會利用 MigrationRuleListener 來處理 MigrationInvoker 對象,也就是給這三個屬性賦值。
在 MigrationRuleListener 的構造方法中,會從配置中心讀取 DUBBO_SERVICEDISCOVERY_MIGRATION 組下面的"當前應用名+.migration"的配置項,配置項爲 yml 格式,對應的對象爲 MigrationRule,也就是可以配置具體的遷移規則,比如:某個接口或某個應用的 MigrationStep(FORCE_INTERFACE、APPLICATION_FIRST、FORCE_APPLICATION),還可以配置 threshold,表示一個閾值,比如:配置爲 2,表示應用級 Invoker 數量是接口級 Invoker 數量的兩倍時才使用應用級 Invoker,不然就使用接口級數量,可以參考:https://cn.dubbo.apache.org/zh/docs/advanced/migration-invoker/
如果沒有配置遷移規則,則會看當前應用中是否配置了 migration.step,如果沒有,那就從全局配置中心讀取 dubbo.application.service-discovery.migration 來獲取 MigrationStep,如果也沒有配置,那 MigrationStep 默認爲 APPLICATION_FIRST
如果沒有配置遷移規則,則會看當前應用中是否配置了 migration.threshold,如果沒有配,則 threshold 默認爲 -1。
在應用中可以這麼配置:
dubbo:
application:
name: dubbo-springboot-demo-consumer
parameters:
migration.step: FORCE_APPLICATION
migration.threshold: 2
確定了 step 和 threshold 之後,就要真正開始給 MigrationInvoker 對象中的三個屬性賦值了,先根據 step 調用不同的方法
switch (step) {
case APPLICATION_FIRST:
// 先進行接口級服務引入得到對應的ClusterInvoker,並賦值給invoker屬性
// 再進行應用級服務引入得到對應的ClusterInvoker,並賦值給serviceDiscoveryInvoker屬性
// 再根據兩者的數量判斷到底用哪個,並且把確定的ClusterInvoker賦值給currentAvailableInvoker屬性
migrationInvoker.migrateToApplicationFirstInvoker(newRule);
break;
case FORCE_APPLICATION:
// 只進行應用級服務引入得到對應的ClusterInvoker,並賦值給serviceDiscoveryInvoker和currentAvailableInvoker屬性
success = migrationInvoker.migrateToForceApplicationInvoker(newRule);
break;
case FORCE_INTERFACE:
default:
// 只進行接口級服務引入得到對應的ClusterInvoker,並賦值給invoker和currentAvailableInvoker屬性
success = migrationInvoker.migrateToForceInterfaceInvoker(newRule);
}
這裏只需要分析當 step 爲 APPLICATION_FIRST 時,是如何確定最終要使用的 ClusterInvoker 的。
得到了接口級 ClusterInvoker 和應用級 ClusterInvoker 之後,就會利用 DefaultMigrationAddressComparator 來進行判斷:
threshold 默認爲 0,那就表示在既有應用級 Invoker 又有接口級 Invoker 的情況下,就一定會用應用級 Invoker,兩個正數相除,結果肯定爲正數,當然你自己可以控制 threshold,如果既有既有應用級 Invoker 又有接口級 Invoker 的情況下,你想在應用級 Invoker 的個數大於接口級 Invoker 的個數時採用應用級Invoker,那就可以把 threshold 設置爲 1,表示個數相等,或者個數相除之後的結果大於 1 時用應用級 Invoker,否者用接口級 Invoker
這樣 MigrationInvoker 對象中的三個數據就能確定好值了,和在最終的接口代理對象執行某個方法時,就會調用 MigrationInvoker 對象的 invoke,在這個invoke 方法中會直接執行 currentAvailableInvoker 對應的 invoker 的 invoker 方法,從而進入到了接口級 ClusterInvoker 或應用級 ClusterInvoker 中,從而進行負載均衡,選擇出具體的 DubboInvoer 或 TripleInvoker,完成真正的服務調用。
服務調用底層原理
在 Dubbo2.7 中,默認的是 Dubbo 協議,因爲 Dubbo 協議相比較於 Http1.1 而言,Dubbo 協議性能上是要更好的。
但是 Dubbo 協議自己的缺點就是不通用,假如現在通過 Dubbo 協議提供了一個服務,那如果想要調用該服務就必須要求服務消費者也要支持 Dubbo 協議,比如想通過瀏覽器直接調用 Dubbo 服務是不行的,想通過 Nginx 調 Dubbo 服務也是不行得。
而隨着企業的發展,往往可能會出現公司內部使用多種技術棧,可能這個部門使用 Dubbo,另外一個部門使用 Spring Cloud,另外一個部門使用 gRPC,那此時部門之間要想相互調用服務就比較複雜了,所以需要一個通用的、性能也好的協議,這就是 Triple 協議。
Triple 協議是基於 Http2 協議的,也就是在使用 Triple 協議發送數據時,會按 HTTP2 協議的格式來發送數據,而 HTTP2 協議相比較於 HTTP1 協議而言,HTTP2是 HTTP1 的升級版,完全兼容 HTTP1,而且 HTTP2 協議從設計層面就解決了 HTTP1 性能低的問題。
另外,Google 公司開發的 gRPC,也基於的 HTTP2,目前 gRPC 是雲原生事實上協議標準,包括 k8s/etcd 等都支持 gRPC 協議。
所以 Dubbo3.0 爲了能夠更方便的和 k8s 進行通信,在實現 Triple 的時候也兼容了 gRPC,也就是可以用 gPRC 的客戶端調用 Dubbo3.0 所提供的 triple 服務,也可以用 triple 服務調用 gRPC 的服務。
Triple 的底層原理分析
就是因爲 HTTP2 中的數據幀機制,Triple 協議才能支持 UNARY、SERVER_STREAM、BI_STREAM 三種模式。
Triple 請求調用和響應處理
創建一個 Stream 的前提是先得有一個 Socket 連接,所以我們得先知道 Socket 連接是在哪創建的。
在服務提供者進行服務導出時,會按照協議以及對應的端口啓動 Server,比如:Triple 協議就會啓動 Netty 並綁定指定的端口,等待 Socket 連接,在進行服務消費者進行服務引入的過程中,會生成 TripleInvoker 對象,在構造 TripleInvoker 對象的構造方法中,會利用 ConnectionManager 創建一個 Connection 對象,而Connection 對象中包含了一個 Bootstrap 對象(Netty 中用來建立 Socket 連接的),不過以上都只是創建對象,並不會真正和服務去建立 Socket 連接,所以在生成 TripleInvoker 對象過程中不會真正去創建 Socket 連接,那什麼時候創建的呢?
當我們在服務消費端執行以下代碼時:demoService.sayHello("habit")
demoService 是一個代理對象,在執行方法的過程中,最終會調用 TripleInvoker 的 doInvoke() 方法,在 doInvoke() 方法中,會利用 Connection 對象來判斷Socket 連接是否可用,如果不可用並且沒有初始化,那就會創建 Socket 連接。
一個 Connection 對象就表示一個 Socket 連接,在 TripleInvoker 對象中也只有一個 Connection 對象,也就是一個 TripleInvoker 對象只對應一個 Socket 連接,這個和 DubboInvoker 不太一樣,一個 DubboInvoker 中可以有多個 ExchangeClient,每個 ExchangeClient 都會與服務端創建一個 Socket 連接,所以一個DubboInvoker 可以對應多個 Socket 連接,當然多個 Socket 連接的目的就是提高併發,不過在 TripleInvoker 對象中就不需要這麼來設計了,因爲可以 Stream機制來提高併發。
以上,我們知道了,當我們利用服務接口的代理對象執行方法時就會創建一個 Socket 連接,就算這個代理對象再次執行方法時也不會再次創建 Socket 連接了,值得注意的是,有可能兩個服務接口對應的是一個 Socket 連接,舉個例子。
比如服務提供者應用 A,提供了 DemoService 和 HelloService 兩個服務,服務消費者應用 B 引入了這兩個服務,那麼在服務消費者這端,這個兩個接口對應的代理對象對應的 TripleInvoker 是不同的兩個,但是這兩個 TripleInvoker 會公用一個 Socket 連接,因爲 ConnectionManager 在創建 Connection 對象時會根據服務 URL 的 address 進行緩存,後續這兩個代理對象在執行方法時使用的就是同一個 Socket 連接,但是是不同的 Stream。
Socket 連接創建好之後,就需要發送 Invocation 對象給服務提供者了,因爲是基於的 HTTP2,所以要先創建一個 Stream,然後再通過 Stream 來發送數據。
TripleInvoker 中用的是 Netty,所以最終會利用 Netty 來創建 Stream,對應的對象爲 Http2StreamChannel,消費端的 TripleInvoker 最終會利用Http2StreamChannel 來發送和接收數據幀,數據幀對應的對象爲 Http2Frame,它又分爲 Http2DataFrame、Http2HeadersFrame 等具體類型。
正常情況下,會每生成一個數據幀就會通過 Http2StreamChannel 發送出去,但是在 Triple 中有一個小小的優化,會有一個批量發送的思想,當要發送一個數據幀時,會先把數據幀放入一個 WriteQueue 中,然後會從線程池中拿到一個線程調用 WriteQueue 的 flush 方法,該方法的實現爲:
private void flush() {
try {
QueuedCommand cmd;
int i = 0;
boolean flushedOnce = false;
// 只要隊列中有元素就取出來,沒有則退出while
while ((cmd = queue.poll()) != null) {
// 把數據幀添加到Http2StreamChannel中,添加並不會立馬發送,調用了flush才發送
cmd.run(channel);
i++;
// DEQUE_CHUNK_SIZE=128
// 連續從隊列中取到了128個數據幀就flush一次
if (i == DEQUE_CHUNK_SIZE) {
i = 0;
channel.flush();
flushedOnce = true;
}
}
// i != 0 表示從隊列中取到了數據但是沒滿128個
// 如果i=0,flushedOnce=false也flush一次
if (i != 0 || !flushedOnce) {
channel.flush();
}
} finally {
scheduled.set(false);
// 如果隊列中又有數據了,則繼續會遞歸調用flush
if (!queue.isEmpty()) {
scheduleFlush();
}
}
}
總體思想是,只要向 WriteQueue 中添加一個數據幀之後,那就會嘗試開啓一個線程,要不要開啓線程要看 CAS,比如現在有 10 個線程同時向 WriteQueue 中添加了一個數據幀,那麼這 10 個線程中的某一個會 CAS 成功,其他會 CAS 失敗,那麼此時 CAS 成功的線程會負責從線程池中獲取另外一個線程執行上面的 flush 方法,從而獲取 WriteQueue 中的數據幀然後發送出去。
有了底層這套設計之後,對於 TripleInvoker 而 ,它只需要把要發送的數據封裝爲數據幀,然後添加到 WriteQueue 中就可以了。
在 TripleInvoker 的 doInvoke() 源碼中,在創建完成 Socket 連接後,就會:
Triple 請求處理和響應結果發送
其實這部分內容和發送請求和處理響應是非常類似的,無非就是把視角從消費端切換到服務端,前面分析的是消費端發送和接收數據,現在要分析的是服務端接收和發送數據。
消費端在創建一個 Stream 後,會生成一個對應的 StreamObserver 對象用來發送數據和一個 ClientCall.Listener 用來接收響應數據,對於服務端其實也一樣,在接收到消費端創建 Stream 的命令後,也需要生成一個對應的 StreamObserver 對象用來響應數據以及一個 ServerCall.Listener 用來接收請求數據。
在服務導出時,TripleProtocol 的 export 方法中會開啓一個 ServerBootstrap,並綁定指定的端口,並且最重要的是,Netty 會負責接收創建 Stream 的信息,一旦就收到這個信號,就會生成一個 ChannelPipeline,並給 ChannelPipeline 綁定一個 TripleHttp2FrameServerHandler,而這個TripleHttp2FrameServerHandler 就可以用來處理 Http2HeadersFrame 和 Http2DataFrame。
比如在接收到請求頭後,會構造一個 ServerStream 對象,該對象有一個 ServerTransportObserver 對象,ServerTransportObserver 對象就會真正來處理請求頭和請求體:
TriDecoder:
deframe():這個方法的作用和客戶端時一樣的,都是先解析請求體的前 5 個字節,然後解壓請全體,然後反序列化得到請求參數對象,然後調用不同的ServerCall.Listener 中的 onMessage()
close():當客戶端調用 onCompleted 方法時,就表示發送數據完畢,此時會發送一個 DefaultHttp2DataFrame 並且 endStream 爲 true,從而會觸發服務端TriDecoder 對象的 close() 方法,從而調用不同的 ServerCall.Listener 中的 onComplete()
UnaryServerCallListener:
再來看 ServerStreamServerCallListener:
再來看最後一個 BiStreamServerCallListener:
總結
不管是 Unary,還是 ServerStream,還是 BiStream,底層客戶端和服務端之前都只有一個 Stream,它們三者的區別在於: