Dapr - 雲原生的抽象與實現

鍾華,騰訊雲高級工程師,Istio project member、contributor,專注於容器和服務網格,在容器化和服務網格生產落地方面具有豐富經驗,目前負責 Tencent Cloud Mesh 研發工作。

引言

Dapr 是微軟主導的雲原生開源項目,2019 年 10 月首次發佈,到今年 2 月正式發佈 V1.0 版本。在不到一年半的時間內,github star 數達到了 1.2 萬,超過同期的 kubernetes、istio、knative 等,發展勢頭迅猛,業界關注度非常高。

Dapr 這個詞是是 「Distributed Application runtime」的首字母縮寫,非常精煉的解釋了 dapr 是什麼:dapr 是一個爲應用提供分佈式能力的運行時。

什麼是 Runtime

Runtime 是一個抽象的概念,字面意思是程序運行的時候。一般是指用來支持程序運行的實現。描述的是程序正常執行需要的支持:庫、命令和環境等。

常見的 runtime 爲程序提供的支持:

容器運行時,就是容器運行起來需要的一系列程序和環境。比如如何使用 namespace 實現資源隔離,如何使用 cgroup 實現資源限制,這些都是容器運行時需要提供的實現。

特徵:

我們寫 java 程序的不需要寫 java 虛擬機;構建一個容器,通常不需要去寫 runc 的代碼。

 Distributed Application Runtime

Dapr 所提供的「分佈式應用運行時」,是應用程序運行所需分佈式能力的實現,這些能力涵蓋服務通信、數據持久化、外部 binding,pub-sub 等等。比如服務調用需要有容錯重試機制,比如一個數據持久化操作希望使用樂觀鎖,比如發佈消息是要求有投遞保證。

長期以來,這些功能的適配都是集成在業務代碼裏的。dapr 創新之處是將這些功能,從原來 application runtime 中拆分出來,作爲一個獨立的 runtime。dapr runtime 也滿足上面說到的 runtime 的特徵。

瞭解 service mesh 的同學可能會看出,這和 service mesh 使用的 sidecar 模式很類似,這是一種讓系統解耦、讓開發人員關注點分離的方式。但我們也很好奇,dapr 和 service mesh 有什麼關聯,這些越來越多的 sidecar 模型到底有什麼區別?(knative 也用到了 sidecar 模式)。因此,在深入 dapr 之前,我們先了解一個重要的理論背景:Multi runtime。

Multi Runtime

Multi runtime 是由 Red Hat 首席架構師 Bilgin Ibryam 提出的,實際上 multi runtime 和 dapr 並沒有直接的關係,multi runtime 的提出是在 dapr 開源之後。作者的文章重點對當今分佈式應用的需求做了歸類,並且分析了當前流行的雲原生項目是如何滿足這些分佈式需求,包括 kubernetes,istio,dapr 等,最後,作者對分佈式應用和中間件的未來發展,做了推導和預測,這就是 multi runtime。

分佈式應用的需求:

左邊的這些需求,在傳統軟件時代,是耦合在應用代碼裏的,但現如今,有越來越多的分佈式能力從應用中剝離,而剝離的方式也在逐漸變化,從最早期,這些能力從業務代碼剝離到依賴庫中,然後有一些特性剝離到平臺層(kubernetes)。而如今會有更多的非業務能力,剝離到 sidecar 中。

作者預測:理論上每個微服務可以有多個 runtime: 一個業務運行時,和多個分佈式能力運行時,但最理想的情況是,或者最可能出現的情況是:在業務之外的運行時合併爲一個,通過高度模塊化、標準化和可配置的方式,給業務提供所有分佈式能力。

原文:Multi-runtime Microservices Architecture[1]

Dapr

Dapr 是什麼

dapr is a portable,event-driven runtime that makes it easy for any developer to build resilient,stateless and stateful applications that run on the cloud and edge and embraces the diversity of languages and developer frameworks.

關鍵字:可移植,事件驅動,彈性,有狀態和無狀態,雲和邊端,語言無關,框架無關。

這些主要是 dapr 的願景,核心是要提供一個有標準,可配置,包含各種分佈式能力的運行時。

