2019 年,微軟開源了 Dapr 項目。2021 年,螞蟻參照 Dapr 思想開源了 Layotto 項目。如今,螞蟻已落地 Layotto,服務了很多應用。從理想落地到現實的過程中,我們遇到了不少問題,也對項目做了很多改變。回過頭再看,如何看待 Dapr、Layotto 這種多運行時架構?我們能從中學到什麼?
本次我將從以下幾個方面,分享螞蟻在落地多運行時架構之後的思考:
如何看待「可移植性」
多運行時架構能帶來哪些價值
與 Service Mesh、Event Mesh 的區別
如何看待不同的部署形態
如果你熟悉 Multi-Runtime、Dapr 和 Layotto 的概念,可以跳過這一章節,直接進入下一章節。
Multi-Runtime 是一種服務端架構思路,如果用一句話來概括,就是把應用里的所有中間件挪到 Sidecar 里,使得「業務運行時」和「技術運行時」分離開。
更詳細的解釋如下:
具體細節可以詳閱《Multi-Runtime Microservices Architecture》和《Mecha:將 Mesh 進行到底》。
Dapr 的全稱是「Distributed Application Runtime」,即「分布式應用運行時」,是一個由微軟發起的開源項目。
Dapr 項目是業界第一個 Multi-Runtime 實踐項目,Dapr 的 Sidecar,除了可以和 Service Mesh 一樣支持服務間通訊,還可以支持更多的功能,如 state(狀態管理)、pub-sub(消息通訊),resource binding(資源綁定,包括輸入和輸出)。Dapr 將每種功能抽象出標準化的 API(如 state API),每個 API 都有多種實現,比如用戶可以面向 state API 編程,但是可以隨意切換存儲組件,今年用 Redis,明年改成用 MongoDB,業務代碼不用改。
如果之前沒有接觸過 Dapr,更詳細的介紹可以閱讀《Dapr v1.0 展望:從 Service Mesh 到雲原生》這篇文章。
Layotto 是由螞蟻集團 2021 年開源的一個實現 Multi-Runtime 架構的項目,核心思想是在 Service Mesh 的數據面(MOSN)里支持 Dapr API 和 WebAssembly 運行時,實現一個 Sidecar 同時作為 Service Mesh 數據面、多運行時 Runtime、FaaS 運行時。項目地址為:https://github.com/mosn/layotto
以上是本文背景,接下來是本次主題分享。
社區比較關注 Dapr API 的「可移植性」,但在落地過程中,我們不禁反思:你真的需要這種「可移植性」嗎?
數據庫領域曾出現過一個有趣的討論:同一個數據庫能否適用於所有場景,滿足所有需求?比如,一個數據庫能否同時支持 OLAP+OLTP+ACID 等等需求?
今天,我們在建設 Dapr API 的過程中也遇到了有趣的問題:在某個產品領域(比如消息隊列),能否定義一套「標準 API」同時適用於所有的消息隊列?
當然,這兩個問題不能混為一談:即使是兩種不同類型的數據庫,比如兩個數據庫,一個只做 OLAP,另一個只做 OLTP,它們都可以支持 SQL 協議。兩個差距那麼大的數據庫都能用同樣的協議,我們有理由相信:在特定領域,設計一個適用於所有產品的「標準 API」是可行的。
可行,但現在還不完全行。
現在的 Dapr API 還比較簡單,簡單場景足以勝任,但在複雜的業務場景下,做不到「幫助應用 Write once,run on any cloud」。對這個問題,敖小劍老師的文章《死生之地不可不察:論 API 標準化對 Dapr 的重要性》有過詳細描述,大意是說:
1. 現在的 Dapr API 比較簡單,在生產落地的時候滿足不了複雜需求,於是開發者只能添加很多自定義的擴展字段,在 Sidecar 的組件里做特殊處理。比如下面是用 State API 時候的一些自定義擴展字段:
(圖片摘自敖小劍老師的文章)
這些自定義的擴展字段會破壞可移植性:如果你換一個組件,新組件肯定不認識這些字段,所以你得改代碼。
2. 之所以出現這個問題,背後的根本原因是 Dapr API 的設計哲學。社區在設計 Dapr API 時,為了可移植性,設計出的 API 傾向於「功能交集」。比如在設計 Configuration API 時,會考察各種配置中心 A、B、C,如果 A、B、C 都有同一個功能,那麼這個功能才會出現在 Dapr API 中:
然而,在現實世界中,人們的需求可能是 A 和 B 的交集,B 和 C 的交集(如下圖紅色部分),而不是 A、B、C 的交集:
或者更常見的是,用戶的需求是「B 的所有功能」,其中必然包括一些 B 獨有的功能,Dapr API 無法覆蓋:
3. Dapr API 有一定的侵入性
Dapr 提供「標準 API」、「語言 SDK」和「Runtime」,需要應用進行適配(這意味着老應用需要進行改造),侵入性比較大。
因此 Dapr 更適合新應用開發(所謂 Green Field),對於現有的老應用(所謂 Brown Field)則需要付出較高的改造代價。但在付出這些代價之後,Dapr 就可以提供跨雲跨平台的可移植性,這是 Dapr 的核心價值之一。
這些聽起來是解決不了的問題。那怎麼辦?
在設計 API 時,常常出現類似的討論:
等一等……你真的需要從 Redis 換成 Memcached 嗎?
你真的需要這種「可移植性」嗎?
不需要吧!如果你的應用是面向 Redis 編程的,那它天生就能部署到不同的雲上,因為每個雲環境都有託管 Redis 服務。如果沒有這種服務,你可以自己部署一個 Redis,讓它有。
而且不止是 Redis,其他開源產品也可以類似操作。
為了讓討論更具體,讓我們把應用依賴的基礎設施協議劃分成兩類:可信協議與私有協議。
指在某個領域影響力比較大的協議,衡量標準是:有託管服務的雲環境 >=k(k 是某個讓你有安全感的數字,比如 3,5)
比如 Redis 的協議,基本可以認為是和 SQL 一樣的事實標準了,各個雲廠商都提供了 Redis 託管服務;再比如 MySQL 協議,各個雲廠商都會提供兼容 MySQL 協議的數據庫託管服務。
擔心要從 Redis 換成別的緩存產品,就像是擔心「假如我今天引入了 Sidecar,如果以後 Sidecar 架構不流行了,我要去掉 Sidecar 怎麼辦」,或者「假如我今天引入了 Spring Cloud,以後其他框架火了,我要換成別的框架怎麼辦」。那一天當然會出現,但是大部分業務都活不到那一天,如果能,恭喜你,到那時你會有足夠的資源做重構。
比如閉源產品的協議,或者影響力小的開源產品的協議,衡量標準是:有託管服務的雲環境<k。
舉個例子,螞蟻內部的 MQ 是自建 MQ,使用私有協議,業務代碼依賴了這種私有協議就不好部署到別的雲環境了,所以適合用標準化 API 包一層。
再比如,你在調研接入某個阿里雲提供的 MQ,但是發現這個 MQ 的 API 是阿里雲獨有的,別的雲廠商不提供這種服務,如果你害怕被阿里雲綁定,最好用標準化 API 把這個私有 MQ API 包一層。
讀到這,你應該明白我想說的了:
既然「可移植性」這個問題太難了,那就讓我們弱化一下需求,先解決一些更簡單的問題:「弱移植性」。
「可移植性」這個需求太模糊了,我們先明確下需求。我們可以把可移植性分成多個等級:
這是常見狀態:比如某公司內部有一套自研消息隊列系統「XX MQ」,有一個「xx-mq-java-sdk」供業務系統引入。當業務系統想要上雲 / 換雲部署時,由於雲上沒有「XX MQ」,需要換一個 MQ(比如換成 RocketMQ),業務系統需要做重構。
社區有一些通過 sdk 做跨平台的方案,屬於這個級別。比如攜程開源的 Capa 項目,比如騰訊開源的 Femas 項目。
社區的最終目標是 level 4,但是上文已述,現在還沒法完美實現,存在種種問題。對於需要快速落地,解決業務問題的商業公司,現在能實現的目標是:追求 level 2 的可移植性,部分場景可以達到 level 3。這就足夠解決業務問題了。
比如分布式緩存場景,螞蟻在 MOSN 里自建了一套分布式緩存中間件支持 Redis 協議訪問,如果你相信 Redis 協議是具有可移植性的,那麼應用通過 Redis 協議和 MOSN 通信即可,沒必要強行遷移到 Dapr 的「State API」上。在這種情況下,標準化 API 只是作為補充。
如果我們把目標定為 level 3,那麼 Runtime 對外暴露的「兼容層」協議應該是多種多樣的,包括各種領域的可信協議(比如 Redis 協議、MySQL 協議、AWS S3 協議等),以及 Dapr 風格的標準化 API。
由此,我們可以得出兩個觀點:
現在我們回答最開始提出的問題:
答案是:逐漸演進,先考慮從 level 2 演進到 level 3。
為了實現 level 3,我們需要:
放棄面向「功能交集」的設計,改為面向「功能並集」做設計
在 Sidecar 直接支持各種「可信協議」
而為了實現最終的 level 4,我們需要:
標準化 API 是完備的「功能並集」,保證覆蓋到所有的業務場景:
有一套「feature 發現機制」,應用在部署時和基礎設施協商「我需要哪些 feature」,基礎設施根據應用的需求自動綁定組件
本文不再展開。
除了標準化 API,實踐中 Runtime 架構更大的價值在於以下幾個方面:
一個有趣的觀察是:以前 Mesh 的概念強調「代理」,因此一些基礎設施產品想把自己的代碼邏輯也「下沉」進 Sidecar 時可能會遭到 Mesh 團隊的拒絕,或者能「下沉」進去,但是實現的比較 hack,並不規範;而有了 Runtime 的概念後,各種產品把代碼邏輯挪到 Sidecar 行為就合理化了。
這裡說的「下沉」,是指「把應用依賴的公共組件從應用里挪到 Sidecar 里」,分離核心業務邏輯和技術部分。好處就太多了,比如:
Service Mesh 宣傳的好處之一是讓多語言應用復用流量治理類的中間件,現在 Runtime 強調把更多的中間件放進 Sidecar,意味着有更多的中間件能夠被多語言應用復用。比如,以前的中間件都是為 Java 開發的,C++ 用不了,現在可以讓 Node.js/Python/C++ 語言的應用通過 gRPC 調 Sidecar,復用中間件。
原先微服務應用的框架比較重,比如有和配置中心建連、初始化、緩存預熱之類的邏輯,現在這些啟動邏輯都挪到 Runtime 里。當應用或者函數需要擴容時,可以復用原有 Runtime,不需要再做一遍類似的建連預熱動作,從而達到啟動加速的效果。
這個就是 Mesh 一直講的好處:有了 Sidecar 後,不需要天天催促各個業務方升級 sdk,提高了基礎設施的迭代效率。
除了基礎設施,一些業務邏輯也有放進 Sidecar 的訴求,例如處理用戶信息等邏輯。
讓業務邏輯放進 Sidecar 需要保證隔離性,去年嘗試了用 WebAssembly 來做,但是不太成熟,不敢在生產中使用,今年會嘗試其他方案。
在「下沉」的過程中,標準化 API 更多的是起到約束「私有協議」的作用,比如:
限制私有協議的通信模型
設計私有協議時(Layotto 支持「API 插件」功能,允許擴展私有的 gRPC API),需要證明「這個私有協議在其他雲上部署時,存在一個能切換的組件」
作為設計私有協議的指導:參照着標準化 API 去設計私有協議,有理由相信設計出來的協議在換雲部署時,能達到 level 2 可移植性
Dapr 的 InvokeService(用來做 RPC 調用的 API)設計的比較簡單,也有一些不足,在實際 RPC 場景中,Layotto 調整了它的定位,作為 Service Mesh 的輔助:
已有的 Java 微服務的 RPC 流量還是通過 Service Mesh(MOSN)進行轉發,而對於其他語言的微服務,或者其他協議棧的微服務,可以通過 gRPC 調用 Sidecar,由 Sidecar 幫忙做協議轉換,然後把流量接入已有服務體系。
比如很多語言沒有 Hessian 庫,可以通過 gRPC 調 Layotto,Layotto 幫忙做 Hessian 序列化,然後將流量接入 MOSN。
(業界也有一些做多語言微服務打通的項目,比如 dubbogo-pixiu 項目,區別是通過網關的形式部署)
Serivce Mesh 和 Event Mesh 的區別是什麼?網上的說法是 Event Mesh 處理異步調用的流量,Service Mesh 處理同步調用。
Service Mesh 和 Dapr 的區別是什麼?網上的說法是 Service Mesh 是代理,Dapr 是運行時,要抽象 API,做協議轉換。
但是,隨着落地演進,我們漸漸發現這些技術概念的邊界變得很模糊。
如下圖,Layotto 這個 Sidecar 支持了各種協議,好像已經「非驢非馬」了:不只是 Dapr 式的對外暴露標準化 http/gRPC API,抽象分布式能力,也包括 Service Mesh 式的流量攔截、代理轉發,能處理同步調用、異步調用,能處理 Redis 等開源協議的請求,好像把 Event Mesh 的事情也做了,已經變成了一種混合模式的 Sidecar:
所以,如何劃分 Serivce Mesh,Event Mesh 和 Multi-Runtime 的邊界?
個人觀點是,可以把 Dapr 的「標準化 API」看做「Sidecar 增強」。比如「InvokeService API」可以看成「Service Mesh 增強」,「Pubsub API」可以看成是「Event Mesh 增強」,「State API」可以看成「數據中間件增強」,這裡說的數據中間件包括緩存流量轉發和 DB Mesh。從這種角度看,Layotto 更像是 Sidecar 里的「API 網關」。
目前的架構存在一個問題:Runtime 是個巨石應用。
不管是 Dapr 還是 Layotto,都傾向於承載所有和業務無關的功能。
如果你把 Runtime 類比成操作系統的內核,那麼 API 這層就是系統調用,負責抽象基礎設施,簡化編程,而不同的組件類似於驅動,負責把系統調用翻譯成不同基礎設施的協議。Runtime 把所有組件都放在一個進程里,類似於「宏內核」的操作系統把所有子模塊都塞在一起,變成了巨石應用。
巨石應用有什麼問題?模塊間互相耦合,隔離性不好,穩定性降低。比如之前就有研究指出 Linux 中大部分的代碼是驅動,而且很多驅動是「業餘玩家」寫的,穩定性不好,驅動寫的有問題是 kernel 崩潰的主要原因。同樣的,如果 Dapr 或者 Layotto 的一個組件出現 bug,會影響整個 Sidecar。
怎麼解決巨石應用的問題呢?拆!一個思路是把 Runtime 按模塊拆分,每個模塊是一個 Container,整個 Runtime 以 DaemonSet 的形式部署:
這種方案就像操作系統的「微內核」,不同子模塊之間有一定的隔離性,但相互通信的性能損耗會高一些。比如 Event Mesh 容器想要讀取配置中心的配置時,就需要通過網絡調用 Configuration 容器;如果調用頻率過高,就要考慮在 Event Mesh 容器里做一些配置緩存,可能最後每個容器都要做一套緩存。
那麼應該選擇單容器 Runtime 還是多容器 Runtime 呢?這就像操作系統選擇「宏內核」還是「微內核」架構,全看取捨。巨石應用的好處是子模塊之間互相通信性能好,缺點是緊耦合,隔離性不好;如果把 Runtime 拆成多個 Sidecar 則剛好相反。
目前,Dapr 和 Layotto 都是單容器 Runtime。
一個可能的拆分方案是:將 Runtime 按能力「垂直拆分」成多個容器,比如一個容器負責狀態存儲,一個容器負責異步通信等等,容器間通信通過 eBPF 做優化。不過目前還沒看到這樣做的項目。
優化點 1:啟動應用時,需要先啟動 Sidecar 容器,再啟動應用容器。能否讓應用啟動加速?
直覺上想,如果能讓新啟動的應用(或函數)復用已有的 Runtime,就能省掉一些初始化動作,加速啟動。
優化點 2:能否減少 Runtime 的資源占用?
每個 Pod 都有一個 Sidecar 容器,假如一個節點有 20 個 Pod,就得有 20 個 Sidecar,在大規模集群里光是 Sidecar 就要占用很多內存。
能否減少 Runtime 的資源占用?
直覺上想,如果能讓多個容器共享同一個代理(而不是每個容器獨享一個代理),就能減少資源占用。
上述兩點看起來都可以通過「讓多個容器共享同一個代理」來做優化。但事情真有那麼簡單嗎?
其實 Service Mesh 社區有過很多關於數據面部署形態的爭論,大致有以下幾種方案:
Sidecar 模式,每個應用獨享一個代理
(圖片來自<eBPF for Service Mesh? Yes, but Envoy Proxy is here to stay>)
節點上所有 Pod 共享同一個代理
(圖片來自<eBPF for Service Mesh? Yes, but Envoy Proxy is here to stay>)
不需要代理進程,用 eBPF 處理流量
很優雅,但功能有限,滿足不了所有需求。
節點上每個 Service Account 共享一個代理
(圖片來自<eBPF for Service Mesh? Yes, but Envoy Proxy is here to stay>)
混合模式:輕量 Sidecar+ 遠端代理
(圖片來自<eBPF for Service Mesh? Yes, but Envoy Proxy is here to stay>)
上面幾種方案看起來都行,只是取捨問題,但是到了 Runtime 這裡,情況就變了!
情況 1:集群里有各種各樣的中間件,各種各樣的基礎設施
如果集群里有各種各樣的中間件,各種各樣的基礎設施,那還是別用「節點上所有 Pod 共享同一個代理」的模型了。
舉個例子,某集群里有各種各樣的 MQ,如果節點上所有 Pod 共享同一個 Runtime,Runtime 事先不知道 Pod 會用什麼 MQ,所以它必須在編譯時帶上所有 MQ 組件。每次新建一個 Pod 時,這個 Pod 要動態把配置傳給 Runtime,告訴 Runtime 它要用哪個 MQ,然後 Runtime 再根據配置去和相應的 MQ 建立連接。
比如下圖,某個節點上,Pod 1、Pod 2、Pod 3 分別使用 RocketMQ、Kafka、ActiveMQ,這時新啟動了一個 Pod 4,Pod 4 告訴 Runtime 它很有個性,它要用 Pulsar!於是 Runtime 就得去和 Pulsar 建連,做一些初始化動作。所以,Pod 4 啟動並沒有「加速」,因為它沒能復用之前已有的連接。
這種情況下,共享 Runtime 並不能幫助應用啟動加速,無法復用和後端服務器的連接數,雖然能省一些內存,但帶來了一些缺點:增加了複雜度,降低了隔離性等等。
如果強行把 Sidecar 模型的 Runtime 改成共享代理,有用,但投入產出比不高。
情況 2:集群里基礎設施的技術棧比較統一
在這種情況下,共享代理模型可能有一定價值。
比如,某集群只用一種 MQ,RocketMQ。假如使用共享代理模型,某個節點上 Pod 1、Pod 2、Pod 3 已啟動,這時新啟動一個 Pod 4 也要用 RocketMQ,此時就可以復用已有的一些元數據,甚至有可能復用和 MQ 服務器的連接。
這種情況下,共享代理模型的好處有:
應用啟動加速,復用和後端服務器的連接
不過,所謂「啟動加速」也是要看情況的,比如通過優化讓 Runtime 啟動快了 2 秒,但是應用啟動卻要 2 分鐘,那麼優化 2 秒其實並沒有多大用處。尤其是有很多 Java 應用的集群,大部分 Java 應用啟動不快,這點優化價值有限。所以,啟動加速在 FaaS 場景會比較有用。如果函數本身啟動、加載速度較快,優化幾秒還是很有價值的。
提高資源利用率,不用部署那麼多 Sidecar 了
本文討論了 Layotto 落地之後,關於 Multi-Runtime 架構「可移植性」、落地價值以及部署形態等方面的思考。且本文的討論不限定於某個具體項目。
作者簡介
周群力,目前在螞蟻中間件團隊負責 Layotto 項目的開發,以及 Layotto 和 SOFAStack 開源社區的建設。Dapr 貢獻者,Dapr sig-api 的 Co-chair。個人 GitHub:https://github.com/seeflood
參考鏈接:
Multi-Runtime Microservices Architecture:https://www.infoq.com/articles/multi-runtime-microservice-architecture/
Mecha:將 Mesh 進行到底:https://mp.weixin.qq.com/s/sLnfZoVimiieCbhtYMMi1A
從 Service Mesh 到雲原生:https://mp.weixin.qq.com/s/KSln4MPWQHICIDeHiY-nWg
Dapr 項目地址:https://github.com/dapr/dapr
Layotto 項目地址:https://github.com/mosn/layotto
Capa 項目地址:https://github.com/capa-cloud/cloud-runtimes-jvm
Femas 項目地址:https://github.com/polarismesh/femas
十萬億條消息背後的故事
忍受不了糟糕的工作氛圍,我退出了 Google WebAssembly 團隊
Gitee關閉部分開源倉庫:先審核再上線;技術團隊20天開發出App後集體被裁,負責人怒用公司公號發文祝「早日倒閉」 | Q資訊
獨家專訪字節跳動開源委員會:定位「資源中台」,不會為開源設立強KPI
點個在看少個 bug👇