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 来唯一标识。
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 服务实例的创建和销毁而动态变化。如下图所示:
当客户机调用具有特定id 的actor (例如,actor id 123)时,客户机的Dapr 实例会对actor 类型和id 进行哈希,并使用该信息调用到可以为该特定actor id 的请求提供服务的相应Dapr 实例。因此,对于任何给定的actor id ,总是调用同一个分区(或服务实例) 。如下图所示:
这简化了一些选择,但也有一些考虑:
默认情况下,行为体被随机放置在荚中,导致统一分布。
因为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 ,并向调用者抛出异常,以中断可能的死锁情况。
一个Turn 包括响应其他角色或客户端的请求而完整执行一个角色方法,或者完整执行一个定时器/ 提醒回调。尽管这些方法和回调是异步的,但Dapr Actors 运行时不会将它们交错在一起。在允许新的回合之前,一个回合必须完全完成。换句话说,一个当前正在执行的actor 方法或定时器/ 提醒回调必须在允许对方法或回调进行新的调用之前完全完成。如果一个方法或回调的执行已经从该方法或回调返回,并且该方法或回调返回的任务已经完成,则认为该方法或回调已经完成。值得强调的是,即使在不同的方法、定时器和回调之间,也会尊重基于回合的并发性。
Dapr actors 运行时通过在回合开始时获取每个actor 锁,并在回合结束时释放锁来强制执行基于回合的并发。因此,基于回合的并发性是在每个actor 的基础上执行的,而不是跨actor 。角色方法和定时器/ 提醒器回调可以代表不同的角色同时执行。下面的例子说明了上述概念。考虑一个实现了两个异步方法(比如Method1 和Method2 ) 、一个定时器和一个提醒器的actor 类型。下图显示了代表属于该actor 类型的两个actor (ActorId1 和ActorId2 )执行这些方法和回调的时间线的例子。
Edge 案例
在边缘数据采集的场景下,我们也会经常用到Actor :
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 实例并行完成的。