ENode 1.0 - Staged Event-Driven Architecture思想的運用


開源地址:https://github.com/tangxuehua/enode

上一篇文章,簡單介紹了enode框架的command service api設計思路。本文介紹一下enode框架對Staged Event-driven architecture思想的運用。通過前一篇文章我們知道command service是會被高並發的訪問,我們除了可以用異步的方式執行command以及集群的方式來提高系統響應性能外。最根本上要解決的問題是盡量快的處理單個command。這樣才能在單位時間內處理更多的command。

先貼一下enode框架的內部實現架構圖,這樣對大家理解后面的分析有幫助。

我覺得要盡量快的處理command,主要思路有兩點:

能並行處理的盡量並行

  1. command service接收到command后,會把command發送到某個可用的command隊列。然后該command隊列的出口端,如果只有單個線程在處理command,而且這個線程如果有IO操作,那肯定快不到哪里去;因為只要處理單個command的速度跟不上command進入隊列的速度,那command隊列里的command就會不斷增多,導致command執行的延遲增加。所以,思路就是,設計多個線程(就是上圖中的Command Processor中的worker)來同時從command隊列拿command,然后處理。這樣就能實現多個線程在同時處理不同的command。
  2. 但是光這樣還不夠,實際上我們還可以做的更好,那就是command queue也可以設計為多個。也就是說command service接收到command后,會通過一個command router,將當前command路由到某個可用的command隊列,然后將該command發送到該隊列。這樣做的好處是,我們的command service背后有多個command隊列(上圖畫了兩個command queue)支撐着,每個command隊列的出口端又有多個線程在同時處理。這樣的話,我們就能最大化的壓榨我們的服務器CPU和內存等資源了。當然,框架要支持允許用戶配置多少個command隊列,以及每個隊列多少個線程處理。這樣框架使用者就能根據當前服務器的CPU個數來決定該如何配置了。
  3. 同理,domain model產生的事件(domain event)的處理也應該要並行處理;那具體是什么處理呢?就是上圖中的Event Processor所做的事情。Event Processor會包含多個worker,每個worker就是一個線程。每個worker會從event queue中拿出事件,然后將事件進一步分發(dispatch)給所有的事件訂閱者。

那么上面這些並行執行的邏輯是如何訪問共享資源的呢?

對於command processor中的每個work線程,從上面的架構圖可以清晰的看到,共享資源是event store和memory cache。event store,我們會並發的寫入事件;memory cache,我們會並發的更新聚合根。所以,這兩種存儲都必須很好的支持高並發的寫入,且要高效;經過我的一些調研,個人覺得mongodb比較適合作為eventstore。原因是:1)支持集群和sharding;2)支持唯一索引;3)支持關系型查詢;4)高性能,默認是先保存到內存,每100ms將內存數據寫入日志,每1分鍾將內存數據正式寫入磁盤;基於這4點,我們能利用mongodb實現一個比較理想的eventstore。而內存緩存(memory cache),我覺得memcached或者redis都還不錯,都是比較成熟的分布式緩存。利用分布式緩存,我們不必擔心數據放不下的問題,因為我們可以對數據按特征進行分區存放。這個思路就像數據庫的分庫分表類似。需要注意的是,eventstore必須支持嚴格控制並發沖突,mongodb的唯一索引可以確保這一點;而memory cache,不用支持並發沖突檢測,只要能保障快速的根據key讀寫即可。因為我們總是先持久化完事件后再將最新狀態的聚合根更新到memory cache,而持久化事件到eventstore已經做了並發沖突檢測,所以更新到memory cache就一定也是按照事件持久化的順序被更新到memory cache的。另外,實際上event store和memory cache是被整個web服務器集群所共享的。不過幸好mongodb,redis等產品足夠強大,都支持橫向擴展,所以我們完全有信心在web服務器不斷增加的情況下,也對mongodb,redis做相應的橫向擴展,從而不會讓這兩個地方產生瓶頸。

能允許延遲處理的盡量延遲

處理每個command時,會調用domain model執行業務邏輯,然后domain model會產生事件(domain event)。然后框架會在command處理完畢后自動對事件進行后續的處理。主要做的事情是:1)持久化事件到eventstore;2)更新memory cache;3)將事件publish出去。這三步分別對應上圖中的3、4、5三個箭頭。大家可以看到,對於publish事件這一步,我們不是馬上將事件dispatch給事件訂閱者的,而是先發送到event queue,然后異步的方式dispatch事件。

這里可以這樣做的原因是,當領域事件被持久化到eventstore,本質上就已經表示業務邏輯處理完成了。事件是一件已發生的事情,事件被保存了就表示這件已發生的事情被記載了,也就是說,成為了歷史。當我們下次要獲取最新的聚合根時,如果從內存緩存里獲取到的聚合根的狀態是舊的,那從eventstore中通過event sourcing得到的聚合根一定是最新的,因為eventstore中存放了所有最新的歷史。所以,我們可以知道,只要事件被持久化完成了,那后續的所有步驟都可以異步的方式來做。但是為什么更新memory cache沒有異步的做呢?因為command handler在處理業務邏輯時,獲取聚合根是從memory cache獲取的,所以越早更新memory cache,我們拿到的聚合根的數據就越可能是最新的,拿到的數據越新,就意味着產生並發沖突的可能性就越低。實際上,因為像memcached, redis這樣的分布式緩存,性能是非常高的,每秒1萬次的讀寫操作問題應該不大。所以,我們可以認為內存緩存中的數據總是與eventstore實時保持一致的,因為延遲在0.1毫秒以內。因此,我們會在事件被保存到eventstore后,馬上將最新狀態的聚合根寫入到memory cache。