Dapr 架構

dapr 的設計是典型的分層架構,其核心理念,是利用抽象層來實現應用關注點的分離,用以降低分佈式應用的複雜性。

在 dapr 的架構中,核心的三個組成部分:API,Building Blocks 和 Components。

Dapr Building Blocks

這是 dapr 對外提供能力的基本單元,是對分佈式能力的抽象和歸類,包括以下幾大類

這些都是和應用開發息息相關的。每一種 building block 都是完全獨立的,應用可以按需調用。

我們可以對比下 dapr building blocks 和之前 multi runtime 提出的 4 大類 分佈式能力需求。其中 lifecycle 不屬於 runtime 範疇,lifecycle 能力通常是由平臺提供,目前雲原生領域基本上是被 kubernetes 壟斷,除此之外的 networking,state 和 binding 都包括在 dapr 的 building blocks 中。

Dapr Components

Components 提供和各種分佈式實現的對接,包括自建的,雲上的,邊緣等等。

理論上 building block 可以組合使用任意的 components,一個 component 也可以被不同的 building block 使用。比如 actor 和 state 都會使用 state component; 另一個例子,service invocation 會使用 name resolution 和 middleware component,而且不同的場景下,可以選擇不同的 component 實現。

Component 類型和實現:在實現層面,每一種 component 類型 定義了一系列接口(interface definition),每一種 component 類型 有多種 component 實現,他們都實現了 component 類型要求的接口(interface)。

Dapr API

應用如何能使用到這些分佈式能力,這是 dapr 最核心的設計,也是 dapr 應用和非 dapr 應用最關鍵的區別: dapr 利用標準 API 暴露各種分佈式能力。API 定義了應用所需的分佈式能力。dapr 提供兩種 API: HTTP1.1/REST 和 HTTP2/gRPC,兩者在功能上是等價的。這些 API 是平臺無關的,或者說是實現無關的,這是 dapr 能否流行的一個關鍵。

應用只需要按照 API 規範發起,不管是服務訪問,還是存儲,還是發佈消息到隊列裏,都是 HTTP 接口。不管是操作 redis 還是 mysql 都是一樣的 API。在應用看來,一切所需的能力,都可以用 HTTP 協議來表示,這些能力的獲取是標準化的,只要應用需要的分佈式能力不變,那應用的代碼就不需要改變。

將「分佈式原語」映射到 Http API 上,極大地減少了程序員心智的開銷。在應用代碼中不再需要引入相關的組件調用庫,不需要去封裝組件的具體調用方式,不需要對不同的實現做區分。

另外在用戶應用側,dapr 還提供了多種語言的 SDK,這些 SDK 的目的是用更便捷的方式來暴露 building Blocks 的 API,用更加語義化的方法調用,來封裝 Http/gRPC 的調用。

總結

API 調用是如何實現

一個存儲調用的例子:比如一個電商系統,需要持久化存儲,傳統的做法是,我們要先決策使用什麼存儲,mysql 或者 redis 等,我們需要在代碼裏引入相應的 SDK,編寫各異的實現,未來如果應用想要切換存儲類型,或者從本地存儲遷移到雲上,改動非常大。

假設這個系統的特徵是讀多寫少,那我們傾向於用樂觀鎖來更新數據。業務提出來的「用樂觀鎖控制併發寫入」這就是一個典型的分佈式需求,而這種需求的實現在不同的存儲系統中不盡相同,比如 mysql 是需要用戶顯式指定一個字段作爲版本信息,用戶寫操作是需要把版本信息傳回服務器,而 redis 樂觀鎖需要用戶指定在 redis server 端 watch 某個 key。類似的需求還有數據庫一致性,是使用最終一致性還是強一致性,各種存儲實現也不同。

如上圖所示,如果接入使用 dapr runtime,應用發起存儲調用非常簡單,不需要在應用代碼裏引入 redis 或者 mysql 的 SDK,也不用關心實際存儲使用是什麼通信協議,應用代碼裏只需要使用分佈式原語和 dapr runtime 通信,通信的協議是簡單的 Http 或者 gRPC,dapr runtime 去實現這些分佈式能力。

