Actor

Actors

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

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

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

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

DaprActor定义

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

Actor 示意

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

actor方法的调用和提醒会重置空闲时间,例如,提醒的触发会使actor保持活跃。无论actor是活跃还是不活跃,actor提醒都会被触发,如果为不活跃的actor触发,它将首先激活actoractor定时器不重置空闲时间,所以定时器发射不会使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 服务示意图

当客户机调用具有特定idactor(例如,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。角色方法和定时器/提醒器回调可以代表不同的角色同时执行。下面的例子说明了上述概念。考虑一个实现了两个异步方法(比如Method1Method2、一个定时器和一个提醒器的actor类型。下图显示了代表属于该actor类型的两个actorActorId1ActorId2)执行这些方法和回调的时间线的例子。

基于回合的示意图

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实例并行完成的。