通過 Dapr 實現一個簡單的基於 dotnet 的微服務電商系統——通訊框架講解

  書接上回通過 Dapr 實現一個簡單的基於. net 的微服務電商系統,今天來分享一下這套電商 demo 的通訊部分到底是如何工作的,看看它是如何屏蔽與 dapr 繁瑣的溝通工作讓開發者專注於解決業務問題的。

  首先我們再回顧一下 dapr 的 sidecar 是如何與應用相互協同的。和 istio 類似,dapr 的 sidecar 注入可以分爲自動註冊和手動註冊,下面以手動加註解註冊的方式我們來聊一聊 dapr 的工作邏輯。首先當我們設置一個應用 (deployment) 的時候,在 template-metadata 配置了 dapr 相關注解之後,凡是安裝 dapr 集羣的 k8s 會自動將 dapr 的 sidecar 註冊到我們的 pod 中,如下圖:

當服務啓動後,我們可以用 kubectl describe po xxx 的方式看到當前該 pod 會產生兩個容器:

  凡是瞭解 k8s 的開發人員應該知道。在同一個 pod 之中,container 實例之間的通訊應該是基於同一個虛擬內網的,通俗的說就是兩者通訊可以直接通過 localhost:port 的方式,這是 dapr 與應用交互的基礎。和 istio 通過 iptables 來做流量劫持讓 Envoy 代理可以攔截所有的進出 Pod 的流量, 即將入站流量重定向到 Sidecar, 再攔截應用容器的出站流量經過 Sidecar 處理的方案相比,dapr 選擇了一個更加靈活的方式,也就是它只是主動暴露一個端口 (默認 3500),將是否和 dapr 通訊的選擇權留給了應用本身。

  當我們發起一個 rpc 請求時,實際上我們是通過 http(or grpc 這裏不展開)的方式, 訪問了了 http://localhost:3500/v1.0/{invoke}/{servicename}/method/{path} 這麼一個地址。sidecar 通過解析這個地址得到遠程服務名 {servicename}, 以及一個謂詞{invoke} 以及遠程服務的 endpoint:{path}。它會通過內部的 dns 服務名查詢 servicename 得到一個該服務在集羣內的實例列表,通過負載均衡的方式發起一個下游調用。這個下游調用也並非直接像普通 k8s 應用內通過調用 service name 的方式去調用下游 pod 的 container,而是訪問下游 pod 內的 sidecar,通過 sidecar 再去訪問 pod 內的應用實例。他們之間的調用關係如圖所示:

  通過這樣的設計,實際上應用只需要和 daprd 這個 sidcar 打交道即可。同時 dapr 實現了通過謂詞解析成不同的服務類型實現。比如服務間調用通過謂詞:{invoke}、狀態讀寫的謂詞是 {state}、訂閱發佈的謂詞是 {publish}、{subscribe},幾乎所有的行爲都可以分解成謂詞 + 服務名 + endpoint 這種模式 (所有 api 可參考:https://docs.dapr.io/reference/api/),這也是實現這套通訊框架的基礎。

   所以剩下的事情就比較簡單了,dapr 通訊基於 http/grpc,所以我們只需要啓動一套 kestrel+httpclient or grpc service/client 即可簡單快捷的接入 dapr。首先我們還是看看整個 repo(https://github.com/sd797994/Oxygen-Dapr) 的結構:

  Oxygen 這部分主要是包含通用工具層、IOC 依賴注入 (基於 autofac)、本地代理生成器 ProxyGenerator。Client 主要包含一些遠程服務 attr 標記以及客戶端代理工廠。而在 Mesh 這個單獨分層裏主要是對 Dapr 的 Actor 實現了相關封裝、Service 層比較簡單,只是在 hostbuilder 啓動了一個 kestrel 並獲取所有標記了遠程服務的接口來構建路由字典方便將我們的 Application 服務暴露成 restapi。

  本地代理 ProxyGenerator 實現比較簡單,使用了微軟自帶的代理類 DispatchProxy。通過 Autofac 依賴注入接口的時候將接口和代理類實現註冊到 ioc 容器中,這樣當我們通過 IServiceProxyFactory.CreateProxy 時實際上是從 ioc 容器中拿到的 DispatchProxy 實例,這樣調用任意該接口的方法都會被路由到 DispatchProxy 實例,從而實現方法攔截並最終通過 RemoteMessageSender 類型裏的 HttpClient 發起對 dapr 的 sidecar 請求。

  Client 層的 ServerProxyFactory 也比較簡單,其實就三個東西,一個是 IServiceProxyFactory, 這個主要用於發起對遠程 rpc 和 actor 的調用、一個是 IEventBus 以及 IStateManager,分別用於發佈事件和調用 dapr 的狀態管理器。

  Service.Kestrel 層主要是通過啓動時由 RequestDelegateFactory.CreateDelegate 的方式將所有註冊爲 remoteservice 的接口實現爲其構造一個 Func<Tservice, Tin, Task> 這樣的匿名委託並將其路由鍵和該委託註冊到一個全局靜態字典中。當收到請求時通過 kv 鍵值對的方式查詢當前 key(router) 對應的匿名委託,並通過 ioc 容器構造一個 Tservice 實例 (爲什麼要請求時創建一個實例?因爲這樣可以模擬 MVC 創建 controller 的方式將 Tservice 作爲一個 scope 生命週期的對象創建出來,避免 Tservice 內部的構造函數依賴的非單例對象生命週期失效)

  整個請求收發流程如下:

    1、當客戶端通過 IServiceProxyFactory.CreateProxy() 時獲取到該接口的 DispatchProxy 實例。

    2、實例解析各種參數後發起一個 http 調用,http 請求 localhost:3500 的 sidecar 後等待回調。

    3、sidecar 將請求組裝後發給下游 sidecar 並由下游 sidecar 轉發給 pod 內的應用。

    4、應用收到請求後解析 path 得到對應的 RequestDelegate, 調用 RequestDelegate 將請求打到具體的 xxxServcice 服務上,由服務完成具體的業務。

  Mesh.Dapr 則是對 Actor 行爲的一個具體封裝,由於原始的 dapr sdk 需要繼承 BasicActor 然後進行各種 actor 作業,我採用了另外一種方式,通過 emit 靜態代理的方式創建了一個 Actor 服務,由其代爲接收 actor 請求後再轉發給具體的 xxxServcice。同時這個 Actor 服務會啓動一個 timer,當 timer 到期時會進行一次 model 的版本檢查,當版本變化後 (一般是由於 xxxServcice 被調用),會通知 xxxServcice 繼承自基類並重寫的 SaveData 方法,由 xxxServcice 自身考慮是否需要做業務層的持久化 (默認 Actor 代理服務會自動持久化到 dapr 的狀態設備裏),這一步是完全異步的並不會阻塞 Actor 代理原方法的執行,另外在 Actor 的使用中,我們也儘量避免在同步調用時去讀取第三方的設備可能導致 IO 阻塞 actor。在源碼中涉及對 actor 調用 xxxServcice 異步的支持,我主要參考了 async/await 生成狀態機的方式創建了一個 ActorAsyncStateMachine,由該狀態機來完成 actor 服務調用 xxxServcice 的 async/await 實現。

  sample 包含一對客戶端 / 服務端案例包含上述涉及的所有遠程 call,大家可以多參考一下。

  Dapr 原始提供了一套 sdk 用於遠程服務,該框架主要是用於實現 rpc 以及對 dapr 這些 api 的自定義封裝,當使用這套框架後我們就可以不用再考慮創建具體的 webapi 控制器,由 iapplicationservice 申明遠程服務後框架即可自動生成代理服務即可。

  該框架的實現方式當然還有諸多不完善或者我沒考慮到的地方,主要起到一個拋磚引玉的作用,另外也是通過這個來了解 dapr 是如何統一了我們網絡編程模型的, 只有更瞭解 dapr 才能更好的使用和推廣它。慣例,歡迎 fork+star:

  https://github.com/sd797994/Oxygen-Dapr

  https://github.com/sd797994/Oxygen-Dapr.EshopSample

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/66in5CgDg1aOp_D5Pm7msg