Dapr | 雲原生的抽象與實現

騰訊雲原生發表於2021-04-21

引言

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

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

什麼是 Runtime

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

常見的 runtime 為程式提供的支援:

  • 語言 runtime(C/Goang…):作業系統互動,垃圾回收,併發控制等
  • Java runtime: 虛擬機器
  • 容器執行時:namespace,cgroup 等

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

特徵:
  • runtime 是在程式之外,不由程式編寫者提供
  • runtime 的生命週期通常和程式生命週期關聯

我們寫 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 上有了落地。
  • 網路:網路方面的需求 是 service Mesh 的主戰場,比如 istio 可以滿足這裡絕大部分需求,除了 pub/sub。
  • 狀態:包括資料的讀寫,狀態其實是非常難以管理的,涉及冪等,快取,資料流等等。
  • 繫結:主要是指和系統外部資源的互動。

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

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

原文:Multi-runtime Microservices Architecture

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 對外提供能力的基本單元,是對分散式能力的抽象和歸類,包括以下幾大類

  • service-to-service invocation
  • State management
  • Publish and subscribe
  • Resource bindings
  • Actors
  • Observability
  • Secrets

這些都是和應用開發息息相關的。 每一種 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: 通過標準化的方式暴露 building block
  • Building Block: 是能力的抽象
  • Components: 對接能力的實現

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

主要能力:

  • CRUD,包括批量操作
  • 事務
  • 併發:first-write-wins、last-write-wins
  • 一致性:最終一致、強一致性
  • 可插拔 (Pluggable state stores)

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

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

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

Publish and subscribe

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

主要能力:

  • 統一的訊息格式:Cloud Events
  • At-Least-Once guarantees( 訊息絕不會丟,但可能會重複傳遞)
  • 支援訊息過期時間(per message TTL)
  • 支援 topic 可見性配置

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

  • 最基本的計算單元,封裝了可以執行的行為和私有狀態
  • 通過信箱非同步通訊
  • 內部單執行緒
  • 虛擬的:不需要顯示建立,自動 GC

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,不需要考慮協議擴充套件。

一些重疊的功能點:

  • 服務通訊 mTLS
  • 遙測
  • 重試

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 生態將會更加壯大。

相關文章