Dapr | 雲原生的抽象與實現
- 2021 年 4 月 21 日
- 筆記
- Kubernetes介紹&性能優化系列
引言
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 生態將會更加壯大。