並發
1. 並發和並行
- 並發:兩個或多個任務在同一時間段內運行。關注點在任務分割。
- 並行:兩個或多個任務在同一時刻同時運行。關注點在同時執行。
本文大多數情況下不會嚴格區分這兩個概念,默認並發就是指並行機制下的並發。
2. 好處
隨着多核處理器的出現,並發編程可以提高程序的性能(吞吐量和響應能力)。
3. 並發實現方式
共享內存模型
因為並發能提高程序的性能,為了解決並發的需求,許多編程語言提供了共享內存通信機制(本文稱為共享內存模型),體現是引入了Thread(線程)等概念。線程的出現解決了兩個問題,一是GUI出現后急切需要並發機制來保證用戶界面的響應;二是互聯網發展后帶來的多用戶問題。但編寫正確的並發、容錯、可擴展的程序並不容易,對開發人員要求比較高,需要開發人員有能力處理避免死鎖、互斥等待、競爭條件等問題。 當對程序進行縱向擴展(Scale Up)和橫向擴展(Scale Out)時,問題會變得更加復雜。
為什么這么難:
We believe that writing correct concurrent, fault-tolerant and scalable applications is too hard. Most of the time it’s because we are using the wrong tools and the wrong level of abstraction. ——Akka
譯:
我們認為寫正確的並發、容錯、可擴展的程序如此之難,是因為我們用了錯誤的工具和錯誤的抽象。——Akka
開發人員采用共享內存模型進行並發編程時,需要特別關注共享的數據結構及線程間的資源競爭導致的死鎖等問題,這是一個非常大的難點,Actor模型可以很大程度地解決這些問題。
Actor模型
Actor模型這么好,Actor模型是什么?
Actor模型是一個概念模型,用於處理並發計算。它定義了一系列系統組件應該如何動作和交互的通用規則,最著名的使用這套規則的編程語言是Erlang。
Actor由3部分組成:狀態(State)+行為(Behavior)+郵箱(Mailbox),State是指actor對象的變量信息,存在於actor之中,actor之間不共享內存數據,actor只會在接收到消息后,調用自己的方法改變自己的state,從而避免並發條件下的死鎖等問題;Behavior是指actor的計算行為邏輯;郵箱建立actor之間的聯系,一個actor發送消息后,接收消息的actor將消息放入郵箱中等待處理,郵箱內部通過隊列實現,消息傳遞通過異步方式進行。
Actor是分布式存在的內存狀態及單線程計算單元,一個Id對應的Actor只會在集群種存在一個(有狀態的 Actor在集群中一個Id只會存在一個實例,無狀態的可配置為根據流量存在多個),使用者只需要通過Id就能隨時訪問不需要關注該Actor在集群的什么位置。單線程計算單元又保證了消息的順序到達,不存在Actor內部狀態競用問題。
Actor框架--Orleans
Actor模型這么好,怎么實現?
可以通過特定的Actor工具或直接使用編程語言實現Actor模型,Erlang語言含有Actor元素,Scala可以通過Akka框架實現Actor編程。目前C#語言中有兩類比較流行,Akka.NET框架和Orleans框架。本文主要關注Orleans框架。
Orleans是微軟開發的開源、分布式、跨平台的Virtual Actor框架,可以方便C#開發者開發分布式、高擴張、高並發、低延時的應用程序。
架構落地
1. N-Tier架構(代表三層)
N-Tier架構的代表是三層架構,實際項目分了很多層,多數是三層的延伸。傳統的三層體系結構包括無狀態的前端,無狀態的中間層和存儲層。
這種架構存在兩個比較大的問題:
- 由於存儲層在延遲和吞吐量方面的限制,系統很難處理高並發的場景。這種結構下通常的辦法是在中間層和存儲層之間添加緩存層來提高性能。如果引入的是分布式緩存,又會引入狀態同步問題,這時候就需要考慮如何精准快速的更新緩存。
- 這種無狀態的N-Tier架構,中間層內獨立的應用實體之間通信很不方便,當一個請求需要多實體之間調用時,為業務代碼的實現帶來了困難。
Actor在架構層面上提供了一個簡單的方式來構建無鎖分布式大規模的應用程序,而不需要學習和應用復雜的並發和分布式控制,有效的解決了上述兩個問題。
Orleans提供了一種直接的方式構建有狀態的中間層,大量的業務邏輯實體分布式地部署在集群中,彼此相互獨立,又可以相互訪問。
缺點:
Orleans技術很優秀,許多人想用,但是目前國內圈子里的資料很少,代碼多是Demo,Actor初接觸通常覺得不易理解,使得大家找不到Orleans落地的方式。
2. Event Sourcing
在Orleans中,actor中的數據(State)存在於內存中,內存中的數據在斷電、重啟的場景下會丟失,可以使用Event Sourcing技術解決這一問題,Actor的狀態修改是由事件驅動的,事件被持久化起來,然后通過Event Sourcing的技術,還原特定Actor的最新狀態到內存。不僅如此,Event Sourcing還會極大地降低系統的耦合性。
什么是事件溯源
一個對象從創建開始到消亡會經歷很多事件,以前我們是在每次對象參與完一個業務動作后把對象的最新狀態持久化保存到數據庫中,也就是說我們的數據庫中的數據是反映了對象的當前最新的狀態。而事件溯源則相反,不是保存對象的最新狀態,而是保存這個對象所經歷的每個事件,所有的由對象產生的事件會按照時間先后順序有序的存放在數據庫中。
事件溯源:
-
不保存對象的最新狀態,而是保存對象產生的所有事件。
-
通過事件溯源(Event Sourcing,ES)得到對象最新狀態。
Actor內數據的修改,是ACID強一致性的;跨Actor的數據修改,是最終一致性的,通過EventSourcing實現。這樣可以讓我們最大化的降低並發沖突,從而最大化的提高整個系統的吞吐。Actor和DDD,CQRS,Event Soucing(事件溯源)設計模型有天然的融合性,基於Actor可以很好的進行以上實踐。
EventSourcing的概念通常跟CQRS放在一起,CQRS/ES的概念常常出現在DDD中,在DDD中,有許多程序員向往的實現,但是里面的抽象概念比較多,只熟悉三層的開發人員很難駕馭這些概念,基於這些概念提出的架構設計更是難以捉摸,一些前輩為探索DDD最佳實踐,開發了一些DDD框架,但實際項目中,很難保證系統性能。
Actor不好理解,CQRS/ES、DDD不好理解,恰恰這些技術交織在一起能很好的使彼此落地。
3. Ray
說了這么多,目的是為了引出Ray。
Ray是一個集成Actor、Event Sourcing(事件溯源)、Eventual Consistency(最終一致性)的無數據庫事務、高性能分布式雲框架。Ray是一個非常精致小巧的Actor/ES框架,來自生產環境,踩了很多坑,降低了Actor、ES的開發難度。
來自生產環境
ASP.NET Core、Redis、MongoDB、跨平台、gRPC、RabbitMQ、Dapper……許多朋友都掌握了,但依舊好像缺些什么,或許可以嘗試一下新的旅程。這是項目的地址: https://github.com/RayTale, 歡迎大家討論、參與、使用。
設計圖
這張圖方便大家初步了解Ray,但是太過強調細節,對DB太過突出,而Event沒有突出出來。
這張圖更簡潔明了一些。
參考: