The Dapr actors building block
Dapr actors構建塊
The actor model originated in 1973. It was proposed by Carl Hewitt as a conceptual model of concurrent computation, a form of computing in which several computations are executed at the same time. Highly parallel computers weren't yet available at that time, but the more recent advancements of multi-core CPUs and distributed systems have made the actor model popular.
Actor 模型 起源於Carl Hewitt 在 1973 年提出的作為並發計算的概念模型,這種形式的計算會同時執行多個計算。 當時並沒有高度並行的計算機,但多核 Cpu 和分布式系統的最新進步使得Actor 模型 變得流行。
In the actor model, the actor is an independent unit of compute and state. Actors are completely isolated from each other and they will never share memory. Actors communicate with each other using messages. When an actor receives a message, it can change its internal state, and send messages to other (possibly new) actors.
在Actor 模型中,Actor 是一個計算和狀態獨立的單元。 Actors 完全彼此隔離,它們永遠不會共享內存。 Actors 使用消息相互通信。 當一個Actor 收到消息時,它可以更改其內部狀態,並將消息發送到其他 (可能是新的) Actors。
The reason why the actor model makes writing concurrent systems easier is that it provides a turn-based (or single-threaded) access model. Multiple actors can run at the same time, but each actor will process received messages one at a time. This means that you can be sure that at most one thread is active inside an actor at any time. That makes writing correct concurrent and parallel systems much easier.
Actor模型使得編寫並發系統變得更簡單的,它提供了基於 turn-based 的 (或單線程) 訪問模型。 多個Actors可以同時運行,但每個Actor 一次只處理一個接收的消息。 這意味着,在任何時候,都可以確保在Actors 中最多有一個線程處於活動狀態。 這使得編寫正確的並發系統和並行系統變得更加容易。
What it solves
它解決了什么問題
Actor model implementations are usually tied to a specific language or platform. With the Dapr actors building block however, you can leverage the actor model from any language or platform.
Actor 模型的實現通常綁定到特定語言或平台。 使用 Dapr Actor 構建塊可以從任何語言或平台 來使用 Actor 模型。
Dapr's implementation is based on the virtual actor pattern introduced by Project "Orleans". With the virtual actor pattern, you don't need to explicitly create actors. Actors are activated implicitly and placed on a node in the cluster the first time a message is sent to the actor. When not executing operations, actors are silently unloaded from memory. If a node fails, Dapr automatically moves activated actors to healthy nodes. Besides sending messages between actors, the Dapr actor model also support scheduling future work using timers and reminders.
Dapr 的實現基於 項目 "奧爾良" 中引入的虛擬Actor模式。 對於虛擬Actor模式,不需要顯式的創建Actor。 第一次將消息發送到Actor時,Actor將被隱式激活並放置在群集中的節點上。 當不執行操作時,Actor 會以靜默方式從內存中卸載。 如果某個節點出現故障,Dapr 會自動將激活的Actor 移到正常的節點。 除了在Actor之間發送消息以外,Dapr Actor模型還支持使用計時器和提醒調度將來的工作。
While the actor model can provide great benefits, it's important to carefully consider the actor design. For example, having many clients call the same actor will result in poor performance because the actor operations execute serially. Here are some criteria to check if a scenario is a good fit for Dapr actors:
雖然Actor模型 提供了很大的優勢,但必須仔細考慮Actor的設計。 例如,如果多個客戶端調用相同的Actor,則會導致性能不佳,因為Actor 操作會按順序執行。 下面是用於判斷場景是否適用 Dapr Actor的一些原則:
- Your problem space involves concurrency. Without actors, you'd have to introduce explicit locking mechanisms in your code. 問題空間涉及並發性。 如果沒有Actor,則需要在代碼中引入顯式鎖定機制。
- Your problem space can be partitioned into small, independent, and isolated units of state and logic. 可以將問題空間划分為小、獨立和隔離的狀態和邏輯單元。
- You don't need low-latency reads of the actor state. Low-latency reads cannot be guaranteed because actor operations execute serially. 不需要低延遲的讀取Actor 狀態。 因為Actor 操作是按順序執行,不能保證低延遲讀取。
- You don't need to query state across a set of actors. Querying across actors is inefficient because each actor's state needs to be read individually and can introduce unpredictable latencies. 不需要在一組Actor 之間查詢狀態。 跨Actor 的查詢效率低下,因為每個Actor 的狀態都需要單獨讀取,並且可能會導致不可預測的延遲。
One design pattern that fits these criteria quite well is the orchestration-based saga or process manager design pattern. A saga manages a sequence of steps that must be taken to reach some outcome. The saga (or process manager) maintains the current state of the sequence and triggers the next step. If a step fails, the saga can execute compensating actions. Actors make it easy to deal with concurrency in the saga and to keep track of the current state. The eShopOnDapr reference application uses the saga pattern and Dapr actors to implement the Ordering process.
有一種設計模式非常好地滿足這些條件,就是 基於業務流程的 saga 或 流程管理器 設計模式。 Saga 管理必須執行的一系列步驟才能達到某些結果。 Saga (或進程管理器) 維護序列的當前狀態,並觸發下一步。 如果一個步驟失敗,saga 可以執行補償操作。 利用Actor,可以輕松處理 saga 中的並發,並跟蹤當前狀態。 EShopOnDapr 參考應用程序使用 saga 模式和 Dapr Actor來實現訂單處理。
How it works
工作原理
The Dapr sidecar provides the HTTP/gRPC API to invoke actors. This is the base URL of the HTTP API:
Dapr 邊車提供了用於調用actors的 HTTP/gRPC API。 這是 HTTP API 的基 URL:
http://localhost:<daprPort>/v1.0/actors/<actorType>/<actorId>/
<daprPort>
: the HTTP port that Dapr listens on. Dapr 偵聽的 HTTP 端口。<actorType>
: the actor type. actor類型<actorId>
: the ID of the specific actor to call. 要調用的特定actor的 ID。
The sidecar manages how, when and where each actor runs, and also routes messages between actors. When an actor hasn't been used for a period of time, the runtime deactivates the actor and removes it from memory. Any state managed by the actor is persisted and will be available when the actor re-activates. Dapr uses an idle timer to determine when an actor can be deactivated. When an operation is called on the actor (either by a method call or a reminder firing), the idle timer is reset and the actor instance will remain activated.
邊車管理每個Actor 的運行時間和位置,以及在Actor之間路由消息的方式。 如果一段時間未使用某個Actor,則運行時將停用該actor,並將其從內存中刪除。 Actor 所管理的任何狀態都將被保留,並在Actor 重新激活時可用。 Dapr 使用空閑計時器來確定何時可以停用Actor。 當在Actor 上調用操作時 (通過方法調用或提醒觸發) ,會重置空閑計時器,並保持激活actor實例。
The sidecar API is only one part of the equation. The service itself also needs to implement an API specification, because the actual code that you write for the actor will run inside the service itself. Figure 11-1 shows the various API calls between the service and its sidecar:
邊車 API 只是綜合體的一部分。 服務本身還需要實現 API 規范,因為你為Actor 編寫的實際代碼將在服務本身內運行。 圖11-1 顯示了服務和它的邊車之間的各種 API 調用:
圖 11-1。 actor服務和 Dapr 邊車之間的 API 調用。
To provide scalability and reliability, actors are partitioned across all the instances of the actor service. The Dapr placement service is responsible for keeping track of the partitioning information. When a new instance of an actor service is started, the sidecar registers the supported actor types with the placement service. The placement service calculates the updated partitioning information for the given actor type and broadcasts it to all instances. Figure 11-2 shows what happens when a service is scaled out to a second replica:
為了提供可伸縮性和可靠性,將在所有Actor服務實例中對actor進行分區。 Dapr placement 服務負責跟蹤分區信息。 啟動Actor 服務的新實例時,其邊車會將支持的Actor 類型注冊到placement 服務。 placement 服務計算給定Actor 類型的更新分區信息,並將其廣播給所有服務實例。 圖11-2 顯示了將服務擴展到第二個副本時發生的情況:
Figure 11-2. Actor placement service.
圖 11-2。 actor放置服務。
- On startup, the sidecar makes a call to the actor service to get the registered actor types as well as actor configuration settings. 啟動時,邊車調用actor服務以獲取注冊的Actor類型和Actor的配置設置。
- The sidecar sends the list of registered actor types to the placement service. 邊車將注冊的Actor類型的列表發送到placement 服務。
- The placement service broadcasts the updated partitioning information to all actor service instances. Each instance will keep a cached copy of the partitioning information and use it to invoke actors. placement 服務會將更新的分區信息廣播到所有Actor服務實例。 每個Actor服務實例都將保留分區信息的緩存副本,並使用它來調用Actor。
Important
重要
Because actors are randomly distributed across service instances, it should be expected that an actor operation always requires a call to a different node in the network.
由於actor是在各服務實例間隨機分布的,因此Actor 始終需要調用網絡中的其他節點。
The next figure shows an ordering service instance running in Pod 1 call the ship
method of an OrderActor
instance with ID 3
. Because the actor with ID 3
is placed in a different instance, this results in a call to a different node in the cluster:
下圖顯示了在 Pod 1 中運行的訂購服務實例調用 ID 為3的OrderActor
實例的ship
方法 。 由於 ID為3的Orderactor 放在不同的服務實例中,因此將導致調用群集中的不同節點:
Figure 11-3. Calling an actor method.
圖 11-3。 調用actor方法。
- The service calls the actor API on the sidecar. The JSON payload in the request body contains the data to send to the actor. 服務在邊車上調用Actor API。 請求正文中的 JSON 有效負載包含要發送到Actor 的數據。
- The sidecar uses the locally cached partitioning information from the placement service to determine which actor service instance (partition) is responsible for hosting the actor with ID
3
. In this example, it's the service instance in pod 2. The call is forwarded to the appropriate sidecar. 邊車使用來自placement 服務中的本地緩存的分區信息來確定哪個Actor服務實例 (分區) 負責托管 ID 為3的OrderActor 。 在此示例中,它是 pod 2 中的服務實例。 調用將轉發到相應的邊車。 - The sidecar instance in pod 2 calls the service instance to invoke the actor. The service instance activates the actor (if it hasn't already) and executes the actor method. Pod 2 中的邊車實例調用服務實例以調用Actor。 如果Actor尚未准備就緒,則該服務實例將激活該Actor,並執行Actor方法。
Turn-based access model
回合制訪問模型
The turn-based access model ensures that at any time there's at most one thread active inside an actor instance. To understand why this is useful, consider the following example of a method that increments a counter value:
回合制訪問模型可確保任何時候在一個actor實例內最多只有一個線程處於活動狀態。 若要了解為什么這是有用的,請考慮以下用於遞增計數器值的方法示例:
public int Increment() { var currentValue = GetValue(); var newValue = currentValue + 1; SaveValue(newValue); return newValue; }
Let's assume that the current value returned by the GetValue
method is 1
. When two threads call the Increment
method at the same time, there's a risk of both of them calling the GetValue
method before one of them calls SaveValue
. This results in both threads starting with the same initial value (1
). The threads then increment the value to 2
and return it to the caller. The resulting value after the two calls is now 2
instead of 3
which it should be. This is a simple example to illustrate the kind of issues that can slip into your code when working with multiple threads, and is easy to solve. In real world applications however, concurrent and parallel scenarios can become very complex.
假設 GetValue
方法返回的當前值 為 1
。 當兩個線程同時調用 Increment
方法時,有個風險,一個線程會在另一個線程調用 SaveValue
方法之前調用 GetValue
方法 。 這會導致兩個線程以相同初始值 (1
) 開始。 然后,線程遞增值並將 2
其返回給調用方。 現在,兩次調用后的結果值是 2
而不是值 3(期望值是3)
。 這是一個簡單的示例,說明了在使用多個線程時可能會陷入代碼的這類問題,並且很容易解決。 但在實際應用程序中,並發和並行場景可能會變得非常復雜。
In traditional programming models, you can solve this problem by introducing locking mechanisms. For example:
在傳統編程模型中,可以通過引入鎖定機制來解決此問題。 例如:
public int Increment() { int newValue; lock (_lockObject) { var currentValue = GetValue(); newValue = currentValue + 1; SaveValue(newValue); } return newValue; }
Unfortunately, using explicit locking mechanisms is error-prone. They can easily lead to deadlocks and can have serious impact on performance.
遺憾的是,使用顯式鎖定機制容易出錯。 它們很容易導致死鎖,並可能對性能造成嚴重影響。
Thanks to the turn-based access model, you don't need to worry about multiple threads with actors, making it much easier to write concurrent systems. The following actor example closely mirrors the code from the previous sample, but doesn't require any locking mechanisms to be correct:
由於 回合制 訪問模型,您無需擔心在多個線程中使用actors,使編寫並發系統變得更加容易。 下面的actor示例與上一個示例中的代碼密切相關,但不需要任何鎖定機制來保障正確:
public async Task<int> IncrementAsync() { var counterValue = await StateManager.TryGetStateAsync<int>("counter"); var currentValue = counterValue.HasValue ? counterValue.Value : 0; var newValue = currentValue + 1; await StateManager.SetStateAsync("counter", newValue); return newValue; }
Timers and reminders
定時器和提醒器
Actors can use timers and reminders to schedule calls to themselves. Both concepts support the configuration of a due time. The difference lies in the lifetime of the callback registrations:
Actors可以使用定時器和提醒器來調度自身。 這兩個概念都支持配置首次執行等待時間。 不同之處在於回調注冊的生存期:
- Timers will only stay active as long as the the actor is activated. Timers will not reset the idle-timer, so they cannot keep an actor active on their own. 只要actor是活動狀態,定時器就會保持活動狀態。 定時器 不會 重置空閑計時器,因此它們不能使actor處於活動狀態。
- Reminders outlive actor activations. If an actor is deactivated, a reminder will re-activate the actor. Reminders will reset the idle-timer. 提醒器的生命周期長於actor。 如果某個actor失活,提醒器會重新激活該actor。 提醒器 將 重置空閑計時器。
Timers are registered by making a call to the actor API. In the following example, a timer is registered with a due time of 0 and a period of 10 seconds.
定時器是通過調用actor API 來注冊的。 在下面的示例中,注冊了一個定時器,首次執行等待時間為0秒,周期時間為10秒。
curl -X POST http://localhost:3500/v1.0/actors/<actorType>/<actorId>/timers/<name> \ -H "Content-Type: application/json" \ -d '{ "dueTime": "0h0m0s0ms", "period": "0h0m10s0ms" }'
Because the due time is 0, the timer will fire immediately. After a timer callback has finished, the timer will wait 10 seconds before firing again.
由於首次執行等待時間為0,因此將立即觸發定時器。 計時器回調完成后,計時器將等待10秒,然后再次觸發。
Reminders are registered in a similar way. The following example shows a reminder registration with a due time of 5 minutes, and an empty period:
提醒器注冊方式類似。 下面的示例演示了一個提醒器注冊,該提醒器注冊的首次執行等待時間為5分鍾,周期時間為空:
curl -X POST http://localhost:3500/v1.0/actors/<actorType>/<actorId>/reminders/<name> \ -H "Content-Type: application/json" \ -d '{ "dueTime": "0h5m0s0ms", "period": "" }'
This reminder will fire in 5 minutes. Because the given period is empty, this will be a one-time reminder.
此提醒器將在5分鍾后觸發。 由於給定周期時間段為空,意味這是一次性提醒。
Note
備注
Timers and reminders both respect the turn-based access model. When a timer or reminder fires, the callback will not be executed until any other method invocation or timer/reminder callback has finished.
定時器和提醒器均遵循回合制訪問模型。 當定時器或提醒觸發時,直到任何其他方法調用或定時器/提醒回調完成后才會執行回調。
State persistence
狀態持久性
Actor state is persisted using the Dapr state management building block. Because actors can execute multiple state operations in a single turn, the state store component must support multi-item transactions. At the time of writing, the following state stores support multi-item transactions:
使用 Dapr 狀態管理構建塊保存Actor狀態。 由於actors可以在一個回合執行多個狀態操作,因此狀態存儲組件必須支持多項事務。 撰寫本文時,以下狀態存儲支持多項事務:
- Azure Cosmos DB
- MongoDB
- MySQL
- PostgreSQL
- Redis
- RethinkDB
- SQL Server
To configure a state store component for use with actors, you need to append the following metadata to the state store configuration:
若要配置要與actors一起使用的狀態存儲組件,需要將以下元數據附加到狀態存儲配置:
- name: actorStateStore
value: "true"
Here's a complete example for a Redis state store:
下面是 Redis 狀態存儲的完整示例:
apiVersion: dapr.io/v1alpha1 kind: Component metadata: name: statestore spec: type: state.redis version: v1 metadata: - name: redisHost value: localhost:6379 - name: redisPassword value: "" - name: actorStateStore value: "true"
Use the Dapr .NET SDK
使用 Dapr .NET SDK
You can create an actor model implementation using only HTTP/gRPC calls. However, it's much more convenient to use the language specific Dapr SDKs. At the time of writing, the .NET, Java and Python SDKs all provide extensive support for working with actors.
只能使用 HTTP/gRPC 調用創建Actor模型實現。 但是,更方便的方法是使用特定於語言的 Dapr Sdk。 撰寫本文時,.NET、Java 和 Python Sdk 都為使用參與者提供了廣泛的支持。
To get started with the .NET Dapr actors SDK, you add a package reference to Dapr.Actors to your service project. The first step of creating an actual actor is to define an interface that derives from IActor
. Clients use the interface to invoke operations on the actor. Here's a simple example of an actor interface for keeping scores:
若要開始使用 .NET Dapr actors SDK,你需要為你的服務項目添加對 Dapr.Actors 包的引用。 創建實際actor的第一步是定義從 IActor
派生的接口。 客戶端使用接口調用actor上的操作。 下面是一個簡單的actor接口示例,用於保存分數:
public interface IScoreActor : IActor { Task<int> IncrementScoreAsync(); Task<int> GetScoreAsync(); }
Important
重要
The return type of an actor method must be Task
or Task<T>
. Also, actor methods can have at most one argument. Both the return type and the arguments must be System.Text.Json
serializable.
actor方法的返回類型必須為 Task
或 Task<T>
。 此外,actor方法最多只能有一個參數。 返回類型和參數都必須支持 System.Text.Json
序列化。
Next, implement the actor by deriving a ScoreActor
class from Actor
. The ScoreActor
class must also implement the IScoreActor
interface:
接下來,ScoreActor通過從 Actor
類派生來實現actor 。 ScoreActor
類還必須實現 IScoreActor
接口:
public class ScoreActor : Actor, IScoreActor { public ScoreActor(ActorHost host) : base(host) { } // TODO Implement interface methods. }
The constructor in the snippet above takes a host
argument of type ActorHost
. The ActorHost
class represents the host for an actor type within the actor runtime. You need to pass this argument to the constructor of the Actor
base class. Actors also support dependency injection. Any additional arguments that you add to the actor constructor are resolved using the .NET dependency injection container.
上面代碼段中的構造函數采用 ActorHost
類型的參數 host。 ActorHost
類表示actor運行時中的actor類型的宿主。 需要將此參數傳遞給基類 Actor
的構造函數。 Actors還支持依賴項注入。 使用 .NET 依賴注入容器來解析添加到actor構造函數的任何其他參數。
Let's now implement the IncrementScoreAsync
method of the interface:
現在,讓我們實現接口的 IncrementScoreAsync
方法:
public Task<int> IncrementScoreAsync() { return StateManager.AddOrUpdateStateAsync( "score", 1, (key, currentScore) => currentScore + 1 ); }
In the snippet above, a single call to StateManager.AddOrUpdateStateAsync
provides the full implementation for the IncrementScoreAsync
method. The AddOrUpdateStateAsync
method takes three arguments:
在上面的代碼片段中,IncrementScoreAsync
方法實現中只調用了一次 StateManager.AddOrUpdateStateAsync
。 StateManager.AddOrUpdateStateAsync
方法采用三個參數:
- The key of the state to update. 要更新的狀態的鍵。
- The value to write if no score is stored in the state store yet. 如果尚未將評分存儲在狀態存儲中,則為要寫入的值。
- A
Func
to call if there already is a score stored in the state store. It takes the state key and current score, and returns the updated score to write back to the state store. Lambda表示的Func
在狀態存儲中已有分數存儲時調用。 它使用狀態鍵和當前評分,並返回更新后的分數寫回到狀態存儲區。
The GetScoreAsync
implementation reads the current score from the state store and returns it to the client:
GetScoreAsync的
實現讀取狀態存儲中的當前評分,並將其返回給客戶端:
public async Task<int> GetScoreAsync() { var scoreValue = await StateManager.TryGetStateAsync<int>("score"); if (scoreValue.HasValue) { return scoreValue.Value; } return 0; }
To host actors in an ASP.NET Core service, you must add a reference to the Dapr.Actors.AspNetCore package and make some changes to the Startup
class. In the following example, the Configure
method adds the actor endpoints by calling endpoints.MapActorsHandlers
:
若要在 ASP.NET Core 服務中承載actors,你必須添加對 Dapr.Actors.AspNetCore 包的引用並對 Startup
類進行一些更改。 在下面的示例中, Configure
方法通過調用 endpoints.MapActorsHandlers
來添加actor終結點:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { // ... // Actors building block does not support HTTPS redirection. //app.UseHttpsRedirection(); app.UseEndpoints(endpoints => { // Add actor endpoints. endpoints.MapActorsHandlers(); endpoints.MapControllers(); }); }
The actors endpoints are necessary because the Dapr sidecar calls the application to host and interact with actor instances.
actors終結點是必需的,因為 Dapr 邊車調用應用程序來承載和與actor實例進行交互。
Important
重要
Make sure your Startup
class does not contain an app.UseHttpsRedirection
call to redirect clients to the HTTPS endpoint. This will not work with actors. By design, a Dapr sidecar sends requests over unencrypted HTTP by default. The HTTPS middleware will block these requests when enabled.
請確保你的 Startup
類不包含將客戶端重定向到 HTTPS 終結點的 app.UseHttpsRedirection
調用。 這不適用於actors。 按照設計,默認情況下,Dapr 連車通過未加密的 HTTP 發送請求。 啟用HTTPS后,HTTPS 中間件會阻止這些請求。
The Startup
class is also the place to register the specific actor types. In the example below, ConfigureServices
registers the ScoreActor
using services.AddActors
:
Startup
類也是用於注冊特定actor類型的位置。 在下面的示例中, ConfigureServices
使用 services.AddActors
注冊 ScoreActor
:
public void ConfigureServices(IServiceCollection services) { // ... services.AddActors(options => { options.Actors.RegisterActor<ScoreActor>(); }); }
At this point, the ASP.NET Core service is ready to host the ScoreActor
and accept incoming requests. Client applications use actor proxies to invoke operations on actors. The following example shows how a console client application invokes the IncrementScoreAsync
operation on a ScoreActor
instance:
此時,ASP.NET Core 服務已准備好承載 ScoreActor
和接受傳入的請求。 客戶端應用程序使用actor代理來調用actor上的操作。 下面的示例演示了控制台客戶端應用程序如何調用ScoreActor
實例的IncrementScoreAsync
操作 :
static async Task MainAsync(string[] args) { var actorId = new ActorId("scoreActor1"); var proxy = ActorProxy.Create<IScoreActor>(actorId, "ScoreActor"); var score = await proxy.IncrementScoreAsync(); Console.WriteLine($"Current score: {score}"); }
The above example uses the Dapr.Actors package to call the actor service. To invoke an operation on an actor, you need to be able to address it. You'll need two parts for this:
上面的示例使用 Dapr.Actors 包來調用actor服務。 若要在actor上調用操作,需要能夠定位到它。 此操作需要兩部分:
- The actor type uniquely identifies the actor implementation across the whole application. By default, the actor type is the name of the implementation class (without namespace). You can customize the actor type by adding an
ActorAttribute
to the implementation class and setting itsTypeName
property. actor的類型在整個應用程序中唯一標識actor的實現。 默認情況下,actor的類型是實現類的名稱(不包含命名空間)。 可以通過為實現類添加ActorAttribute並設置其TypeName
屬性,來自定義actor的類型。 - The
ActorId
uniquely identifies an instance of an actor type. You can also use this class to generate a random actor id by callingActorId.CreateRandom
.ActorId
唯一標識actor類型的實例。 還可以通過調用ActorId.CreateRandom
來生成隨機的 actor id。
The example uses ActorProxy.Create
to create a proxy instance for the ScoreActor
. The Create
method takes two arguments: the ActorId
identifying the specific actor and the actor type. It also has a generic type parameter to specify the actor interface that the actor type implements. As both the server and client applications need to use the actor interfaces, they're typically stored in a separate shared project.
該示例使用 ActorProxy.Create
為 ScoreActor
創建代理實例。 Create
方法采用兩個參數:ActorId
(用於標識某個特定actor)和actor的類型。 它還具有一個泛型類型參數,用於指定actor的類型所實現的actor接口。 由於服務器和客戶端應用程序都需要使用actor接口,actor接口通常封裝在單獨的共享項目中。
The final step in the example calls the IncrementScoreAsync
method on the actor and outputs the result. Remember that the Dapr placement service distributes the actor instances across the Dapr sidecars. Therefore, expect an actor call to be a network call to another node.
該示例中的最后一個步驟調用 actor上的IncrementScoreAsync
方法並輸出結果。 請記住,Dapr placement 服務跨 Dapr 邊車分發actor實例。 因此,需要將actor 調用作為對另一個節點的網絡調用。
Call actors from ASP.NET Core clients
從 ASP.NET Core 客戶端調用actors
The console client example in the previous section uses the static ActorProxy.Create
method directly to get an actor proxy instance. If the client application is an ASP.NET Core application, you should use the IActorProxyFactory
interface to create actor proxies. The main benefit is that it allows you to manage configuration centrally in the ConfigureServices
method. The AddActors
method takes a delegate that allows you to specify actor runtime options, such as the HTTP endpoint of the Dapr sidecar. The following example specifies custom JsonSerializerOptions
to use for actor state persistence and message deserialization:
上一節中的控制台客戶端示例直接使用靜態 ActorProxy.Create
方法獲取actor代理的實例。 如果客戶端應用程序是 ASP.NET Core 應用程序,則應使用 IActorProxyFactory
接口創建actor代理。 主要優點是它允許您d在 ConfigureServices
方法中集中管理配置。 AddActors
方法參數是一個委托,該委托允許指定actor運行時選項,如 Dapr 邊車的 HTTP 終結點。 下面的示例指定了用於actor狀態持久性和消息反序列化的自定義JsonSerializerOptions
:
public void ConfigureServices(IServiceCollection services) { // ... services.AddActors(options => { var jsonSerializerOptions = new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNameCaseInsensitive = true }; options.JsonSerializerOptions = jsonSerializerOptions; options.Actors.RegisterActor<ScoreActor>(); }); }
The call to AddActors
registers the IActorProxyFactory
for .NET dependency injection. This allows ASP.NET Core to inject an IActorProxyFactory
instance into your controller classes. The following example calls an actor method from an ASP.NET Core controller class:
調用AddActors將
IActorProxyFactory
注冊到.net 依賴項注入容器中。 這允許 ASP.NET Core 將IActorProxyFactory
實例注入到控制器類。 下面的示例從 ASP.NET Core 控制器類調用actor方法:
[ApiController] [Route("[controller]")] public class ScoreController : ControllerBase { private readonly IActorProxyFactory _actorProxyFactory; public ScoreController(IActorProxyFactory actorProxyFactory) { _actorProxyFactory = actorProxyFactory; } [HttpPut("{scoreId}")] public Task<int> IncrementAsync(string scoreId) { var scoreActor = _actorProxyFactory.CreateActorProxy<IScoreActor>( new ActorId(scoreId), "ScoreActor"); return scoreActor.IncrementScoreAsync(); } }
Actors can also call other actors directly. The Actor
base class exposes an IActorProxyFactory
class through the ProxyFactory
property. To create an actor proxy from within an actor, use the ProxyFactory
property of the Actor
base class. The following example shows an OrderActor
that invokes operations on two other actors:
Actors還可以直接調用其他actors。 Actor
基類通過ProxyFactory
屬性公開 IActorProxyFactory
類 。 若要從actor中創建actor代理,請使用Actor
基類的 ProxyFactory
屬性 。 下面的示例演示一個 OrderActor
,它調用兩個其他actors的操作:
public class OrderActor : Actor, IOrderActor { public OrderActor(ActorHost host) : base(host) { } public async Task ProcessOrderAsync(Order order) { var stockActor = ProxyFactory.CreateActorProxy<IStockActor>( new ActorId(order.OrderNumber), "StockActor"); await stockActor.ReserveStockAsync(order.OrderLines); var paymentActor = ProxyFactory.CreateActorProxy<IPaymentActor>( new ActorId(order.OrderNumber), "PaymentActor"); await paymentActor.ProcessPaymentAsync(order.PaymentDetails); } }
Note
備注
By default, Dapr actors aren't reentrant. This means that a Dapr actor cannot be called more than once in the same chain. For example, the call chain Actor A -> Actor B -> Actor A
is not allowed. At the time of writing, there's a preview feature available to support reentrancy. However, there is no SDK support yet. For more details, see the official documentation.
默認情況下,Dapr 執行組件不可重入。 這意味着不能在同一鏈中多次調用同一個 Dapr action。 例如,調用鏈 Actor A -> Actor B -> Actor A
是不被允許的。 撰寫本文時,有一個預覽功能可用於支持重入。 但是,尚無 SDK 支持。 有關更多詳細信息,請參閱 官方文檔。
Call non-.NET actors
調用非.NET actors
So far, the examples used strongly-typed actor proxies based on .NET interfaces to illustrate actor invocations. This works great when both the actor host and client are .NET applications. However, if the actor host is not a .NET application, you don't have an actor interface to create a strongly-typed proxy. In these cases, you can use a weakly-typed proxy.
到目前為止,這些示例使用基於 .NET 接口的強類型actor代理來說明actor調用。 當actor宿主和客戶端都是 .NET 應用程序時,這非常有效。 但是,如果actor宿主不是 .NET 應用程序,則沒有創建強類型代理的actor接口。 在這些情況下,可以使用弱類型代理。
You create weakly-typed proxies in a similar way to strongly-typed proxies. Instead of relying on a .NET interface, you need to pass in the actor method name as a string.
創建弱類型代理的方式與強類型代理類似。 需要將actor方法名稱作為字符串傳遞,而不是依賴於 .NET 接口。
[HttpPut("{scoreId}")] public Task<int> IncrementAsync(string scoreId) { var scoreActor = _actorProxyFactory.CreateActorProxy( new ActorId(scoreId), "ScoreActor"); return scoreActor("IncrementScoreAsync"); }
Timers and reminders
定時器和提醒器
Use the RegisterTimerAsync
method of the Actor
base class to schedule actor timers. In the following example, a TimerActor
exposes a StartTimerAsync
method. Clients can call the method to start a timer that repeatedly writes a given text to the log output.
使用 Actor
基類的 RegisterTimerAsync
方法預置actor定時器。 在下面的示例中, TimerActor
公開 StartTimerAsync
方法。 客戶端可以調用 StartTimerAsync
方法來啟動一個定時器,該定時器將給定的文本重復寫入輸出日志。
public class TimerActor : Actor, ITimerActor { public TimerActor(ActorHost host) : base(host) { } public Task StartTimerAsync(string name, string text) { return RegisterTimerAsync( name, nameof(TimerCallback), Encoding.UTF8.GetBytes(text), TimeSpan.Zero, TimeSpan.FromSeconds(3)); } public Task TimerCallbackAsync(byte[] state) { var text = Encoding.UTF8.GetString(state); Logger.LogInformation($"Timer fired: {text}"); return Task.CompletedTask; } }
The StartTimerAsync
method calls RegisterTimerAsync
to schedule the timer. RegisterTimerAsync
takes five arguments:
StartTimerAsync
方法調用 RegisterTimerAsync
來預置定時器。 RegisterTimerAsync
采用了五個參數:
- The name of the timer. 定時器的名稱。
- The name of the method to call when the timer fires. 定時器定時調用的回調函數。
- The state to pass to the callback method. 傳給回調函數的狀態,即實參。
- The amount of time to wait before the callback method is first invoked. 首次調用回調函數等待的時間。
- The time interval between callback method invocations. You can specify
TimeSpan.FromMilliseconds(-1)
to disable periodic signaling. 回調函數調用之間的時間間隔。 可以指定 以TimeSpan.FromMilliseconds(-1)
來禁用定期信號。
The TimerCallbackAsync
method receives the user state in binary form. In the example, the callback decodes the state back to a string
before writing it to the log.
TimerCallbackAsync
方法以二進制形式接收用戶狀態。 在示例中,回調在將狀態寫入日志之前將狀態解碼回string
。
Timers can be stopped by calling UnregisterTimerAsync
:
可以通過調用UnregisterTimerAsync
來注銷定時器 :
public class TimerActor : Actor, ITimerActor { // ... public Task StopTimerAsync(string name) { return UnregisterTimerAsync(name); } }
Remember that timers do not reset the actor idle timer. When no other calls are made on the actor, it may be deactivated and the timer will be stopped automatically. To schedule work that does reset the idle timer, use reminders which we'll look at next.
請記住,定時器不會重置actor空閑計時器。 當actor上未進行其他調用時,可能會停用該actor,並且定時器將自動停止。 若要安排重置空閑計時器的工作,請使用我們接下來將提到的提醒器。
To use reminders in an actor, your actor class must implement the IRemindable
interface:
若要在actor中使用提醒器,actor類必須實現 IRemindable
接口:
public interface IRemindable { Task ReceiveReminderAsync( string reminderName, byte[] state, TimeSpan dueTime, TimeSpan period); }
The ReceiveReminderAsync
method is called when a reminder is fired. It takes 4 arguments:
提醒器定時時調用ReceiveReminderAsync方法。 它采用 4 個參數:
- The name of the reminder. 提醒器的名稱。
- The user state provided during registration. 注冊期間提供的用戶狀態。
- The invocation due time provided during registration. 注冊期間提供的首次調用等待時間。
- The invocation period provided during registration. 注冊期間提供的調用周期(兩次調用的時間間隔)。
To register a reminder, use the RegisterReminderAsync
method of the actor base class. The following example sets a reminder to fire a single time with a due time of three minutes.
若要注冊提醒器,請使用 actor基類的 RegisterReminderAsync
方法。 以下示例設置一個提醒器,首次調用等待時間為3分鍾,只觸發一次。
public class ReminderActor : Actor, IReminderActor, IRemindable { public ReminderActor(ActorHost host) : base(host) { } public Task SetReminderAsync(string text) { return RegisterReminderAsync( "DoNotForget", Encoding.UTF8.GetBytes(text), TimeSpan.FromSeconds(3), TimeSpan.FromMilliseconds(-1)); } public Task ReceiveReminderAsync( string reminderName, byte[] state, TimeSpan dueTime, TimeSpan period) { if (reminderName == "DoNotForget") { var text = Encoding.UTF8.GetString(state); Logger.LogInformation($"Don't forget: {text}"); } return Task.CompletedTask; } }
The RegisterReminderAsync
method is similar to RegisterTimerAsync
but you don't have to specify a callback method explicitly. As the above example shows, you implement IRemindable.ReceiveReminderAsync
to handle fired reminders.
RegisterReminderAsync
方法類似於 RegisterTimerAsync
,但不必顯式指定回調方法。 如上面的示例所示,實現 IRemindable.ReceiveReminderAsync
以處理觸發的提醒。
Reminders both reset the idle timer and are persistent. Even if your actor is deactivated, it will be reactivated at the moment a reminder fires. To stop a reminder from firing, call UnregisterReminderAsync
.
提醒同時重置空閑計時器和持久性。 即使actor已失活,也會在觸發提醒時重新激活。 若要停止觸發提醒,請調用 UnregisterReminderAsync
。
Sample application: Dapr Traffic Control
示例應用程序:Dapr 交通控制
The default version of Dapr Traffic Control does not use the actor model. However, it does contain an alternative actor-based implementation of the TrafficControl service that you can enable. To make use of actors in the TrafficControl service, open up the src/TrafficControlService/Controllers/TrafficController.cs
file and uncomment the USE_ACTORMODEL
statement at the top of the file:
Dapr 交通控制的默認版本不使用actor模型。 但是,它確實包含可以啟用的基於actor的TrafficControl 服務的替代實現。 若要使用 TrafficControl 服務中的actors,請打開 src/TrafficControlService/Controllers/TrafficController.cs
文件並取消注釋文件頂部的 USE_ACTORMODEL
語句:
#define USE_ACTORMODEL
When the actor model is enabled, the application uses actors to represent vehicles. The operations that can be invoked on the vehicle actors are defined in an IVehicleActor
interface:
啟用actor模型后,應用程序將使用actors來表示車輛。 可在車輛 actors上調用在IVehicleActor
接口中定義的操作:
public interface IVehicleActor : IActor { Task RegisterEntryAsync(VehicleRegistered msg); Task RegisterExitAsync(VehicleRegistered msg); }
The (simulated) entry cameras call the RegisterEntryAsync
method when a new vehicle is first detected in the lane. The only responsibility of this method is storing the entry timestamp in the actor state:
首次檢測到車輛時,模擬入口相機會調用 RegisterEntryAsync
方法。 此方法的唯一責任是在actor狀態中存儲進入時間戳:
var vehicleState = new VehicleState { LicenseNumber = msg.LicenseNumber, EntryTimestamp = msg.Timestamp }; await StateManager.SetStateAsync("VehicleState", vehicleState);
When the vehicle reaches the end of the speed camera zone, the exit camera calls the RegisterExitAsync
method. The RegisterExitAsync
method first gets the current states and updates it to include the exit timestamp:
當車輛到達速度相機區域末尾時,退出相機調用 RegisterExitAsync
方法。 RegisterExitAsync
方法首先獲取當前狀態並更新它以包括退出時間戳:
var vehicleState = await StateManager.GetStateAsync<VehicleState>("VehicleState"); vehicleState.ExitTimestamp = msg.Timestamp;
Note
備注
The code above currently assumes that a VehicleState
instance has already been saved by the RegisterEntryAsync
method. The code could be improved by first checking to make sure the state exists. Thanks to the turn-based access model, no explicit locks are required in the code.
上面的代碼當前假定VehicleState
實例已由 RegisterEntryAsync
方法保存。 可以通過首先檢查確保狀態存在,來改進代碼。 得益於基於回合的訪問模型,代碼中不需要顯式鎖。
After the state is updated, the RegisterExitAsync
method checks if the vehicle was driving too fast. If it was, the actor publishes a message to the collectfine
pub/sub topic:
狀態更新后, RegisterExitAsync
方法將檢查車輛是否駕駛速度過快。 如果是,則actor將消息發布到 collectfine
發布/訂閱 主題:
int violation = _speedingViolationCalculator.DetermineSpeedingViolationInKmh( vehicleState.EntryTimestamp, vehicleState.ExitTimestamp); if (violation > 0) { var speedingViolation = new SpeedingViolation { VehicleId = msg.LicenseNumber, RoadId = _roadId, ViolationInKmh = violation, Timestamp = msg.Timestamp }; await _daprClient.PublishEventAsync("pubsub", "collectfine", speedingViolation); }
The code above uses two external dependencies. The _speedingViolationCalculator
encapsulates the business logic for determining whether or not a vehicle has driven too fast. The _daprClient
allows the actor to publish messages using the Dapr pub/sub building block.
上面的代碼使用兩個外部依賴項。 _speedingViolationCalculator
封裝用於確定車輛是否駕駛速度過快的業務邏輯。 _daprClient
允許actor使用 Dapr pub/sub 構建基塊發布消息。
Both dependencies are registered in the Startup
class and injected into the actor using constructor dependency injection:
這兩個依賴項在 Startup
類中注冊,並且使用構造函數依賴項注入注入到actor中:
private readonly DaprClient _daprClient; private readonly ISpeedingViolationCalculator _speedingViolationCalculator; private readonly string _roadId; public VehicleActor( ActorHost host, DaprClient daprClient, ISpeedingViolationCalculator speedingViolationCalculator) : base(host) { _daprClient = daprClient; _speedingViolationCalculator = speedingViolationCalculator; _roadId = _speedingViolationCalculator.GetRoadId(); }
The actor based implementation no longer uses the Dapr state management building block directly. Instead, the state is automatically persisted after each operation is executed.
基於actor的實現不再直接使用 Dapr 狀態管理構建基塊。 而是在執行每個操作后自動保存狀態。
Summary
總結
The Dapr actors building block makes it easier to write correct concurrent systems. Actors are small units of state and logic. They use a turn-based access model which saves you from having to use locking mechanisms to write thread-safe code. Actors are created implicitly and are silently unloaded from memory when no operations are performed. Any state stored in the actor is automatically persisted and loaded when the actor is reactivated. Actor model implementations are typically created for a specific language or platform. With the Dapr actors building block however, you can leverage the actor model from any language or platform.
Dapr actors構建基塊可以更輕松地編寫正確的並發系統。 Actors是封裝狀態和邏輯的小單元。 它們使用基於回合的訪問模型,無需使用鎖定機制編寫線程安全代碼。 Actors是隱式創建的,不再執行任何操作時以靜默方式從內存中卸載。 自動持久保存actor中存儲的任何狀態,並且在重新激活actor時自動加載狀態。 Actor模型實現通常為特定語言或平台創建的。 但是,借助 Dapr actors構建基塊,可以在任何語言或平台上使用actor模型。
Actors support timers and reminders to schedule future work. Timers do not reset the idle timer and will allow the actor to be deactivated when no other operations are performed. Reminders do reset the idle timer and are also persisted automatically. Both timers and reminders respect the turn-based access model, making sure that no other operations can execute while the timer/reminder events are handled.
Actors支持定時器和提醒器來安排將來的工作。 定時器不會重置空閑計時器,並且允許actor不再執行其他操作時停用。 提醒器會重置空閑計時器,並且也會自動保持。 定時器和提醒器都遵守基於回合的訪問模型,確保在處理定時器/提醒事件時無法執行任何其他操作。
Actor state is persisted using the Dapr state management building block. Any state store that supports multi-item transactions can be used to store actor state.
使用 Dapr 狀態管理構建基塊 持久保存actor狀態。 支持多項事務的任何狀態存儲都可用於存儲actor狀態。
附錄:基於回合的訪問
摘自https://docs.dapr.io/developing-applications/building-blocks/actors/actors-overview/#turn-based-access
A turn consists of the complete execution of an actor method in response to a request from other actors or clients, or the complete execution of a timer/reminder callback. Even though these methods and callbacks are asynchronous, the Dapr Actors runtime does not interleave them. A turn must be fully finished before a new turn is allowed. In other words, an actor method or timer/reminder callback that is currently executing must be fully finished before a new call to a method or callback is allowed. A method or callback is considered to have finished if the execution has returned from the method or callback and the task returned by the method or callback has finished. It is worth emphasizing that turn-based concurrency is respected even across different methods, timers, and callbacks.
一個回合表示一個actor方法對來自其它actors或客戶端的請求進行響應並全部執行完畢(一次完整執行),或是表示定時器/提醒器回調函數的一次完整執行。即便這些方法和回調是異步的,Dapr Actors運行時不會並行調用它們。一個回合必須在開始另一個回合之前完全結束。換句話說,當前被調用的actor方法或定時器/提醒器回調函數必須在另一個方法或回調被調用時完全結束(完成執行)。如果方法或回調的已經返回,且方法或回調返回的任務也已完成,那么這個方法或回調才被認為是執行完畢的(結束了一個回合的)。值得強調的是,對於同一個actor實例,即便是跨不同方法,定時器和回調之間的基於回合的並發也是遵從此規則的。
The Dapr actors runtime enforces turn-based concurrency by acquiring a per-actor lock at the beginning of a turn and releasing the lock at the end of the turn. Thus, turn-based concurrency is enforced on a per-actor basis and not across actors. Actor methods and timer/reminder callbacks can execute simultaneously on behalf of different actors.
Dapr actors運行時通過在開始一個回合時取得actor鎖,並在結束這個回合時釋放這個actor鎖,以實現強制基於回合的並發的目的。因此,基於回合的並發是在每個actor內部強制執行,而不是跨不同actor。不同的actors的方法和定時器/提醒器回調可以同時執行。
The following example illustrates the above concepts. Consider an actor type that implements two asynchronous methods (say, Method1 and Method2), a timer, and a reminder. The diagram below shows an example of a timeline for the execution of these methods and callbacks on behalf of two actors (ActorId1 and ActorId2) that belong to this actor type.
下面的示例演示了上面的概念。考慮一個actor類型有兩個異步方法(例如,Method1和Method2),一個定時器和一個提醒器。下圖顯示同屬於這個actor類型的兩個actor,分別表示為ActorId1和ActorId2,以及它們的方法和回調函數的運行的時間線。