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

開場白

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

引言
  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. 共享內存模型

多核處理器出現後,你們經常使用的併發編程模型是共享內存模型。程序員

共享內存模型圖

這種編程模型的使用帶來了許多痛點,好比:github

  • 編程:多線程、鎖、併發集合、異步、設計模式(隊列、約定順序、權重)、編譯
  • 無力:單系統的無力性:①地理分佈型②容錯型
  • 性能:鎖,性能會下降
  • 測試:
    • 從坑裏爬出來不難,難的是咱們不知道本身是否是在坑裏(開發調試的時候沒有熱點多是正常的)
    • 遇到bug難以重現。有些問題特別是系統規模大了,可能運行幾個月才能重現問題
  • 維護:
    • 咱們要保證全部對象的同步都是正確的、順序的獲取多個鎖。
    • 12個月後換了另外10個程序員仍然按照這個規則維護代碼。

簡單總結:算法

  • 併發問題確實存在
  • 共享內存模型正確使用掌握的知識量多
  • 加鎖效率就低
  • 存在許多不肯定性
  1. Actor模型

Actor模型是一個概念模型,用於處理併發計算。Actor由3部分組成:狀態(State)+行爲(Behavior)+郵箱(Mailbox),State是指actor對象的變量信息,存在於actor之中,actor之間不共享內存數據,actor只會在接收到消息後,調用本身的方法改變本身的state,從而避免併發條件下的死鎖等問題;Behavior是指actor的計算行爲邏輯;郵箱創建actor之間的聯繫,一個actor發送消息後,接收消息的actor將消息放入郵箱中等待處理,郵箱內部經過隊列實現,消息傳遞經過異步方式進行。數據庫

image

Actor是分佈式存在的內存狀態及單線程計算單元,一個Id對應的Actor只會在集羣種存在一個(有狀態的 Actor在集羣中一個Id只會存在一個實例,無狀態的可配置爲根據流量存在多個),使用者只須要經過Id就能隨時訪問不須要關注該Actor在集羣的什麼位置。單線程計算單元保證了消息的順序到達,不存在Actor內部狀態競用問題。編程

舉個例子:設計模式

多個玩家合做在打Boss,每一個玩家都是一個單獨的線程,可是Boss的血量須要在多個玩家之間同步。同時這個Boss在多個服務器中都存在,所以每一個服務器都有多個玩家會同時打這個服務器裏面的Boss。api

若是多線程併發請求,默認狀況下它只會併發處理。這種狀況下可能形成數據衝突。可是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不共享狀態,經過消息傳遞,每次調用都是一次網絡請求,不太適合實施細粒度的並行
  • 編程思惟須要轉變

image


第一小節總結:上面內容由下往上,從代碼層面細粒度層面表達了採用Actor模型的好處或緣由。


CQRS/ES(架構層面)
  1. 從1000萬用戶併發修改用戶資料的假設場景開始

image

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

image

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

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

image

1000萬用戶,一個用戶一個Actor,1000萬個內存對象。

image

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,事件溯源提升性能的同時,能夠用來保證內存數據的高可用。

image

image

  1. CQRS

上面1000萬併發場景的內容來自網友分享的PPT,與咱們實際項目思路一致,就拿來與你們分享這個過程,下圖是咱們交易所項目中的架構圖:

image

開源版本架構圖:

image

( 開源項目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多在社區交流。

相關文章
相關標籤/搜索