Service Invocation

主要能力:

在 kubernetes 中使用 dapr,dapr 會爲每個服務生成一個新的 service (以-dapr結尾),sidecar 之間的通信都是 gRPC,每個應用需要指定一個 app-id 用於服務發現,應用需要顯示的發起對 runtime API 的調用,沒有類似 mesh 的 iptables 透明攔截。

大家可以腦洞一下,如果 dapr 這種模式能大規模流行,那市面上大部分 RPC 是不是都不再需要了,如今大部分 RPC 雖然各有專長,但是大部分功能都是類似的,服務發現、編解碼、網絡傳輸,有的 RPC 框架還帶服務治理的能力。大部分能力目前都可以由 mesh 或者 dapr 這類 runtime 來提供,這也是一個明顯的趨勢。

State management

主要能力:

State 提供一致的鍵值****對存儲抽象,這裏不包括關係型或者其他類型的存儲。總的來說,在雲原生領域(以 kubernetes 和 etcd 爲代表),鍵值對存儲的適用範圍更廣。另外相比其他存儲類型,鍵值對存儲引擎的接口抽象更容易實現,即使是關係型數據庫,也能輕鬆的實現對鍵值對 API 的支持。

但仍然不是所有的存儲引擎都能提供等價的鍵值對存儲能力 (見 dapr 存儲實現差異 [2])。爲了保證應用程序的可移植性,這裏的確是需要一些適配工作。比如像 Memcached,Cassandra 這些是不支持事務的,而很多數據庫也不能提供基於 ETag 的樂觀鎖能力。

對於併發控制,在 API 層,dapr 利用 HTTP ETags 來實現併發控制,類似 kubernetes 對象的 resource version,具體地:dapr 在返回數據時,會帶上 Etag 屬性。如果用戶需要使用樂觀鎖做更新操作,請求中需要帶回 Etag,只有當 Etag 和服務器上數據的相同時,更新操作纔會成功。如果更新操作沒有帶上 Etag,那併發模式將是 last-write-wins

Publish and subscribe

使用發佈和訂閱模式,微服務間可以充分的解耦。

主要能力:

Runtime 不僅可以做能力的對接適配,還可以做增強,這是一個例子:如果消息組件原生支持消息有效期,那 runtime 直接轉發 TTL 相關操作,過期的行爲由組件直接控制,而對於那些不支持消息有效期的組件,dapr 會在 runtime 中補齊相關的過期功能。(CloudEvent 裏有 expiration)

兩種訂閱方式

二者提供的功能是一致的。外部聲明方式需要多維護一個 CRD 對象,適合訂閱者或訂閱主題經常發生變化的場景,這樣在調整時不需要改應用代碼。應用編碼方式剛好相反,訂閱配置寫死在代碼裏,適合訂閱主題不需要動態調整的場景。

Bindings

Bindings 其實和之前的 pub/sub 非常類似,也是利用異步通信傳遞消息。它倆主要的區別是:pub/sub 主要面向的是 dapr 內部應用,而 bindings 主要解決的和外部依賴系統的輸入輸出。

實際上它倆下層的 components 有很多是重疊的,比如說 kafka,redis 既可以作爲內部消息傳遞,也可以作爲外部消息傳遞。pub/sub 基本可以等同於消息隊列,但 bindings 主要是處理事件(trigger handler),比如 twitter 關鍵字事件,比如 github webhooks 等。

Actor

Actor 是一種併發編程的模型,Actor 表示的是一個最基本的計算單元,封裝了可以執行的行爲和私有狀態。actor 之間相互隔離,它們並不互相共享內存,也就是說,一個 actor 能維持一個私有的狀態,並且這個狀態不可能被另一個 actor 所改變。在 actor 模型裏每個 actor 都有地址 (信箱),所以它們才能夠相互發送消息。每個 actor 只能順序地處理消息。單個 actor 不考慮併發。

Dapr 中 actor 是虛擬的,它們並不一定要常駐內存。它們不需要顯式創建或銷燬。dapr actor runtime 在第一次接收到該 actor ID 的請求時自動激活 actor。如果該 actor 在一段時間內未被使用,那麼 runtime 將回收內存對象。如果以後需要重新啓動,它還將還原 actor 的一切原有數據。

