Actor

Actors

actor 模式将 actor 描述为最低层次的 “计算单元”。换句话说,你把代码写在一个自足的单元(称为 actor)中,这个单元每次接收消息并处理它们,没有任何并发或线程。当你的代码处理一条消息时,它可以向其他角色发送一条或多条消息,或者创建新的角色。底层运行时管理每个角色的运行方式、时间和地点,并在角色之间路由消息。

大量的 actor 可以同时执行,而且 actor 之间可以独立执行。Dapr 包括一个专门实现 Virtual Actor 模式的运行时。通过 Dapr 的实现,你可以根据 Actor 模式编写 Dapr 的 actor,Dapr 利用底层平台提供的可扩展性和可靠性保证。与其他任何技术决策一样,你应该根据你要解决的问题来决定是否使用 actor。

actor 设计模式可以很好地适应一些分布式系统问题和场景,但你首先应该考虑的是模式的约束条件。一般来说,在以下情况下,可以考虑用 actor 模式来模拟你的问题或场景。

  • 你的问题空间涉及大量(数千或更多)小的,独立的,孤立的状态和逻辑单元。
  • 你想使用单线程对象,这些对象不需要从外部组件中进行大量的交互,包括在一组 actor 中查询状态。
  • 你的 actor 实例不会因为发出 I/O 操作而以不可预知的延迟来阻塞调用者。

Dapr 中 Actor 定义

每一个 actor 都被定义为一个 actor 类型的实例,就像一个对象是一个类的实例一样。例如,可能有一个 actor 类型实现了计算器的功能,并且可能有许多该类型的 actor 分布在集群的不同节点上。每一个这样的 actor 都由一个 actor ID 来唯一标识。

Actor 示意

Dapr 行为体是虚拟的,这意味着它们的寿命与它们在内存中的表现无关。因此,它们不需要被显式创建或销毁。Dapr actors 运行时在第一次收到对该 actor ID 的请求时,会自动激活一个 actor。如果一个 actor 在一段时间内没有被使用,Dapr Actors 运行时就会对内存中的对象进行垃圾回收。如果以后需要重新激活它,它也会保持该 actor 存在的知识。

对 actor 方法的调用和提醒会重置空闲时间,例如,提醒的触发会使 actor 保持活跃。无论 actor 是活跃还是不活跃,actor 提醒都会被触发,如果为不活跃的 actor 触发,它将首先激活 actor。actor 定时器不重置空闲时间,所以定时器发射不会使 actor 保持活跃状态。定时器只有在 actor 处于活动状态时才会发射。Dapr 运行时用来查看 actor 是否可以被垃圾回收的空闲时间和扫描间隔是可以配置的。当 Dapr 运行时调用 actor 服务以获取支持的 actor 类型时,可以传递这些信息。

由于虚拟 actor 模型的存在,这种虚拟 actor 寿命抽象带有一些注意事项,事实上 Dapr Actors 的实现有时会偏离这个模型。一个 actor 在第一次向它的 actor ID 发送消息时就会自动激活(导致一个 actor 对象被构造)。经过一段时间后,该 actor 对象会被垃圾回收。在未来,再次使用 actor ID,会导致一个新的 actor 对象被构造。一个 actor 的状态会超过对象的寿命,因为状态存储在为 Dapr 运行时配置的状态提供者中。

分布式、容错与 Placement 服务

为了提供可扩展性和可靠性,actors 实例分布在整个集群中,Dapr 会根据需要自动将它们从故障节点迁移到健康节点。actor 分布在 actor 服务的实例中,这些实例分布在集群中的节点上。每个服务实例都包含一组给定 actor 类型的 actor。

Dapr 角色运行时为您管理分配方案和密钥范围设置。这是由 actor Placement 服务完成的。当创建一个新的服务实例时,相应的 Dapr 运行时会注册它可以创建的 actor 类型,而 Placement 服务会计算给定 actor 类型的所有实例的分区。这个每个 actor 类型的分区信息表会在环境中运行的每个 Dapr 实例中更新和存储,并且可以随着新的 actor 服务实例的创建和销毁而动态变化。如下图所示:

Placement 服务示意图

当客户机调用具有特定 id 的 actor(例如,actor id 123)时,客户机的 Dapr 实例会对 actor 类型和 id 进行哈希,并使用该信息调用到可以为该特定 actor id 的请求提供服务的相应 Dapr 实例。因此,对于任何给定的 actor id,总是调用同一个分区(或服务实例)。如下图所示:

Actor 调用示意图

这简化了一些选择,但也有一些考虑:

  • 默认情况下,行为体被随机放置在荚中,导致统一分布。
  • 因为 actor 是随机放置的,所以应该预期 actor 的操作总是需要网络通信,包括方法调用数据的序列化和反序列化,从而产生延迟和开销。

Actor 通信

你可以通过调用 HTTP/gRPC 端点与 Dapr 进行交互,调用 actor 方法。

POST/GET/PUT/DELETE http://localhost:3500/v1.0/actors/<actorType>/<actorId>/<method/state/timers/reminders>

你可以在请求体中为 actor 方法提供任何数据,而请求的响应将在响应体中,也就是 actor 调用的数据。

并发机制

Dapr Actors 运行时为访问 actor 方法提供了一个简单的基于回合的访问模型。这意味着在任何时候,一个 actor 对象的代码内都不能有超过一个线程在活动。基于轮流访问大大简化了并发系统,因为不需要数据访问的同步机制。这也意味着系统在设计时必须特别考虑每个 actor 实例的单线程访问性质。

单个 actor 实例不能同时处理一个以上的请求。如果期望一个行为体实例处理并发请求,就会造成吞吐量瓶颈。如果两个 actor 之间存在循环请求,同时向其中一个 actor 发出外部请求,那么 actor 就会相互死锁。Dapr actor 运行时自动超时调用 actor,并向调用者抛出异常,以中断可能的死锁情况。

Actor 单线程处理

一个 Turn 包括响应其他角色或客户端的请求而完整执行一个角色方法,或者完整执行一个定时器/提醒回调。尽管这些方法和回调是异步的,但 Dapr Actors 运行时不会将它们交错在一起。在允许新的回合之前,一个回合必须完全完成。换句话说,一个当前正在执行的 actor 方法或定时器/提醒回调必须在允许对方法或回调进行新的调用之前完全完成。如果一个方法或回调的执行已经从该方法或回调返回,并且该方法或回调返回的任务已经完成,则认为该方法或回调已经完成。值得强调的是,即使在不同的方法、定时器和回调之间,也会尊重基于回合的并发性。

Dapr actors 运行时通过在回合开始时获取每个 actor 锁,并在回合结束时释放锁来强制执行基于回合的并发。因此,基于回合的并发性是在每个 actor 的基础上执行的,而不是跨 actor。角色方法和定时器/提醒器回调可以代表不同的角色同时执行。下面的例子说明了上述概念。考虑一个实现了两个异步方法(比如 Method1 和 Method2)、一个定时器和一个提醒器的 actor 类型。下图显示了代表属于该 actor 类型的两个 actor(ActorId1 和 ActorId2)执行这些方法和回调的时间线的例子。

基于回合的示意图

Edge 案例

在边缘数据采集的场景下,我们也会经常用到 Actor:

Dapr Actors setup on IoT Edge

ActorClient 模拟多个传感器,并通过 Daprd sidecar(与容器内的 ActorClient 一起启动)调用 Actor 方法,将生成的传感器数据传递给 SensorActor 实例。有多种方法可以作为客户端调用 Actor 方法:使用 Actor Service Remoting、不使用 Actor Service Remoting 或仅使用 REST API。

SensorActor 是实际的 Actor 实现。Dapr 将为每个独特的 ActorId(在我们的例子中是传感器)自动创建一个该类型的实例,基本上提供了每个模拟传感器的虚拟表示。传感器数据同样由 Daprd sidecar 传递给 Actor 实现。接收到传感器数据后,将其存储在 Actor 状态(配置状态存储)中。SensorActor 实例注册一个定时器。当它开火时,对这个特定的 Actor(传感器)实例的所有可用传感器数据进行聚合,并将结果发送到 IoT Hub。这是对所有 Actor 实例并行完成的。