烂翻译系列之面向.NET开发人员的Dapr——Actors构建块

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放置服务。

  1. 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的配置设置。
  2. The sidecar sends the list of registered actor types to the placement service.      边车将注册的Actor类型的列表发送到placement 服务。
  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.      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方法。

  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.      服务在边车上调用Actor API。 请求正文中的 JSON 有效负载包含要发送到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 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 中的服务实例。 调用将转发到相应的边车。
  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.      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 方法采用三个参数:

  1. The key of the state to update.      要更新的状态的键。
  2. The value to write if no score is stored in the state store yet.      如果尚未将评分存储在状态存储中,则为要写入的值。
  3. 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上调用操作,需要能够定位到它。 此操作需要两部分:

  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 ActorAttribute to the implementation class and setting its TypeName property.      actor的类型在整个应用程序中唯一标识actor的实现。 默认情况下,actor的类型是实现类的名称(不包含命名空间)。 可以通过为实现类添加ActorAttribute并设置其 TypeName 属性,来自定义actor的类型。
  2. The ActorId uniquely identifies an instance of an actor type. You can also use this class to generate a random actor id by calling ActorId.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 采用了五个参数:

  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 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 个参数:

  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.      注册期间提供的调用周期(两次调用的时间间隔)。

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,以及它们的方法和回调函数的运行的时间线。

作者:菜鸟吊思
本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利.
施主您好,贫僧接受供养,施主一粒米,大如须弥山!南无阿弥陀佛!
原文地址:https://www.cnblogs.com/sad-bird/p/15028926.html