1. 程式人生 > >基於Actor模型的CQRS、ES解決方案分享

基於Actor模型的CQRS、ES解決方案分享

開場白

大家晚上好,我是鄭承良,跟大家分享的話題是《基於Actor模型的CQRS/ES解決方案分享》,最近一段時間我一直是這個話題的學習者、追隨者,這個話題目前生產環境落地的資料少一些,分享的內容中有一些我個人的思考和理解,如果分享的內容有誤、有疑問歡迎大家提出,希望通過分享這種溝通方式大家相互促進,共同進步。

引言
  1. 話題由三部分組成:
  • Actor模型&Orleans:在程式設計的層面,從細粒度-由下向上的角度介紹Actor模型;
  • CQRS/ES:在框架的層面,從粗粒度-由上向下的角度介紹Actor模型,說明Orleans技術在架構方面的價值;
  • Service Fabric:從架構部署的角度將上述方案落地上線。
  1. 群裡的小夥伴技術棧可能多是Java和Go體系,分享的話題主要是C#技術棧,沒有語言紛爭,彼此相互學習。比如:Scala中,Actor模型框架有akka,CQRS/ES模式與程式語言無關,Service Fabric與K8S是同類平臺,可以相互替代,我自己也在學習K8S。
Actor模型&Orleans(細粒度)
  1. 共享記憶體模型

多核處理器出現後,大家常用的併發程式設計模型是共享記憶體模型。

這種程式設計模型的使用帶來了許多痛點,比如:

  • 程式設計:多執行緒、鎖、併發集合、非同步、設計模式(佇列、約定順序、權重)、編譯
  • 無力:單系統的無力性:①地理分佈型②容錯型
  • 效能:鎖,效能會降低
  • 測試:
    • 從坑裡爬出來不難,難的是我們不知道自己是不是在坑裡(開發除錯的時候沒有熱點可能是正常的)
    • 遇到bug難以重現。有些問題特別是系統規模大了,可能執行幾個月才能重現問題
  • 維護:
    • 我們要保證所有物件的同步都是正確的、順序的獲取多個鎖。
    • 12個月後換了另外10個程式設計師仍然按照這個規則維護程式碼。

簡單總結:

  • 併發問題確實存在
  • 共享記憶體模型正確使用掌握的知識量多
  • 加鎖效率就低
  • 存在許多不確定性
  1. 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例項。

  1. 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(架構層面)
  1. 從1000萬用戶併發修改使用者資料的假設場景開始

  1. 每次修改操作耗時200ms,每秒5個操作
  2. MySQL連線數在5K,分10個庫
  3. 5 *5k *10=25萬TPS
  4. 1000萬/25萬=40s

在秒殺場景中,由於對樂觀鎖/悲觀鎖的使用,推測系統響應時間更復雜。

  1. 使用Actor解決高併發的效能問題

1000萬用戶,一個使用者一個Actor,1000萬個記憶體物件。

200萬件SKU,一件SKU一個Actor,200萬個記憶體物件。

  • 平均一個SKU承擔1000萬/200萬=5個請求
  • 1000萬對資料庫的讀寫壓力變成了200萬
  • 1000萬的讀寫是同步的,200萬的資料庫壓力是非同步的
  • 非同步落盤時可以採用批量操作

總結:

由於1000萬+使用者的請求根據購物意願分散到200萬個商品SKU上: 每個記憶體領域物件都強制序列執行使用者請求,避免了競爭爭搶; 記憶體領域物件上扣庫存操作處理時間極快,基本沒可能出現請求阻塞情況;

從架構層面徹底解決高併發爭搶的效能問題。 理論模型,TPS>100萬+……

  1. EventSourcing:記憶體物件高可用保障

Actor是分散式存在的記憶體狀態及單執行緒計算單元,採用EventSourcing只記錄狀態變化引發的事件,事件落盤時只有Add操作,上述設計中很依賴Actor中State,事件溯源提高效能的同時,可以用來保證記憶體資料的高可用。

  1. 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。


第三小節總結:

  1. 藉助Service Fabric或K8S實現低成本運維、構建叢集的目的。
  2. 建立分散式系統的兩種最佳實踐:
  • 程序級別:容器+運維工具(k8s/sf)
  • 執行緒級別:Actor+運維工具(k8s/sf)

上面是我對今天話題的分享。

參考:

  1. ES/CQRS部分內容參考:《領域模型 + 記憶體計算 + 微服務的協奏曲:乾坤(演講稿)》 2017年網際網路應用架構實戰峰會
  2. 其他細節來自網際網路,不一一列出

討論

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多在社群交