但是,將事件dispatch給所有的事件訂閱者這個操作是非常耗時的,因為我們無法知道每個事件訂閱者具體做的事情,比如有些是更新CQRS查詢端的讀庫的表,有些是調用外部系統的接口,等等。所以,dispatch這個邏輯必須異步,實際上,這也是CQRS架構的核心思路。另外,我們為了盡量快的dispatch事件,如上面提到,我們會開多個線程去並行的dispatch事件給事件訂閱者。

有一個需要好好考慮的問題是:我們如何保證事件的持久化順序與publish出去的順序相同?這個問題,下次專門寫一篇文章好好討論吧。有興趣的也可以想想為什么要解決這個問題,也非常歡迎和我討論解決方案。

關於來自ebay的經驗學習

可伸縮性最佳實踐:來自eBay的經驗,這篇文章中提到,提高可伸縮性的一項關鍵措施是積極地采取異步策略。

如果組件A同步調用組件B,那么A和B就是緊密耦合的,而緊耦合的系統其可伸縮性特征是各部分 必須共同進退——要伸縮A必須同時伸縮B。同步調用的組件在可用性方面也面臨着同樣的問題。我們回到最基本的邏輯:如果A推出B,那么非B推出非A。也就 是說,若B不可用,則A也不可用。如果反過來A和B的聯系是異步的,不管是通過隊列、多播消息、批處理還是什么其他手段,它們就可以分別地伸縮。而且,此 時A和B的可用性特征是相互獨立的——即使B受困或者死掉,A仍然能夠繼續前進。

整個基礎設施從上到下都應該貫徹這項原則。即使在單個組件內部也可通過SEDA(分階段的事件驅動架構,Staged Event-Driven Architecture)等技術實現異步性,同時保持一個易於理解的編程模型。組件之間也遵守同樣的原則——盡可能避免同步帶來的耦合。在多數情況下, 兩個組件在任何事件中都不會有直接的業務聯系。在所有的層次,把過程分解為階段(stages or phases),然后將它們異步地連接起來,這是伸縮的關鍵。

用異步的原則解耦程序,盡可能將過程變為異步的。對於要求快速響應的系統,這樣做可以從根本上減少請求者所經歷的響應延遲。對於網站或者交易系統, 犧牲數據或執行的延遲時間(完成全部工作的實踐)來換取用戶的延遲時間(用戶得到響應的時間)是值得的。活動跟蹤、單據開付、決算和報表等處理過程顯然都 應該屬於后台活動。主要用例過程中常常有很多步驟可以進一部分解成異步運行。任何可以晚點再做的事情都應該晚點再做。

還有一個同等重要的方面認識到的人不多:異步性可以從根本上降低基礎設施的成本同步地執行操作迫使你必須按照負載的峰值來配備基礎設施——即使在任務最重的那一天里任務最重的那一秒,設施也必須有能力立即完成處理。而將昂貴的處理過程轉變為異步的流,基礎設施就不需要按照峰值來配備,只需要滿足平 均負載。而且也不需要立即處理所有的請求,異步隊列可以將處理任務分攤到較長的時間里,因而起到削峰的作用。系統的負載變化越大,曲線越多尖峰,就越能從異步處理中得益。

enode框架內部的實現思路,正是學習了這種SEDA的思想,將一個command的處理過程分為兩個階段(command執行,event分發),每個階段盡量調用多的資源去並行處理,兩個階段之間通過隊列連接,實現這兩個階段之間相互不受影響,比如事件分發失敗不會影響command的執行;並且,因為兩個階段之間沒有直接聯系,所以事件分發雖然相對較慢,但也不會影響command的執行效率;這就是SEDA的好處,將過程分階段,分階段的依據是找出這個過程中哪些地方可以延遲執行,即可以允許異步執行。然后用隊列銜接每個階段,對每個階段優化處理。

總結

通過上面的分析,我們知道了,enode框架內部實現的主要設計思路是:

  1. 每個系統依賴於enode框架都可以支持web服務器集群;
  2. 每台web服務器上面部署了一個enode框架實例;
  3. 每個enode框架實例有一個唯一的command service;
  4. 每個command service內部有多個command queue,通過command router來路由command;
  5. 每個command queue的出口端都有一個command processor在消費command;
  6. 每個command processor內部實際是通過多個worker線程在並行的消費command;
  7. 每個worker線程都訪問共享的event store和memory cache資源;
  8. 每個worker線程在消費完一個command后,產生的事件先發送到一個可用的event queue,同樣也會通過一個event queue router來路由;
  9. 每個event queue的出口端都有一個event processor在消費event;
  10. 每個event processor內部實際也是通過多個worker線程在並行的分發event給事件訂閱者;

就寫這些吧,希望本文能對大家理解enode有所幫助。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM