Fork me on GitHub

面向.NET开发人员的Dapr- actors 构建块

原文地址:https://docs.microsoft.com/en-us/dotnet/architecture/dapr-for-net-developers/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 挎斗提供了用于调用Actor 的 HTTP/gRPC API。 这是 HTTP API 的基 URL:

http://localhost:<daprPort>/v1.0/actors/<actorType>/<actorId>/

  • <daprPort>: the HTTP port that Dapr listens on.
  • <daprPort>: Dapr 侦听的 HTTP 端口。
  • <actorType>: the actor type.
  • <actorType>:执行组件类型。
  • <actorId>: the ID of the specific actor to call.
  • <actorId>:要调用的特定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 重新激活时可用。 Dapr 使用空闲计时器来确定何时可以停用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 调用:

执行组件服务和 Dapr 挎斗之间的 API 调用关系图。

Figure 11-1. API calls between actor service and Dapr sidecar.

图 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 显示了将服务扩展到第二个副本时发生的情况:

执行组件放置服务的关系图。

igure 11-2. Actor placement service.

  1. On startup, the sidecar makes a call to the actor service to get the registered actor types as well as actor configuration settings.
  2. The sidecar sends the list of registered actor types to the placement service.
  3. 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.
  1. 启动时,挎斗调用actor服务以获取注册的Actor类型和Actor的配置设置。
  2. 挎斗将注册的Actor类型的列表发送到placement 服务。
  3. placement 服务会将更新的分区信息广播到所有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 method of an instance with ID . Because the actor with ID is placed in a different instance, this results in a call to a different node in the cluster:shipOrderActor33

下图显示了在 Pod 1 中运行的ordering 服务实例调用 ship OrderActor ID 为的实例的方法 3 。 由于 ID 的actor 3 放在不同的实例中,因此将导致调用群集中的不同节点:

调用执行组件方法的关系图。

Figure 11-3. Calling an actor method.

图 11-3。 调用执Actor方法。

  1. The service calls the actor API on the sidecar. The JSON payload in the request body contains the data to send to the actor.
  2. 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 . In this example, it's the service instance in pod 2. The call is forwarded to the appropriate sidecar.3
  3. 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.
  1. 服务在挎斗上调用Actor API。 请求正文中的 JSON 有效负载包含要发送到Actor 的数据。
  2. 挎斗使用placement 服务中的本地缓存的分区信息来确定哪个执行组件服务实例 (分区) 负责托管 ID 为的Actor 。 在此示例中,它是 pod 2 中的服务实例。 调用将转发到相应的挎斗 3。
  3. Pod 2 中的挎斗实例调用服务实例以调用Actor。 如果Actor尚未 并执行Actor方法,则该服务实例将激活该执行组件。
Turn-based access model

Turn-based 的访问模型

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:

turn-based 的访问模型可确保在一个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 method is . When two threads call the method at the same time, there's a risk of both of them calling the method before one of them calls . This results in both threads starting with the same initial value (). The threads then increment the value to and return it to the caller. The resulting value after the two calls is now instead of 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.GetValue1IncrementGetValueSaveValue1223

假设方法返回的当前值 GetValue1 。 当两个线程同时调用方法时,它们会在调用方法 Increment GetValue 之前调用方法 SaveValue 。 这会导致两个线程以相同初始值开始 (1) 。 然后,线程递增值并将 2 其返回给调用方。 现在,两次调用后的结果值是, 2 而不是它的值 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:

由于使用的是Turn-based 的访问模型,因此,您无需担心使用actors 的多线程,使得编写并发系统变得更加容易。 下面的执行组件示例将对上一个示例中的代码进行密切镜像,但不需要任何锁定机制是正确的:

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,则会重新激活该执行组件。 提醒 重置空闲计时器。

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秒。

Bash

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分钟,空时间为空:

Bash

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.

计时器和提醒均遵循turn-based 的访问模型。 当计时器或提醒触发时,直到任何其他方法调用或计时器/提醒回调完成后才会执行回调。

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 状态。 由于执行组件可以一轮执行多个状态操作,因此状态存储组件必须支持多项事务。 撰写本文时,以下状态存储支持多项事务:

  • 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 一起使用的状态存储组件,需要将以下元数据附加到状态存储配置:

YAML

- name: actorStateStore
  value: "true"

Here's a complete example for a Redis state store:

下面是 Redis 状态存储的完整示例:

YAML

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 都为使用Actor 提供了广泛的支持。

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 . Clients use the interface to invoke operations on the actor. Here's a simple example of an actor interface for keeping scores:IActor

若要开始使用 .NET Dapr actors SDK,你可以将包引用Dapr.Actors 添加到 你的服务项目。 创建实际Actor 的第一步是定义并继承的接口 IActor 。 客户端使用接口调用Actor 上的操作。 下面是一个简单的Actor 接口示例,用于保持分数:

C#

public interface IScoreActor : IActor
{
    Task<int> IncrementScoreAsync();

    Task<int> GetScoreAsync();
}

Important

重要

The return type of an actor method must be or . Also, actor methods can have at most one argument. Both the return type and the arguments must be serializable.TaskTask<T>System.Text.Json

Actor 方法的返回类型必须为 TaskTask<T> 。 此外,Actor 方法最多只能有一个参数。 返回类型和参数都必须支持 System.Text.Json 序列化。

Next, implement the actor by deriving a class from . The class must also implement the interface:ScoreActorActorScoreActorIScoreActor

接下来,通过从派生类来实现参与者 ScoreActor ActorScoreActor类还必须实现 IScoreActor 接口:

C#

public class ScoreActor : Actor, IScoreActor
{
    public ScoreActor(ActorHost host) : base(host)
    {
    }

    // TODO Implement interface methods.
}

The constructor in the snippet above takes a argument of type . The class represents the host for an actor type within the actor runtime. You need to pass this argument to the constructor of the 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.hostActorHostActorHostActor

上面代码段中的构造函数采用 host 类型的参数 ActorHostActorHost类表示Actor 运行时中的actor 类型的宿主。 需要将此参数传递给基类的构造函数 Actor 。 actor 还支持依赖项注入。 使用 .NET 依赖关系注入容器来解析添加到actor 构造函数的任何其他参数。

Let's now implement the method of the interface:IncrementScoreAsync

现在,让我们实现 IncrementScoreAsync 接口的方法:

C#

public Task<int> IncrementScoreAsync()
{
    return StateManager.AddOrUpdateStateAsync(
        "score",
        1,
        (key, currentScore) => currentScore + 1
    );
}

In the snippet above, a single call to provides the full implementation for the method. The method takes three arguments:StateManager.AddOrUpdateStateAsyncIncrementScoreAsyncAddOrUpdateStateAsync

在上面的代码片段中,对方法的一次调用 StateManager.AddOrUpdateStateAsync 提供了完整的 IncrementScoreAsync 方法实现。 AddOrUpdateStateAsync方法采用三个参数:

  1. The key of the state to update.
  2. The value to write if no score is stored in the state store yet.
  3. A 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.Func
  1. 要更新的状态的键。
  2. 如果尚未将评分存储在状态存储中,则为要写入的值。
  3. Func 状态存储中已有分数存储时要调用的。 它将使用状态键和当前评分,并返回更新后的分数以写回到状态存储区。

The implementation reads the current score from the state store and returns it to the client:GetScoreAsync

GetScoreAsync实现读取状态存储中的当前评分,并将其返回给客户端:

C#

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 class. In the following example, the method adds the actor endpoints by calling :StartupConfigureendpoints.MapActorsHandlers

若要在 ASP.NET Core 服务中承载Actor,你必须添加对包的引用 Dapr.Actors.AspNetCore 并对Startup 类进行一些更改 。 在下面的示例中, Configure 方法通过调用来添加执行组件终结点 endpoints.MapActorsHandlers