Actor placement service 爲系統提供了 actor 分發和管理,placement 會跟蹤 actor 類型和所有實例的分區,並將這些分區信息同步到每個 dapr 實例中,並跟蹤他們的創建和銷燬。

Middleware Pipelines

注意 middleware pipelines 是一個 component 類型,而不是 building block。

Dapr 官方提供流量管控的能力比較弱,和 istio 相比的話,目前 dapr 只有重試,加密等少數的管控能力,但 dapr 提供一個擴展的方式:這就是 middleware pipelines,用戶可以按需編寫不同的實現,並把他們級聯起來使用。

其實這種方式在各種編程語言 web 框架中非常常見,只是叫法不同,有的叫裝飾者模型,有的叫洋蔥模型,其實模式都是一樣:請求在路由到用戶代碼之前,會先按序執行 middleware pipelines,請求經過應用處理後,再按相反順序執行上述 middleware pipeline。通常在前序中對 request 做相應的增強處理,在後續中對 response 做增強處理。

咋一看這可能是一個不太起眼的功能,但和傳統 web 框架的 middleware 不一樣, dapr runtime 本身是在應用進程之外,所以不存在語言限制的問題。這使得 middleware 提供的功能可以跨語言共享。比如 dapr 原生沒有提供限流和自定義鑑權的功能(呼聲很高的 2 個場景),我們可以遵循 middleware 的接口按需實現,然後植入 dapr 運行時中。

部署模式

Dapr 使用 sidecar 模式來暴露 building blocks 的能力,這裏的 sidecar 除了包括 sidecar container 外,還可以是 sidecar process。

在非容器化環境中,用戶應用和 dapr runtime 都是獨立的進程;而在 kubernetes 這種容器化環境中,dapr runtime 作爲 sidecar container 注入到 業務 pod 中,這和 service mesh sidecar 模式是一致的。

控制面

整個控制面還是一個微服務。和 istio 早期有點類似。

Sidecar injector:利用 kubernetes mutating webhook 給業務 pod 注入 dapr runtime sidecar 容器,以及運行所需的環境變量,啓動參數等。包括連接控制面 operator 的地址(control-plane-address)等。

Operator:會 list watch 用戶定義的 Component 資源,並下發給數據面的 dapr runtime。數據面 runtime 會持有一個 OperatorClient 去 連接控制面 Operator。

Sentry: 爲 dapr 系統中的工作負載提供基於 mtls 的安全通信。mtls 能強制通信雙方進行身份認證,同時在認證之後保證通信都走加密通道。Sentry 的功能很類似 istio 裏的 Citadel (目前已經合併到 istiod)。在整個過程中,sentry 充當證書頒發機構(CA),處理 dapr sidecar 發起的簽署證書請求,另外還要負責證書的輪轉。除了 dapr sidecar 之間的自動 mTLS 之外,sidecar 和 dapr 控制面服務之間也是強制性的 mTLS。

Placement:用於跟蹤 actor 的類型和實例分佈,並同步給數據面的 runtime。

性能

sidecar 模式會帶來額外的性能開銷。以我們使用 service mesh 的經驗來看,這種模式的性能開銷主要是 2 個方面,一個是流量經過 sidecar 的攔截、流量管控和轉發損耗,另一個是 sidecar 需要從控制面同步管理數據,sidecar 需要存儲和處理這些數據,這可能會給數據面內存和 CPU 帶來壓力,特別是大規模場景下。

在官方對 dapr V1.0 的性能測試數據看: 在不開啓 mtls 和 遙測的情況下,延遲 P90 大概增加 1.4 ms,在開啓 mtls 和 0.1 tracing rate 情況下,P90 數據大概還會增加了 3ms 左右。

這個數據要比 istio 好,dapr sidecar 沒有太多的流量管控和修改的功能,也沒有使用 iptables 攔截,開銷相對較小。爲了儘可能提高通信效率,dapr sidecar 之間的通信固定使用 gRPC 協議。而且 dapr 從數據面同步的數據量也非常少,所以也不會有類似 istio 場景下頻繁 reload xDS 的問題。

