狀態(State)+行為(Behavior)+郵箱(Mailbox)
基於Actor模型的CQRS、ES解決方案分享
開場白
大家晚上好,我是鄭承良,跟大家分享的話題是《基於Actor模型的CQRS/ES解決方案分享》,最近一段時間我一直是這個話題的學習者、追隨者,這個話題目前生產環境落地的資料少一些,分享的內容中有一些我個人的思考和理解,如果分享的內容有誤、有疑問歡迎大家提出,希望通過分享這種溝通方式大家相互促進,共同進步。
引言
- 話題由三部分組成:
- Actor模型&Orleans:在編程的層面,從細粒度-由下向上的角度介紹Actor模型;
- CQRS/ES:在框架的層面,從粗粒度-由上向下的角度介紹Actor模型,說明Orleans技術在架構方面的價值;
- Service Fabric:從架構部署的角度將上述方案落地上線。
- 群里的小伙伴技術棧可能多是Java和Go體系,分享的話題主要是C#技術棧,沒有語言紛爭,彼此相互學習。比如:Scala中,Actor模型框架有akka,CQRS/ES模式與編程語言無關,Service Fabric與K8S是同類平台,可以相互替代,我自己也在學習K8S。
Actor模型&Orleans(細粒度)
- 共享內存模型
多核處理器出現后,大家常用的並發編程模型是共享內存模型。
這種編程模型的使用帶來了許多痛點,比如:
- 編程:多線程、鎖、並發集合、異步、設計模式(隊列、約定順序、權重)、編譯
- 無力:單系統的無力性:①地理分布型②容錯型
- 性能:鎖,性能會降低
- 測試:
- 從坑里爬出來不難,難的是我們不知道自己是不是在坑里(開發調試的時候沒有熱點可能是正常的)
- 遇到bug難以重現。有些問題特別是系統規模大了,可能運行幾個月才能重現問題
- 維護:
- 我們要保證所有對象的同步都是正確的、順序的獲取多個鎖。
- 12個月后換了另外10個程序員仍然按照這個規則維護代碼。
簡單總結:
- 並發問題確實存在
- 共享內存模型正確使用掌握的知識量多
- 加鎖效率就低
- 存在許多不確定性
- Actor模型
Actor模型是一個概念模型,用於處理並發計算。Actor由3部分組成:狀態(State)+行為(Behavior)+郵箱(Mailbox),State是指actor對象的變量信息,存在於actor之中,actor之間不共享內存數據,actor只會在接收到消息后,調用自己的方法改變自己的state,從而避免並發條件下的死鎖等問題;Behavior是指actor的計算行為邏輯;郵箱建立actor之間的聯系,一個actor發送消息后,接收消息的actor將消息放入郵箱中等待處理,郵箱內部通過隊列實現,消息傳遞通過異步方式進行。
Actor是分布式存在的內存狀態及單線程計算單元,一個Id對應的Actor只會在集群種存在一個(有狀態的 Actor在集群中一個Id只會存在一個實例,無狀態的可配置為根據流量存在多個),使用者只需要通過Id就能隨時訪問不需要關注該Actor在集群的什么位置。單線程計算單元保證了消息的順序到達,不存在Actor內部狀態競用問題。
舉個例子:
多個玩家合作在打Boss,每個玩家都是一個單獨的線程,但是Boss的血量需要在多個玩家之間同步。同時這個Boss在多個服務器中都存在,因此每個服務器都有多個玩家會同時打這個服務器里面的Boss。
如果多線程並發請求,默認情況下它只會並發處理。這種情況下可能造成數據沖突。但是Actor是單線程模型,意味着即使多線程來通過Actor ID調用同一個Actor,任何函數調用都是只允許一個線程進行操作。並且同時只能有一個線程在使用一個Actor實例。
- Actor模型:Orleans
Actor模型這么好,怎么實現?
可以通過特定的Actor工具或直接使用編程語言實現Actor模型,Erlang語言含有Actor元素,Scala可以通過Akka框架實現Actor編程。C#語言中有兩類比較流行,Akka.NET框架和Orleans框架。這次分享內容使用了Orleans框架。
特點:
Erlang和Akka的Actor平台仍然使開發人員負擔許多分布式系統的復雜性:關鍵的挑戰是開發管理Actor生命周期的代碼,處理分布式競爭、處理故障和恢復Actor以及分布式資源管理等等都很復雜。Orleans簡化了許多復雜性。
優點:
- 降低開發、測試、維護的難度
- 特殊場景下鎖依舊會用到,但頻率大大降低,業務代碼里甚至不會用到鎖
- 關注並發時,只需要關注多個actor之間的消息流
- 方便測試
- 容錯
- 分布式內存
缺點:
- 也會出現死鎖(調用順序原因)
- 多個actor不共享狀態,通過消息傳遞,每次調用都是一次網絡請求,不太適合實施細粒度的並行
- 編程思維需要轉變
第一小節總結:上面內容由下往上,從代碼層面細粒度層面表達了采用Actor模型的好處或原因。
CQRS/ES(架構層面)
- 從1000萬用戶並發修改用戶資料的假設場景開始
- 每次修改操作耗時200ms,每秒5個操作
- MySQL連接數在5K,分10個庫
- 5 *5k *10=25萬TPS
- 1000萬/25萬=40s
在秒殺場景中,由於對樂觀鎖/悲觀鎖的使用,推測系統響應時間更復雜。
- 使用Actor解決高並發的性能問題
1000萬用戶,一個用戶一個Actor,1000萬個內存對象。
200萬件SKU,一件SKU一個Actor,200萬個內存對象。
- 平均一個SKU承擔1000萬/200萬=5個請求
- 1000萬對數據庫的讀寫壓力變成了200萬
- 1000萬的讀寫是同步的,200萬的數據庫壓力是異步的
- 異步落盤時可以采用批量操作
總結:
由於1000萬+用戶的請求根據購物意願分散到200萬個商品SKU上: 每個內存領域對象都強制串行執行用戶請求,避免了競爭爭搶; 內存領域對象上扣庫存操作處理時間極快,基本沒可能出現請求阻塞情況;
從架構層面徹底解決高並發爭搶的性能問題。 理論模型,TPS>100萬+……
- EventSourcing:內存對象高可用保障
Actor是分布式存在的內存狀態及單線程計算單元,采用EventSourcing只記錄狀態變化引發的事件,事件落盤時只有Add操作,上述設計中很依賴Actor中State,事件溯源提高性能的同時,可以用來保證內存數據的高可用。
- CQRS
上面1000萬並發場景的內容來自網友分享的PPT,與我們實際項目思路一致,就拿來與大家分享這個過程,下圖是我們交易所項目中的架構圖:
開源版本架構圖:
( 開源項目github:https://github.com/RayTale/Ray )
第二小節總結:由上往下,架構層面粗粒度層面表達了采用Actor模型的好處或原因。
Service Fabric
系統開發完成后Actor要組成集群,系統在集群中部署,實現高性能、高可用、可伸縮的要求。部署階段可以選擇Service Fabric或者K8S,目的是降低分布式系統部署、管理的難度,同時滿足彈性伸縮。
交易所項目可以采用Service Fabric部署,也可以采用K8S,當時K8S還沒這么流行,我們采用了Service Fabric,Service Fabric 是一款微軟開源的分布式系統平台,可方便用戶輕松打包、部署和管理可縮放的可靠微服務和容器。開發人員和管理員不需解決復雜的基礎結構問題,只需專注於實現苛刻的任務關鍵型工作負荷,即那些可縮放、可靠且易於管理的工作負荷。支持Windows與Linux部署,Windows上的部署文檔齊全,但在Linux上官方資料沒有。現在推薦K8S。
第三小節總結:
- 借助Service Fabric或K8S實現低成本運維、構建集群的目的。
- 建立分布式系統的兩種最佳實踐:
- 進程級別:容器+運維工具(k8s/sf)
- 線程級別:Actor+運維工具(k8s/sf)
上面是我對今天話題的分享。
參考:
- ES/CQRS部分內容參考:《領域模型 + 內存計算 + 微服務的協奏曲:乾坤(演講稿)》 2017年互聯網應用架構實戰峰會
- 其他細節來自互聯網,不一一列出
討論
T: 1000W用戶,購買200W SKU,如果不考慮熱點SKU,則每個SKU平均為5個並發減庫存的更新; 而總共的SKU分10個數據庫存儲,則每個庫存儲20W SKU。所以20W * 5 = 100W個並發的減庫存;
T: 每個庫負責100W的並發更新,這個並發量,不管是否采用actor/es,都要采用group commit的技術
T: 否則單機都不可能達到100W/S的數據寫入。
T: 采用es的方式,就是每秒插入100W個事件;不采用ES,就是每秒更新100W次商品減庫存的SQL update語句
Y: 哦
T: 不過實際上,除了阿里的體量,不可能並發達到1000W的
T: 1000W用戶不代表1000W並發
T: 如果真的是1000W並發,可能實際在線用戶至少有10億了
T: 因為如果只有1000W在線用戶,那是不可能這些用戶同時在同一秒內發起購買的,大家想一下是不是這樣
Y: 這么熟的名字
T: 所以,1000W在線用戶的並發實際只有10W最多了
T: 也就是單機只有1W的並發更新,不需要group commit也無壓力
Y: 嗯
問答
Q1:單點故障后,正在處理的 cache 數據如何處理的,例如,http,tcp請求…畢竟涉及到錢
A:actor有激活和失活的生命周期,激活的時候使用快照和Events來恢復最新內存狀態,失活的時候保存快照。actor框架保證系統中同一個key只會存在同一個actor,當單點故障后,actor會在其它節點重建並恢復最新狀態。
Q2:event ID生成的速度如何保證有效的scale?有沒有遇到需要后期插入一些event,修正前期系統運行的bug?有沒有遇到需要把前期已經定好的event再拆細的情況?有遇到系統錯誤,需要replay event的情況? A:1. 當時項目中event ID采用了MongoDB的ObjectId生成算法,沒有遇到問題;有遇到后期插入event修正之前bug的情況;有遇到將已定好的event修改的情況,采用的方式是加版本號;沒有,遇到過系統重新遷移刪除快照重新replay event的情況。
Q3:數據落地得策略是什么?還是說就是直接落地? A:event數據直接落地;用於支持查詢的數據,是Handler消費event后異步落庫。
Q4:actor跨物理機器集群事務怎么處理? A:結合事件溯源,采用最終一致性。
Q5:Grain Persistence使用Relational Storage容量和速度會不會是瓶頸? A:Grain Persistence存的是Grain的快照和event,event是只增的,速度沒有出現瓶頸,而且開源版本測試中PostgreSQL性能優於MongoDB,在存儲中針對這兩個方面做了優化:比如分表、歸檔處理、快照處理、批量處理。
Q6:SF中的reliable collection對應到k8s是什么? A:不好意思,這個我不清楚。
Q7:開發語言是erlang嗎?Golang有這樣的開發模型庫支持嗎? A:開發語言是C#。Golang我了解的不多,proto.actor可以了解一下:https://github.com/AsynkronIT/protoactor-go
Q8:能否來幾篇博客闡述如何一步步使用orleans實現一個簡單的事件總線 A:事件總線的實現使用的是RabbitMQ,這個可以看一下開源版本的源碼EventBus.RabbitMQ部分,博客的可能后面會寫,如果不996的話(笑臉)
Q9:每個pod的actor都不一樣,如何用k8s部署actor,失敗的節點如何監控,並借助k8s自動恢復? A:actor是無狀態的,失敗恢復依靠重新激活時事件溯源機制。k8s部署actor官方有支持,可以參考官方示例。在實際項目中使用k8s部署Orleans,我沒有實踐過,后來有同事驗證過可以,具體如何監控不清楚。
Q10:Orleans中,持久化事件時,是否有支持並發沖突的檢測,是如何實現的? A:Orleans不支持;工作中,在事件持久化時做了這方面的工作,方式是根據版本號。
Q11:Orleans中,如何判斷消息是否重復處理的?因為分布式環境下,同一個消息可能會被重復發送到actor mailbox中的,而actor本身無法檢測消息是否重復過來。 A:是的,在具體項目中,通過框架封裝實現了冪等性控制,具體細節是通過插入事件的唯一索引。
Q12:同一個actor是否會存在於集群中的多台機器?如果可能,怎樣的場景下可能會出現這種情況? A:一個Id對應的Actor只會在集群種存在一個。
Q13: 響應式架構 消息模式Actor實現與Scala.Akka應用集成 這本書對理解actor的幫助大嗎,還有實現領域驅動設計這本
A:這本書我看過,剛接觸這個項目時看的,文章說的有些深奧,因為當時關注的是Orleans,文中講的是akka,幫助不大,推薦具體項目的官方文檔。實現領域驅動這本書有收獲,推薦專題式閱讀,DDD多在社區交流。