C#

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 挎斗调用应用程序来承载和与执行组件实例进行交互。

Important

重要

Make sure your class does not contain an 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.Startupapp.UseHttpsRedirection

请确保你的 Startup 类不包含 app.UseHttpsRedirection 将客户端重定向到 HTTPS 终结点的调用。 这不适用于Actor。 按照设计,默认情况下,Dapr 挎斗通过未加密的 HTTP 发送请求。 启用后,HTTPS 中间件会阻止这些请求。

The class is also the place to register the specific actor types. In the example below, registers the using :StartupConfigureServicesScoreActorservices.AddActors

Startup类也是用于注册特定执行组件类型的位置。 在下面的示例中, ConfigureServices ScoreActor 使用注册 services.AddActors

C#

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 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 operation on a instance:ScoreActorIncrementScoreAsyncScoreActor

此时,ASP.NET Core 服务已准备好承载 ScoreActor 和接受传入的请求。 客户端应用程序使用Actor代理来调用Actor上的操作。 下面的示例演示了控制台客户端应用程序如何 IncrementScoreAsync 对实例调用操作 ScoreActor

C#

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 上调用操作,需要能够解决该操作。 此操作需要两个部分:

  1. 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 to the implementation class and setting its property.ActorAttributeTypeName
  2. The uniquely identifies an instance of an actor type. You can also use this class to generate a random actor id by calling .ActorIdActorId.CreateRandom
  1. actor type 在整个应用程序中唯一标识执行组件实现。 默认情况下,actor 类型是 (没有命名空间) 的实现类的名称。 可以通过将添加 ActorAttribute 到实现类并设置其属性,自定义参与者类型 TypeName
  2. ActorId唯一标识actor 类型的实例。 还可以通过调用来使用此类生成随机执行组件 id ActorId.CreateRandom

The example uses to create a proxy instance for the . The method takes two arguments: the 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.CreateScoreActorCreateActorId

该示例使用 ActorProxy.CreateScoreActor 创建代理实例 。 Create方法采用两个参数:标识特定Actor和actor ActorId 类型。 它还具有一个泛型类型参数,用于指定actor类型所实现的actor接口。 由于服务器和客户端应用程序都需要使用actor 接口,它们通常存储在单独的共享项目中。

The final step in the example calls the 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.IncrementScoreAsync

该示例中的最后一个步骤调用 Actor上的方法IncrementScoreAsync 并输出结果。 请记住,Dapr placement 服务跨 Dapr 分支分发actor 实例。 因此,需要将actor 调用作为对另一个节点的网络调用。

Call actors from ASP.NET Core clients
从 ASP.NET Core 客户端调用参与者

The console client example in the previous section uses the static method directly to get an actor proxy instance. If the client application is an ASP.NET Core application, you should use the interface to create actor proxies. The main benefit is that it allows you to manage configuration centrally in the method. The 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 to use for actor state persistence and message deserialization:ActorProxy.CreateIActorProxyFactoryConfigureServicesAddActorsJsonSerializerOptions

上一部分中的控制台客户端示例直接使用静态 ActorProxy.Create 方法获取Actor 代理实例。 如果客户端应用程序是 ASP.NET Core 应用程序,则应使用 IActorProxyFactory 接口创建Actor 代理。 主要优点是它允许您集中管理方法中的配置 ConfigureServices . AddActors方法采用一个委托,该委托允许指定actor 运行时选项,如 Dapr 挎斗的 HTTP 终结点。 下面的示例指定了用于actor  JsonSerializerOptions 状态持久性和消息反序列化的自定义:

C#

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 registers the for .NET dependency injection. This allows ASP.NET Core to inject an instance into your controller classes. The following example calls an actor method from an ASP.NET Core controller class:AddActorsIActorProxyFactoryIActorProxyFactory