但相比 service mesh,dapr sidecar 管控了更多的流量類型,比如狀態存儲,應用系統對這類流量的延遲變化更加敏感,用戶在接入 dapr 之前需要慎重評估。目前 dapr 還在項目初期,業界還沒有太多大規模,精細化的落地測評。

和 Service Mesh 比較

二者都使用了 sidecar 模式,功能上也有重疊,理論上二者是可以共存的,雖然同時使用這 2 種技術可能不是一個最優的方案(開銷和維護成本)。

Service mesh 定位偏向於服務級別的網絡基礎設施層。Service mesh 做了很多努力來讓 mesh 層對應用層透明,期望服務能平滑的遷移。理想的情況下,應用開發者應該不感知 mesh 層的存在,所以 mesh 面向的主要是系統運維人員。

Dapr 旨在提供應用所需要的分佈式能力,這些能力是和業務的正常運作息息相關的, dapr 提供的能力不是透明的,是需要應用顯示的調用,所以 dapr 主要面向的開發人員。

服務調用方面,mesh 使用透明轉發,對應用程序更友好,但是支持的協議有限,mesh 對七層的協議擴展一直是一個難點。而在 dapr 裏必須顯示發起調用,所有調用都是會轉爲 gRPC,不需要考慮協議擴展。

一些重疊的功能點:

Istio 和 Dapr 進一步比較

istio 有強大的流量管控能力,這些是 dapr 不具備的。在 istio 數據面中,每個 envoy 都同步獲取了整個網格內服務信息(通過 xDS)的全貌,包括服務所有的 endpoint IP,以及這些 endpoint 的特徵,這讓 istio 可以實現很多複雜的負載均衡場景。

而 dapr sidecar 沒有實現類似的能力,在 kubernetes 平臺下,dapr 應用間的服務互訪,還是依賴 kubernetes service 提供的隨機負載均衡。這是 dapr 的短板,dapr runtime 不感知其他 endpoint 的信息, 因此 dapr 甚至不能提供 round robin 的負載均衡策略。

Dapr 的核心功能是爲應用提供了標準化的分佈式能力,諸如狀態管理,訂閱發佈,Actor 等等,這些領域 istio 基本不涉及。

另外在遙測領域,二者也有區別,istio 的遙測主要是集中在服務間調用,而 dapr 除了能觀察服務間調用,還把觀測範圍擴展到了 pub/sub 領域,這得益於 dapr 使用 cloud events 格式來傳遞 pub-sub 消息,這樣 dapr 可以將遙測信息寫入 cloud events 進行傳遞。

另外目前 dapr 在 kubernetes 的控制面是微服務,而 Isito 控制面已經是一個單體,未來 dapr 控制面有可能也會合併成一個單體。

總結

雖然前面我們分析了 dapr 這種 multi runtime 出現的背景和趨勢,但仍不得不說 dapr 的設計非常的新穎。dapr 的創新之處在於提供標準化的分佈式能力 API,這一點既是開發人員非常歡迎的模式,但也是業務接入最大的挑戰,因爲這涉及到項目的改造甚至重寫。另外,dapr 還提供了良好的實現擴展層,目前官方已經實現了大量主流中間件的的接入 ,另外 Azure 自家的不少雲產品都已經實現了 dapr compatible。

我想應該有不少程序員都做過這樣的「美夢」: 我不想面對各種依賴組件複雜的差異,我只想面向接口編程、面向抽象編程。如今 dapr 把這種理想化的架構模式初步實現了!這也是爲什麼 dapr 目前雖然還不是很成熟,但已經吸引了大量開發者的關注。接下來隨着社區的積極投入,dapr 生態將會更加壯大。

參考資料

[1] Multi-runtime Microservices Architecture: (https://www.infoq.com/articles/multi-runtime-microservice-architecture/)

[2] 見 dapr 存儲實現差異: (https://docs.dapr.io/operations/Components/setup-state-store/supported-state-stores/)

[3] dapr docs:( https://docs.dapr.io/)

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