学Dapr Actors 看这篇就够了
介紹
Actor模式將Actor描述為最低級(jí)別的“計(jì)算單元”。換句話說,您在一個(gè)獨(dú)立的單元(稱為actor)中編寫代碼,該單元接收消息并一次處理一個(gè)消息,沒有任何并發(fā)或線程。
再換句話說,根據(jù)ActorId劃分獨(dú)立計(jì)算單元后,相同的ActorId重入要排隊(duì),可以理解為lock(ActorId)
注:這里有個(gè)反例,就是重入性的引入,這個(gè)概念目前還是Preview,它允許同一個(gè)鏈內(nèi)可以重復(fù)進(jìn)入,判斷的標(biāo)準(zhǔn)不止是ActorId這么簡(jiǎn)單,即自己調(diào)自己是被允許的。這個(gè)默認(rèn)是關(guān)閉的,需要手動(dòng)開啟,即默認(rèn)不允許自己調(diào)自己。
當(dāng)您的代碼處理一條消息時(shí),它可以向其他參與者發(fā)送一條或多條消息,或者創(chuàng)建新的參與者。底層運(yùn)行時(shí)管理每個(gè)參與者運(yùn)行的方式、時(shí)間和地點(diǎn),并在參與者之間路由消息。
大量的Actor可以同時(shí)執(zhí)行,Actor彼此獨(dú)立執(zhí)行。
Dapr 包含一個(gè)運(yùn)行時(shí),它專門實(shí)現(xiàn)了 Virtual Actor 模式。 通過 Dapr 的實(shí)現(xiàn),您可以根據(jù) Actor 模型編寫 Dapr Actor,而 Dapr 利用底層平臺(tái)提供的可擴(kuò)展性和可靠性保證。 ?
什么時(shí)候用Actors
Actor 設(shè)計(jì)模式非常適合許多分布式系統(tǒng)問題和場(chǎng)景,但您首先應(yīng)該考慮的是該模式的約束。一般來說,如果出現(xiàn)以下情況,請(qǐng)考慮使用Actors模式來為您的問題或場(chǎng)景建模:
您的問題空間涉及大量(數(shù)千個(gè)或更多)小的、獨(dú)立且孤立的狀態(tài)和邏輯單元。
您希望使用不需要與外部組件進(jìn)行大量交互的單線程對(duì)象,包括跨一組Actors查詢狀態(tài)。
您的 Actor 實(shí)例不會(huì)通過發(fā)出 I/O 操作來阻塞具有不可預(yù)測(cè)延遲的調(diào)用者。
Dapr Actor
每個(gè)Actor都被定義為Actor類型的實(shí)例,就像對(duì)象是類的實(shí)例一樣。 ?例如,可能有一個(gè)執(zhí)行計(jì)算器功能的Actor類型,并且可能有許多該類型的Actor分布在集群的各個(gè)節(jié)點(diǎn)上。每個(gè)這樣的Actor都由一個(gè)Acotr ID唯一標(biāo)識(shí)。 ?
生命周期
Dapr Actors是虛擬的,這意味著他們的生命周期與他們的內(nèi)存表現(xiàn)無關(guān)。因此,它們不需要顯式創(chuàng)建或銷毀。Dapr Actors運(yùn)行時(shí)在第一次收到對(duì)該Actor ID 的請(qǐng)求時(shí)會(huì)自動(dòng)激活該Actor。如果一個(gè)Actor在一段時(shí)間內(nèi)沒有被使用,Dapr Actors運(yùn)行時(shí)就會(huì)對(duì)內(nèi)存中的對(duì)象進(jìn)行垃圾回收。如果稍后需要重新激活,它還將保持對(duì)參與者存在的了解。如果稍后需要重新激活,它還將保持對(duì) Actor 的一切原有數(shù)據(jù)。
調(diào)用 Actor 方法和提醒會(huì)重置空閑時(shí)間,例如提醒觸發(fā)將使Actor保持活躍。無論Actor是活躍還是不活躍,Actor提醒都會(huì)觸發(fā),如果為不活躍的Actor觸發(fā),它將首先激活演員。Actor 計(jì)時(shí)器不會(huì)重置空閑時(shí)間,因此計(jì)時(shí)器觸發(fā)不會(huì)使 Actor 保持活動(dòng)狀態(tài)。計(jì)時(shí)器僅在Actor處于活動(dòng)狀態(tài)時(shí)觸發(fā)。
Reminders 和 Timers 最大的區(qū)別就是Reminders會(huì)保持Actor的活動(dòng)狀態(tài),而Timers不好會(huì)
Dapr 運(yùn)行時(shí)用來查看Actor是否可以被垃圾回收的空閑超時(shí)和掃描間隔是可配置的。當(dāng) Dapr 運(yùn)行時(shí)調(diào)用 Actor 服務(wù)以獲取支持的 Actor 類型時(shí),可以傳遞此信息。
由于Virtual Actor模型的存在,這種Virtual Actor生命周期抽象帶來了一些注意事項(xiàng),事實(shí)上,Dapr Actors實(shí)現(xiàn)有時(shí)會(huì)偏離這個(gè)模型。 ?
第一次將消息發(fā)送到Actor ID時(shí),Actor被自動(dòng)激活(導(dǎo)致構(gòu)建Actor對(duì)象)。 經(jīng)過一段時(shí)間后,Actor對(duì)象將被垃圾回收。被回收后再次使用Actor ID將導(dǎo)致構(gòu)造一個(gè)新的Actor對(duì)象。 Actor 的狀態(tài)比對(duì)象的生命周期長(zhǎng),因?yàn)闋顟B(tài)存儲(chǔ)在 Dapr 運(yùn)行時(shí)配置的狀態(tài)管理組件中。
注:Actor被垃圾回收之前,Actor對(duì)象是會(huì)復(fù)用的。這里會(huì)導(dǎo)致一個(gè)問題,在.Net Actor類中,構(gòu)造函數(shù)在Actor存活期間只會(huì)被調(diào)用一次。
分發(fā)和故障轉(zhuǎn)移
為了提供可擴(kuò)展性和可靠性,Actor 實(shí)例分布在整個(gè)集群中,Dapr 根據(jù)需要自動(dòng)將它們從故障節(jié)點(diǎn)遷移到健康節(jié)點(diǎn)。
Actors 分布在 Actor 服務(wù)的實(shí)例中,而這些實(shí)例分布在集群中的節(jié)點(diǎn)之間。 對(duì)于給定的Actor類型,每個(gè)服務(wù)實(shí)例都包含一組Actor。
Dapr安置服務(wù)(Placement Service)
Dapr Actor 運(yùn)行時(shí)為您管理分發(fā)方案和密鑰范圍設(shè)置。這是由Actor Placement 服務(wù)完成的。創(chuàng)建服務(wù)的新實(shí)例時(shí),相應(yīng)的 Dapr 運(yùn)行時(shí)會(huì)注冊(cè)它可以創(chuàng)建的Actor類型,并且安置服務(wù)會(huì)計(jì)算給定Actor類型的所有實(shí)例的分區(qū)。每個(gè)Actor類型的分區(qū)信息表被更新并存儲(chǔ)在環(huán)境中運(yùn)行的每個(gè)Dapr實(shí)例中,并且可以隨著Actor服務(wù)的新實(shí)例的創(chuàng)建和銷毀而動(dòng)態(tài)變化。這如下圖所示:
當(dāng)客戶端調(diào)用具有特定ID的Actor(例如,Actor ID 123)時(shí),客戶端的 Dapr 實(shí)例會(huì)Hash Actor類型和 ID,并使用該信息調(diào)用可以為特定Actor ID的請(qǐng)求提供服務(wù)的相應(yīng)Dapr實(shí)例。因此,始終為任何給定的Actor ID 調(diào)用相同的分區(qū)(或服務(wù)實(shí)例)。這如下圖所示:
這簡(jiǎn)化了一些選擇,但也帶來了一些考慮: ?
默認(rèn)情況下,Actor 隨機(jī)放置到 pod 中,從而實(shí)現(xiàn)均勻分布。
因?yàn)锳ctor是隨機(jī)放置的,應(yīng)該可以預(yù)料到Actor操作總是需要網(wǎng)絡(luò)通信,包括方法調(diào)用數(shù)據(jù)的序列化和反序列化,產(chǎn)生延遲和開銷。
注:Dapr Actor 放置服務(wù)僅用于 Actor 放置,因此如果您的服務(wù)不使用 Dapr Actors,則不需要。 放置服務(wù)可以在所有托管環(huán)境中運(yùn)行,包括自托管和 Kubernetes。
Actor通訊
您可以通過HTTP/gRPC調(diào)用Actor,當(dāng)然也可以用SDK。
POST/GET/PUT/DELETE http://localhost:3500/v1.0/actors/<actorType>/<actorId>/<method/state/timers/reminders>并發(fā)
Dapr Actor 運(yùn)行時(shí)為訪問 Actor 方法提供了一個(gè)簡(jiǎn)單的回合制(turn-basesd)的訪問模型。這意味著在任何時(shí)候,Actor 對(duì)象的代碼中都不能有超過一個(gè)線程處于活動(dòng)狀態(tài)。
單個(gè)Actor實(shí)例一次不能處理多個(gè)請(qǐng)求。如果預(yù)期要處理并發(fā)請(qǐng)求,Actor 實(shí)例可能會(huì)導(dǎo)致吞吐量瓶頸。
單個(gè)Actor實(shí)例指每個(gè)Actor ID對(duì)應(yīng)的Actor對(duì)象。單個(gè)Actor不并發(fā)就沒有問題
如果兩個(gè) Actor 之間存在循環(huán)請(qǐng)求,而同時(shí)向其中一個(gè) Actor 發(fā)出外部請(qǐng)求,則 Actor 之間可能會(huì)陷入僵局。Dapr Actor運(yùn)行時(shí)自動(dòng)超時(shí)Actor調(diào)用并向調(diào)用者拋出異常以中斷可能的死鎖情況。
重入性(Preview)
作為對(duì) dapr 中基礎(chǔ) Actor 的增強(qiáng)。現(xiàn)在重入性為預(yù)覽功能,感興趣的小伙伴可以到看官方文檔。
回合制訪問(Turn-based access)
一個(gè)回合包括一個(gè)Actor方法的完整執(zhí)行以響應(yīng)來自其他Actor或客戶端的請(qǐng)求,或者一個(gè)計(jì)時(shí)器/提醒回調(diào)的完整執(zhí)行。即使這些方法和回調(diào)是異步的,Dapr Actor運(yùn)行時(shí)也不會(huì)將它們交叉。一個(gè)回合必須完全完成后,才允許進(jìn)行新的回合。換句話說,當(dāng)前正在執(zhí)行的Actor方法或計(jì)時(shí)器/提醒回調(diào)必須完全完成,才能允許對(duì)方法或回調(diào)的新調(diào)用。
Dapr Actor運(yùn)行時(shí)通過在回合開始時(shí)獲取每個(gè)Actor的鎖并在回合結(jié)束時(shí)釋放鎖來實(shí)現(xiàn)基于回合的并發(fā)性。 因此,基于回合的并發(fā)是在每個(gè)Actor的基礎(chǔ)上執(zhí)行的,而不是跨Actor。Actor 方法和計(jì)時(shí)器/提醒回調(diào)可以代表不同的 Actor 同時(shí)執(zhí)行。
以下示例說明了上述概念。考慮實(shí)現(xiàn)兩個(gè)異步方法(例如 Method1 和 Method2)、計(jì)時(shí)器和提醒的Actor 類型。下圖顯示了代表屬于此Actor類型的兩個(gè)Actors(ActorId1 和 ActorId2)執(zhí)行這些方法和回調(diào)的時(shí)間線示例。
Actor狀態(tài)管理
Actor可以使用狀態(tài)管理功能可靠地保存狀態(tài)。您可以通過 HTTP/gRPC 端點(diǎn)與 Dapr 交互以進(jìn)行狀態(tài)管理。
要使用 actor,您的狀態(tài)存儲(chǔ)必須支持事務(wù)。這意味著您的狀態(tài)存儲(chǔ)組件必須實(shí)現(xiàn) TransactionalStore 接口。只有一個(gè)狀態(tài)存儲(chǔ)組件可以用作所有參與者的狀態(tài)存儲(chǔ)。
事務(wù)支持列表:https://docs.dapr.io/reference/components-reference/supported-state-stores/
注:建議學(xué)習(xí)的時(shí)候都用Redis,官方所有的示例也都是基于Redis,比較容易上手,且Dapr init默認(rèn)集成
Actor計(jì)時(shí)器和提醒
Actor可以通過注冊(cè)計(jì)時(shí)器或提醒來安排自己的定期工作。
計(jì)時(shí)器和提醒的功能非常相似。主要區(qū)別在于,Dapr Actor 運(yùn)行時(shí)在停用后不保留有關(guān)計(jì)時(shí)器的任何信息,而使用 Dapr Actor 狀態(tài)提供程序保留有關(guān)提醒的信息。
定時(shí)器和提醒的調(diào)度配置是相同的,總結(jié)如下:
DueTime 是一個(gè)可選參數(shù),用于設(shè)置第一次調(diào)用回調(diào)之前的時(shí)間或時(shí)間間隔。如果省略 DueTime,則在定時(shí)器/提醒注冊(cè)后立即調(diào)用回調(diào)。
支持的格式:
RFC3339 日期格式,例如2020-10-02T15:00:00Z
time.Duration 格式,例如2h30m
ISO 8601 持續(xù)時(shí)間格式,例如PT2H30M
period 是一個(gè)可選參數(shù),用于設(shè)置兩次連續(xù)回調(diào)調(diào)用之間的時(shí)間間隔。當(dāng)以 ISO 8601-1 持續(xù)時(shí)間格式指定時(shí),您還可以配置重復(fù)次數(shù)以限制回調(diào)調(diào)用的總數(shù)。如果省略 period,則回調(diào)將僅被調(diào)用一次。
支持的格式:
time.Duration 格式,例如2h30m
ISO 8601 持續(xù)時(shí)間格式,例如PT2H30M, R5/PT1M30S
ttl 是一個(gè)可選參數(shù),用于設(shè)置計(jì)時(shí)器/提醒到期和刪除的時(shí)間或時(shí)間間隔。如果省略 ttl,則不應(yīng)用任何限制。
支持的格式:
RFC3339 日期格式,例如2020-10-02T15:00:00Z
time.Duration 格式,例如2h30m
ISO 8601 持續(xù)時(shí)間格式,例如PT2H30M
當(dāng)您同時(shí)指定周期內(nèi)的重復(fù)次數(shù)和 ttl 時(shí),計(jì)時(shí)器/提醒將在滿足任一條件時(shí)停止。
Actor 運(yùn)行時(shí)配置
actorIdleTimeout - 停用空閑 actor 之前的超時(shí)時(shí)間。每個(gè) actorScanInterval 間隔都會(huì)檢查超時(shí)。默認(rèn)值:60 分鐘
actorScanInterval - 指定掃描演員以停用空閑Actor的頻率的持續(xù)時(shí)間。閑置時(shí)間超過 actor_idle_timeout 的 Actor 將被停用。默認(rèn)值:30 秒
drainOngoingCallTimeout - 在耗盡Rebalanced的Actor的過程中的持續(xù)時(shí)間。這指定了當(dāng)前活動(dòng) Actor 方法完成的超時(shí)時(shí)間。如果當(dāng)前沒有 Actor 方法調(diào)用,則忽略此項(xiàng)。默認(rèn)值:60 秒
drainRebalancedActors - 如果為 true,Dapr 將等待 drainOngoingCallTimeout 持續(xù)時(shí)間以允許當(dāng)前角色調(diào)用完成,然后再嘗試停用角色。默認(rèn)值:true
drainRebalancedActors與上面的drainOngoingCallTimeout需搭配使用
reentrancy - (ActorReentrancyConfig) - 配置角色的重入行為。如果未提供,則禁用可重入。默認(rèn)值:disabled, 0
remindersStoragePartitions - 配置Actor提醒的分區(qū)數(shù)。如果未提供,則所有提醒都將保存為Actor狀態(tài)存儲(chǔ)中的單個(gè)記錄。默認(rèn)值:0
分區(qū)提醒(Preview)
在 sidecar 重新啟動(dòng)后,Actor 提醒會(huì)保留并繼續(xù)觸發(fā)。在 Dapr 運(yùn)行時(shí)版本 1.3 之前,提醒被保存在 actor 狀態(tài)存儲(chǔ)中的單個(gè)記錄上。
此為Preview功能,感興趣可以看官方文檔
.Net調(diào)用Dapr的Actor
與以往不同,Actor示例會(huì)多創(chuàng)建一個(gè)共享類庫(kù)用于存放Server和Client共用的部分
創(chuàng)建Assignment.Shared
創(chuàng)建類庫(kù)項(xiàng)目,并添加Dapr.ActorsNuGet包引用,最后添加以下幾個(gè)類:
AccountBalance.cs
namespace Assignment.Shared; public class AccountBalance {public string AccountId { get; set; } = default!;public decimal Balance { get; set; } }IBankActor.cs
注:這個(gè)是Actor接口,IActor是Dapr SDK提供的
using Dapr.Actors;namespace Assignment.Shared; public interface IBankActor : IActor {Task<AccountBalance> GetAccountBalance();Task Withdraw(WithdrawRequest withdraw); }OverdraftException.cs
namespace Assignment.Shared; public class OverdraftException : Exception {public OverdraftException(decimal balance, decimal amount): base($"Your current balance is {balance:c} - that's not enough to withdraw {amount:c}."){} }WithdrawRequest.cs
namespace Assignment.Shared; public class WithdrawRequest {public decimal Amount { get; set; } }創(chuàng)建Assignment.Server
創(chuàng)建類庫(kù)項(xiàng)目,并添加Dapr.Actors.AspNetCoreNuGet包引用和Assignment.Shared項(xiàng)目引用,最后修改程序端口為5000。
注:Server與Shared和Client的NuGet包不一樣,Server是集成了服務(wù)端的一些功能
修改program.cs
var builder = WebApplication.CreateBuilder(args); builder.Services.AddSingleton<BankService>(); builder.Services.AddActors(options => {options.Actors.RegisterActor<DemoActor>(); });var app = builder.Build();app.UseRouting();app.UseEndpoints(endpoints => {endpoints.MapActorsHandlers(); });app.Run();添加BankService.cs
using Assignment.Shared;namespace Assignment.Server; public class BankService {// Allow overdraft of up to 50 (of whatever currency).private readonly decimal OverdraftThreshold = -50m;public decimal Withdraw(decimal balance, decimal amount){// Imagine putting some complex auditing logic here in addition to the basics.var updated = balance - amount;if (updated < OverdraftThreshold){throw new OverdraftException(balance, amount);}return updated;} }添加BankActor.cs
using Assignment.Shared; using Dapr.Actors.Runtime; using System;namespace Assignment.Server; public class BankActor : Actor, IBankActor, IRemindable // IRemindable is not required {private readonly BankService bank;public BankActor(ActorHost host, BankService bank): base(host){// BankService is provided by dependency injection.// See Program.csthis.bank = bank;}public async Task<AccountBalance> GetAccountBalance(){var starting = new AccountBalance(){AccountId = this.Id.GetId(),Balance = 10m, // Start new accounts with 100, we're pretty generous.};var balance = await StateManager.GetOrAddStateAsync("balance", starting);return balance;}public async Task Withdraw(WithdrawRequest withdraw){var starting = new AccountBalance(){AccountId = this.Id.GetId(),Balance = 10m, // Start new accounts with 100, we're pretty generous.};var balance = await StateManager.GetOrAddStateAsync("balance", starting)!;if (balance.Balance <= 0){// Simulated reminder depositif (Random.Shared.Next(100) > 90){await RegisterReminderAsync("Deposit", null, TimeSpan.FromSeconds(5), TimeSpan.FromMilliseconds(-1));}}// Throws Overdraft exception if the account doesn't have enough money.var updated = this.bank.Withdraw(balance.Balance, withdraw.Amount);balance.Balance = updated;await StateManager.SetStateAsync("balance", balance);}public async Task ReceiveReminderAsync(string reminderName, byte[] state, TimeSpan dueTime, TimeSpan period){if (reminderName == "Deposit"){var balance = await StateManager.GetStateAsync<AccountBalance>("balance")!;if (balance.Balance <= 0){balance.Balance += 60; // 50(Overdraft Threshold) + 10 = 60Console.WriteLine("Deposit: 10");}else{Console.WriteLine("Deposit: ignore");}}} }運(yùn)行Assignment.Server
使用Dapr CLI來啟動(dòng),先使用命令行工具跳轉(zhuǎn)到目錄 dapr-study-room\Assignment07\Assignment.Server,然后執(zhí)行下面命令
dapr run --app-id testactor --app-port 5000 --dapr-http-port 3500 --dapr-grpc-port 50001 dotnet run創(chuàng)建Assignment.Client
創(chuàng)建控制臺(tái)項(xiàng)目,并添加Dapr.ActorsNuGet包引用和Assignment.Shared項(xiàng)目引用。
修改Program.cs
using Assignment.Shared; using Dapr.Actors; using Dapr.Actors.Client;Console.WriteLine("Creating a Bank Actor"); var bank = ActorProxy.Create<IBankActor>(ActorId.CreateRandom(), "BankActor"); Parallel.ForEach(Enumerable.Range(1, 10), async i => {while (true){var balance = await bank.GetAccountBalance();Console.WriteLine($"[Worker-{i}] Balance for account '{balance.AccountId}' is '{balance.Balance:c}'.");Console.WriteLine($"[Worker-{i}] Withdrawing '{1m:c}'...");try{await bank.Withdraw(new WithdrawRequest() { Amount = 1m });}catch (ActorMethodInvocationException ex){Console.WriteLine("[Worker-{i}] Overdraft: " + ex.Message);}Task.Delay(1000).Wait();} });Console.ReadKey();運(yùn)行Assignment.Client
使用Dapr CLI來啟動(dòng),先使用命令行工具跳轉(zhuǎn)到目錄 dapr-study-room\Assignment07\Assignment.Client,然后執(zhí)行下面命令
dotnet run本章源碼
Assignment07
https://github.com/doddgu/dapr-study-room
我們正在行動(dòng),新的框架、新的生態(tài)
我們的目標(biāo)是自由的、易用的、可塑性強(qiáng)的、功能豐富的、健壯的。
所以我們借鑒Building blocks的設(shè)計(jì)理念,正在做一個(gè)新的框架MASA Framework,它有哪些特點(diǎn)呢?
原生支持Dapr,且允許將Dapr替換成傳統(tǒng)通信方式
架構(gòu)不限,單體應(yīng)用、SOA、微服務(wù)都支持
支持.Net原生框架,降低學(xué)習(xí)負(fù)擔(dān),除特定領(lǐng)域必須引入的概念,堅(jiān)持不造新輪子
豐富的生態(tài)支持,除了框架以外還有組件庫(kù)、權(quán)限中心、配置中心、故障排查中心、報(bào)警中心等一系列產(chǎn)品
核心代碼庫(kù)的單元測(cè)試覆蓋率90%+
開源、免費(fèi)、社區(qū)驅(qū)動(dòng)
還有什么?我們?cè)诘饶?#xff0c;一起來討論
經(jīng)過幾個(gè)月的生產(chǎn)項(xiàng)目實(shí)踐,已完成POC,目前正在把之前的積累重構(gòu)到新的開源項(xiàng)目中
目前源碼已開始同步到Github(文檔站點(diǎn)在規(guī)劃中,會(huì)慢慢完善起來):
MASA.BuildingBlocks
MASA.Contrib
MASA.Utils
MASA.EShop
BlazorComponent
MASA.Blazor
QQ群:7424099
微信群:加技術(shù)運(yùn)營(yíng)微信(MasaStackTechOps),備注來意,邀請(qǐng)進(jìn)群
總結(jié)
以上是生活随笔為你收集整理的学Dapr Actors 看这篇就够了的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Redis集群之脑裂:一次奇怪的数据丢失
- 下一篇: postgres中的中文分词zhpars