AddActors 注册 IActorProxyFactory .net 依赖项注入而调用的。 这允许 ASP.NET Core 将实例注入 IActorProxyFactory 控制器类。 下面的示例从 ASP.NET Core 控制器类调用执行组件方法:

C#

[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 base class exposes an class through the property. To create an actor proxy from within an actor, use the property of the base class. The following example shows an that invokes operations on two other actors:ActorIActorProxyFactoryProxyFactoryProxyFactoryActorOrderActor

Actors还可以直接调用其他Actors。 Actor基类 IActorProxyFactory 通过属性公开类 ProxyFactory 。 若要从Actor 中创建执行组件代理,请使用 ProxyFactory 基类的属性 Actor 。 下面的示例演示一个 OrderActor ,它对两个其他Actor 调用操作:

C#Copy

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 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.Actor A -> Actor B -> Actor A

默认情况下,Dapr actors 不可重入。 这意味着不能在同一链中多次调用 Dapr actor。 例如, 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 接口。

C#

[HttpPut("{scoreId}")]
public Task<int> IncrementAsync(string scoreId)
{
    var scoreActor = _actorProxyFactory.CreateActorProxy(
        new ActorId(scoreId),
        "ScoreActor");

    return scoreActor("IncrementScoreAsync");
}
Timers and reminders
计时器和提醒

Use the method of the base class to schedule actor timers. In the following example, a exposes a method. Clients can call the method to start a timer that repeatedly writes a given text to the log output.RegisterTimerAsyncActorTimerActorStartTimerAsync

使用 RegisterTimerAsync 基类的 Actor 方法计划actor 计时器。 在下面的示例中, TimerActor 公开 StartTimerAsync 方法。 客户端可以调用 方法来启动一个计时器,该计时器将给定的文本重复写入日志输出。

C#

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 method calls to schedule the timer. takes five arguments:StartTimerAsyncRegisterTimerAsyncRegisterTimerAsync

StartTimerAsync方法调用 RegisterTimerAsync 来调度计时器。 RegisterTimerAsync 采用五个参数:

  1. The name of the timer.
  2. The name of the method to call when the timer fires.
  3. The state to pass to the callback method.
  4. The amount of time to wait before the callback method is first invoked.
  5. The time interval between callback method invocations. You can specify to disable periodic signaling.TimeSpan.FromMilliseconds(-1)
  1. 计时器的名称。
  2. 触发计时器时要调用的方法的名称。
  3. 要传递给回调方法的状态。
  4. 首次调用回调方法之前要等待的时间。
  5. 回调方法调用之间的时间间隔。 可以指定 以 TimeSpan.FromMilliseconds(-1) 禁用定期信号。

The method receives the user state in binary form. In the example, the callback decodes the state back to a before writing it to the log.TimerCallbackAsyncstring

TimerCallbackAsync方法以二进制形式接收用户状态。 在示例中,回调在将状态写入日志之前将状态 string 解码 。

Timers can be stopped by calling :UnregisterTimerAsync

可以通过调用 UnregisterTimerAsync 来停止计时器 :

C#

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 interface:IRemindable

若要在Actor 中使用提醒,Actor 类必须实现 IRemindable 接口:

C#

public interface IRemindable
{
    Task ReceiveReminderAsync(
        string reminderName, byte[] state,
        TimeSpan dueTime, TimeSpan period);
}

The method is called when a reminder is fired. It takes 4 arguments:ReceiveReminderAsync

触发提醒时调用ReceiveReminderAsync 方法。 它采用 4 个参数:

  1. The name of the reminder.
  2. The user state provided during registration.
  3. The invocation due time provided during registration.
  4. The invocation period provided during registration.
  1. 提醒的名称。
  2. 注册期间提供的用户状态。
  3. 注册期间提供的调用到期时间。
  4. 注册期间提供的调用周期。

To register a reminder, use the method of the actor base class. The following example sets a reminder to fire a single time with a due time of three minutes.RegisterReminderAsync

若要注册提醒,请使用 Actor基类的 方法RegisterReminderAsync 。 以下示例设置一个提醒,以在到期时间为 3 分钟时发送一次。

C#

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 method is similar to but you don't have to specify a callback method explicitly. As the above example shows, you implement to handle fired reminders.RegisterReminderAsyncRegisterTimerAsyncIRemindable.ReceiveReminderAsync

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 file and uncomment the statement at the top of the file:src/TrafficControlService/Controllers/TrafficController.csUSE_ACTORMODEL

Dapr 交通控制的默认版本不使用Actor 模型。 但是,它确实包含可以启用的 TrafficControl 服务的基于Actor 的替代实现。 若要使用 TrafficControl 服务中的Actor ,请打开 文件并取消注释文件 src/TrafficControlService/Controllers/TrafficController.cs 顶部的 USE_ACTORMODEL 语句:

C#

#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 interface:IVehicleActor

启用Actor模型后,应用程序将使用Actor 来表示车辆。 可在车辆Actor 上调用的操作在接口中 IVehicleActor 定义:

C#

public interface IVehicleActor : IActor
{
    Task RegisterEntryAsync(VehicleRegistered msg);
    Task RegisterExitAsync(VehicleRegistered msg);
}

The (simulated) entry cameras call the 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

首次在 (检测到) 车辆时,模拟入口相机会 调用 RegisterEntryAsync 方法。 此方法的唯一职责是在actor 状态中存储条目时间戳:

C#

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 method. The method first gets the current states and updates it to include the exit timestamp:RegisterExitAsyncRegisterExitAsync

当车辆到达速度相机区域末尾时,退出摄像头调用 RegisterExitAsync 方法。 RegisterExitAsync方法首先获取当前状态并更新它以包括退出时间戳:

C#

var vehicleState = await StateManager.GetStateAsync<VehicleState>("VehicleState");
vehicleState.ExitTimestamp = msg.Timestamp;

Note

备注

The code above currently assumes that a instance has already been saved by the 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.VehicleStateRegisterEntryAsync

上面的代码当前假定 实例 VehicleState 已由 方法 RegisterEntryAsync 保存。 可以通过首先检查 以确保状态存在来改进代码。 得益于turn-based 的访问模型,代码中不需要显式锁。

After the state is updated, the method checks if the vehicle was driving too fast. If it was, the actor publishes a message to the pub/sub topic:RegisterExitAsynccollectfine

状态更新后, RegisterExitAsync 方法将检查车辆是否驾驶速度过快。 如果是,则Actor 将消息发布到 pub/sub 主题collectfine

C#

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 encapsulates the business logic for determining whether or not a vehicle has driven too fast. The allows the actor to publish messages using the Dapr pub/sub building block._speedingViolationCalculator_daprClient

上面的代码使用两个外部依赖项。 封装用于确定车辆是否驾驶速度过快 _speedingViolationCalculator 的业务逻辑。 允许 actor 使用 Dapr pub/sub 构建基块发布消息。

Both dependencies are registered in the class and injected into the actor using constructor dependency injection:Startup

这两个依赖项在 类 Startup 中注册,并且使用构造函数依赖项注入注入到Actor中:

C#

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 是隐式创建的,在未执行任何操作时以无提示方式从内存中卸载。 重新激活actors 时,自动持久保存并加载actors 中存储的任何状态。 actors 模型实现通常是为特定语言或平台创建的。 但是,借助 Dapr 执行组件构建基块,可以从任何语言或平台利用执行actors 模型。

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.

Actor 支持计时器和提醒来调度将来的工作。 计时器不会重置空闲计时器,并且允许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 状态管理构建基块 持久保存执行组件状态。 支持多项事务的任何状态存储都可用于存储执行组件状态。

References
posted @ 2021-07-04 21:49  张善友  阅读(1912)  评论(2编辑  收藏  举报