Dapr for dotnet | 并发计算模型 - Virtual Actors
Actor 模型簡介
actor 模型起源于1973年。 它是由 Carl Hewitt 作為 并發計算的概念模型 提出的, 并發計算是一種同時執行多個計算的計算形式 。 當時還沒有高度并行的計算機,但多核 cpu 和分布式系統的最新進展使 actor 模型流行起來。
在 actor 模型中, actor 是一個獨立的計算單元和狀態單元。 這些 actor 彼此完全隔離,他們永遠不會共享內存。 actor 之間使用消息相互通信。 當 actor 接收到消息時,它可以更改其內部狀態,并向其他(可能是新的) actor 發送消息。
actor 模型使編寫并發和并行系統變得更容易 的原因是它提供了一個基于回合(或單線程)的處理模型。 多個 actor 可以同時運行,但每一種 actor 在某一個時刻只處理一條消息。 這意味著你可以確定在任何時候在一個 actor 中最多只有一個線程是活動的。 這使得編寫正確的并發和并行系統變得更加容易。
Dapr Actors 簡介
actor 模式/模型 闡述了 Actors 為 最低或最小級別的 “計算單元”。 換句話說,您將代碼寫入獨立單元 ( 稱為 actor) ,該單元接收消息并一次處理消息,而不進行任何類型的并行或線程處理。
當代碼處理一條消息時,它可以向其他 Actors 發送一條或多條消息,或者創建新的 Actors。 底層 runtime 將管理每個 actor 的運行方式、時機和位置,并在 Actors 之間傳遞消息。
大量 Actors 可以同時執行,而 Actors 可以相互獨立執行。
actor 模型實現通常被綁定到特定的語言或平臺上。 但是,通過 Dapr actor 構建塊的實現,您可以從任何語言或平臺依據 Actors 模型編寫 Dapr Actor,而 Dapr 利用了底層平臺提供的可擴展性和可靠性保證。
Dapr 包含專門實現 Orleans – Virtual Actors 模式 的運行時。 通過這個虛擬 actor 模型,你不需要顯式的創建 actor。在第一次向actor 發送消息時,actor 被隱式激活并放置在集群中的一個節點上。當一個actor在一段時間內未被使用時或不執行操作時,actor 將以靜默方式從內存中(In memory)卸載。如果一個節點發生故障,Dapr會自動將激活的 actor 轉移到健康的節點。 除了在 actor 之間發送消息外,Dapr actor 模型還支持使用 Timer(計時器)和 Reminder(提醒器)來調度未來的工作。
Actors 模式的應用場景
盡管 actor 模型可以提供很大的好處(可以很好適應一些分布式系統問題和場景),但仔細考慮 actor 設計模式也是很重要的。 例如, 許多客戶機調用同一個 actor 將導致性能低下,因為 actor 操作是線性連續執行的。
與任何其他技術決策一樣,您應該根據您嘗試解決的問題來決定是否使用 Actors。 一般來說,在下列情況的問題或場景中,可以考慮使用 actor 模式來處理:
- 【需要處理的問題】
- 【不需要處理的問題】
Dapr 中的 Actors(并發計算模型 & 分布式單例模型)
每個 actor 都定義為 actor 類型的實例,與對象是類的實例的方式相同。 例如,可能存在實現計算器功能的 actor 類型,并且該類型的許多 Actors 分布在集群的各個節點上。 每個這樣的 actor 都是由一個 actor ID (全局唯一)確定的。
Dapr Actor 生命周期
Dapr Actors 是虛擬的,意思是他們的 生命周期與他們在內存中(in - memory)的表現不相關。 因此,它們不需要顯式創建或銷毀。 Dapr Actors 運行時在第一次接收到該 actor ID(全局唯一)的請求時自動激活 actor。 如果 actor 在一段時間內未被使用,那么 Dapr Actors 運行時將回收內存對象。 如果以后需要重新啟動,它還將保持對 actor 的一切原有數據(數據持久化)。
計時器(timer)和提醒器(reminders)
調用 actor 方法和 reminders(提醒器)將重置空閑 timer(計時器) ,例如,reminders 觸發將使 actor 保持活動狀態。 不論 actor 是否處于活動狀態或不活動狀態 actor reminders 都會觸發,對不活動 actor ,那么會首先激活 actor。 actor timers 不會重置空閑 timer,因此 timer(計時器)觸發不會使 actor 保持活動狀態。 timer 僅在 actor 活躍時被觸發。
actor 可以使用 timer 和 reminders 來安排對自己的調用。 這兩個概念 都支持適當時間的配置。 區別在于回調注冊的生命周期 :
- 只有當 actor 被激活時,計時器(timer)才會保持活動狀態。 計時器不會重置空閑計時器,因此它們不能讓參與者自己處于活動狀態。
- 提醒器比 actor 活得長。 如果 actor 被禁用,一個提醒器(reminders)可以重新激活該 actor。 提醒器(reminders)將重置空閑計時器(timer)。
Actor 之間的消息傳遞
空閑超時和掃描時間間隔 Dapr 運行時(daprd)用于查看是否可以對 actor 進行垃圾收集。 當 daprd 調用 actor 服務以獲取受支持的 actor 類型時,此時可以用于信息傳遞。
Virtual actors 生命周期抽象會將一些警告作為 virtual actors 模型的結果,而事實上, Dapr Actors 實施有時會偏離此模型。
Dapr actor 狀態持久化
在第一次將消息發送到其 actor 標識時,將自動激活 actor ( 導致構造 actor 對象) 。 在一段時間后,actor 對象將被垃圾回收。 以后,再次使用 actor ID 訪問,將構造新的 actor。 actor 的狀態比對象的生命周期更久,因為 actor 的狀態存儲在 Dapr 運行時的 State Management building block 中(也就是說 actor 即使不在活躍狀態,仍然可以讀取它的狀態)。
Dapr Actors 的工作方式
Dapr 的 Sidecar 提供了 HTTP/gRPC 的 API 來調用 actor。
基于 HTTP 的 Sidecar API 調用格式
http://localhost:<daprPort>/v1.0/actors/<actorType>/<actorId>/- < daprPort >:Dapr Sidecar 所監聽的 HTTP 端口。
- < actorType >:actor 的執行組件類型。
- < actorId >:所指定的要調用的某個 actor 的 Id。
Sidecar 管理每個 actor 運行的方式、時間和地點,并且還會在 actor 之間路由消息。 當某個 actor 處于未使用狀態一段時間后,運行時會停用該 actor 并從內存中刪除它。 actor 管理的任何狀態都會持久保存,并在 actor 重新激活時可用。 Dapr 使用空閑計時器/Timer 來確定何時可停用參與者。 當對 actor 調用操作時(通過方法調用或 提醒器/Reminder 觸發),將重置空閑計時器,并且 actor 實例保持在激活狀態。
Service 與 Sidecar 之間的各種 API 調用
Sidecar API 只是其中的一部分。 服務本身也需要實現 API 規范,因為你為 actor 編寫的實際代碼將在服務本身內部運行。 下圖顯示了 Service 與 Sidecar 之間的各種 API 調用:
Dapr actor 提供可擴展性和可靠性
分發和故障轉移
為了提供可擴展性和可靠性,Actors 實例分布在整個集群中, Dapr 會根據需要自動將對象從失敗的節點遷移到健康的節點。
Actors 分布在 actor 服務的實例中,并且這些實例分布在集群中的節點之間。 每個服務實例(actorId)都包含給定 Actors 類型(actorType)的一組 Actors。
Actor 安置服務(Actor placement service)
在 self-hosted 模式下,dapr 初始化時會啟動一個 placement service 的 docker 容器服務,我們下面所說的 “安置服務”、“placement-service” 等都指代的是這個容器。
Dapr Sidecar 為了提供可伸縮性和可靠性,在 actor 服務的所有實例中對 actor 進行了分區。 Dapr 安置服務(Actor placement service) 負責跟蹤分區信息。 當啟動 actor 服務的新實例時,Sidecar 會向安置服務注冊受支持的 actor 類型。安置服務計算給定 actor 類型的更新分區信息,并將其廣播到所有實例。 下圖顯示了當服務橫向擴展到第二個副本時發生的情況:
【重要】actor 跨服務實例隨機分布,因此可以預料 actor 操作總是需要調用網絡中的其他節點。
下圖顯示了在 Pod 1 中運行的排序服務實例調用 ID 為 3 的 OrderActor 實例的 ship 方法。 由于 ID 為 3 的參與者被放置在不同的實例中,這會導致調用群集中的不同節點:
【注意】pod1 和 pod2 中所運行的都是 Ordering Service 服務的實例。
Actor 通信
您可以通過 HTTP/gRPC 來與 Dapr 交互以調用 actor 方法。
# POST/GET/PUT/DELETE http://localhost:3500/v1.0/actors/<actorType>/<actorId>/<method/state/timers/reminders>您可以在請求主體中為 actor 方法提供任何數據,并且請求的響應在響應主體中,這是來自 actor 方法調用的數據。
并發(Concurrency)
Dapr Actors 運行時提供了一個簡單的基于回合的訪問模型,用于訪問 Actors 方法。 這意味著任何時候都只有一個線程在一個 actor 對象的代碼內活動。 基于回合的訪問大大簡化了并發系統,因為不需要同步數據訪問機制。 這也意味著系統的設計必須考慮到每個 actor 實例的單線程訪問性質。
- 單個 actor 實例一次無法處理多個請求。 如果 actor 實例預期要處理并發請求,可能會導致吞吐量瓶頸。
- 如果兩個 Actors 之間存在循環請求,而外部請求同時向其中一個 Actors 發出外部請求,那么 Actors 可以相互死鎖。 Dapr actor 運行時會自動分出 actor 調用,并向調用方引發異常以中斷可能死鎖的情況。
基于回合的訪問
一個回合包括執行 actor 方法以響應來自其他 Actors 或客戶端的請求,或執行 timer/reminders 回調。 即使這些方法和回調是異步的,但 Dapr Actors 運行時并沒有將它們交錯(Interleave ,即并發調用它們)。 在允許新回合之前,必須完全結束之前的回合。 換句話說,在允許對方法或回調進行新調用之前,必須完全完成當前正在執行的 actor 方法或 timer/reminders 回調。 如果執行從方法或回調返回結果,并且方法或回調返回的任務已完成,則方法或回調將被視為已完成。 值得強調的是,即使在不同方法、timer和回調中,基于回合的并發也一樣起作用。
Dapr Actors 運行時通過在回合開始時獲取每個 Actors 的鎖定并在該回合結束時釋放鎖定來實施基于回合的并行。 因此, 基于回合的并發性是按每個 actor 執行的,而不是跨 Actors 執行的。 Actor 方法和 timer/reminders 回調可以代表不同的 Actors 同時執行。
下面的示例演示了上述概念。 現在有一個實現了兩個異步方法(例如,方法 1 和方法 2)、timer 和 reminders 的 actor。 下圖顯示了執行這些方法的時間線的示例,并代表屬于此 Actors 類型的兩個 Actors ( ActorId1 和 ActorId2) 的回調。
使用 Dapr for dotnet SDK 項目演示
- Actor.StateManager
- Timer(計時器)
- Reminder(提醒器)
新建 lib 類庫 Common 項目
此處 Demo 演示依然使用 FrontEnd 和 BackEnd 項目進行,新建 lib 類庫 Common,添加如下 Nuget 依賴包:
- Dapr.Actors v1.7.0
- Dapr.Actors.AspNetCore v1.7.0
新建接口 IWorkflowActor 并繼承 IActor,添加如下代碼:
using Dapr.Actors;namespace Common;public interface IWorkflowActor : IActor {Task<bool> ApproveAsync();Task<int> IncrementAsync();#region TimerTask RegisterTimerAsync();Task UnregisterTimerAsync();#endregion#region ReminderTask RegisterReminderAsync();Task UnregisterReminderAsync();#endregion }BackEnd 項目改造
Program.cs 注冊 actor,映射Actor路由
在 Console 項目中直接調用 ActorProxy.Create 的靜態方法來創建一個 actor 的代理實例。而如果客戶端是一個 asp.net core 應用, 你可以使用 IActorProxyFactory (構造函數 DI)接口來創建 actor 的代理。 他帶來的最主要的好處是你可以在同一個地方進行配置。AddActors 擴展方法可以接收一個委托來允許你指定 actor 運行時的一些選項(options),比如 dapr sidecar 的 http 端點。
下面的例子使用自定義的 JsonSerializerOptions 來配置 actor 狀態持久化和消息反序列化:
// 注冊 actor services.AddActors(options => {var jsonSerializerOptions = new JsonSerializerOptions(){PropertyNamingPolicy = JsonNamingPolicy.CamelCase,PropertyNameCaseInsensitive = true};options.JsonSerializerOptions = jsonSerializerOptions;options.Actors.RegisterActor<WorkflowActor>(); });//映射Actor路由 app.MapActorsHandlers();Timer 和 Reminder 相關方法說明:
- Timer 通過父類 Actor 的 RegisterTimerAsync(異步注冊) 和 UnregisterTimerAsync(異步取消注冊);
- Timer 父類 Actor 的 RegisterTimerAsync(異步注冊)通過 callback 實現 StateManager 相關操作;
- Reminder 通過實現 Actor 的 RegisterReminderAsync(注冊)和 UnregisterReminderAsync(異步取消注冊);
- Reminder 通過實現 IRemindable 接口的 ReceiveReminderAsync(異步接收提醒);
IActorStateManager 接口信息(StateManager)
IRemindable 接口信息,ReceiveReminderAsync(異步接收提醒)
其中 Timer 和 Reminder 方法中的參數 TimeSpan dueTime, TimeSpan period 說明:
- dueTime 首次延遲啟動時間;
- period 激活后的間隔執行周期;
FrontEnd 項目改造
Dapr run 啟動測試
vs 先編譯生成下文件,然后執行如下命令:
- 啟動 BackEnd 服務
- 啟動 FrontEnd 服務
【注意】dapr run 啟動服務時,保持兩個服務協議模式對等(ssl),切記一個服務器開啟 ssl,另一個服務未開啟 ssl,這樣兩個項目通信時會出現異常(目標通信拒絕);
開啟 ssl 后面添加 --app-ssl 即可;
- 瀏覽器訪問 Swagger 頁面:
頁面顯示如下:
curl 命令訪問:(從http://ASP.NET Core客戶端調用Actor )
curl -X 'GET' \'http://localhost:5001/api/DaprActorsClient/Approve/111' \-H 'accept: */*'curl -X 'GET' \'http://localhost:5001/api/DaprActorsClient/Increment/222' \-H 'accept: */*'- Timer 注冊 / 取消注冊
- Reminder 注冊 / 取消注冊
Demo 中的方法均可成功調用,此處就不再截圖說明,感興趣的小伙伴可自行參照測試;
總結
- Dapr actor 是一個獨立的計算單元和狀態單元。 這些 actor 彼此完全隔離,他們永遠不會共享內存。 actor 之間使用消息相互通信;
- Dapr actor 是虛擬的,生命周期與他們在內存中(in - memory)的表現不相關;
- Dapr actor 是基于回合的訪問模型,任何候都只有一個線程在一個 actor 對象的代碼內活動;
- Dapr actor 中的 timer 不能持久化,再次啟動服務 計時器 會失效;
- Dapr actor 中的 reminder 會持久化,再次啟動服務 提醒器 會繼續執行;
- Reminder 觸發將使 actor 保持活動狀態;
- Timer(計時器)觸發不會使 actor 保持活動狀態。 timer 僅在 actor 活躍時被觸發;
參考文章
- Dapr Actors 概述,https://docs.dapr.io/zh-hans/developing-applications/building-blocks/actors/actors-overview/
- Dapr Actor 構建塊,https://docs.microsoft.com/zh-cn/dotnet/architecture/dapr-for-net-developers/actors
總結
以上是生活随笔為你收集整理的Dapr for dotnet | 并发计算模型 - Virtual Actors的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: easyui filebox文件类型判断
- 下一篇: linux unison数据同步,lin