Actor模型是如何让编写并发系统变得更简单的?

在上周Dapr的直播中,小伙伴提了很多关于Actor模型的问题。Actor模型作为Dapr中重要的部分,大大简化了并发编程的复杂度,但其能解决什么问题,工作原理又是啥?

Actor模型

Actor模型起源于Carl Hewitt在1973年提出的作为并发计算的概念模型,这种形式的计算会同时执行多个计算。当时并没有高度并行的计算机,但多核Cpu和分布式系统的最新进步使得Actor模型变得流行。

在Actor模型中,Actor是一个计算和状态独立的单元。Actors完全彼此隔离,它们永远不会共享内存。Actors 使用消息相互通信。当一个Actor 收到消息时,它可以更改其内部状态,并将消息发送到其他 (可能是新的) Actors。

Actor模型使得编写并发系统变得更简单,它提供了基于 turn-based 的 (或单线程) 访问模型。多个Actors可以同时运行,但每个Actor 一次只处理一个接收的消息。这意味着,在任何时候,都可以确保在Actors 中最多有一个线程处于活动状态,这使得编写正确的并发系统和并行系统变得更加容易。

Actor模型能解决啥问题

Actor 模型的实现通常绑定到特定语言或平台。使用 Dapr Actor 构建块可以从任何语言或平台来使用 Actor 模型。

Dapr 的实现基于项目 "奥尔良" 中引入的虚拟Actor模式。对于虚拟Actor模式,不需要显式的创建Actor。第一次将消息发送到Actor时,Actor将被隐式激活并放置在群集中的节点上。当不执行操作时,Actor 会以静默方式从内存中卸载。如果某个节点出现故障,Dapr 会自动将激活的Actor 移到正常的节点。除了在Actor之间发送消息以外,Dapr Actor模型还支持使用计时器和提醒调度将来的工作。

虽然Actor模型提供了很大的优势,但必须仔细考虑Actor的设计。例如,如果多个客户端调用相同的Actor,则会导致性能不佳,因为Actor 操作会按顺序执行。下面的检查清单是是否适用于 Dapr Actor的一些标准:

  • 问题空间涉及并发性。如果没有Actor,则需要在代码中引入显式锁定机制。
  • 可以将问题空间分区为小、独立和隔离的状态和逻辑单元。
  • 不需要低延迟的读取Actor 状态。因为Actor 操作是按顺序执行,不能保证低延迟读取。
  • 不需要在一组Actor 之间查询状态。跨Actor 的查询效率低下,因为每个Actor 的状态都需要单独读取,并且可能会导致不可预测的延迟。

满足这些条件的一种设计模式就是基于业务流程的saga或流程管理器设计模式。Saga管理必须执行的一系列步骤才能达到某些结果。Saga (或进程管理器) 维护序列的当前状态,并触发下一步。如果一个步骤失败,saga可以执行补偿操作。利用Actor,可以轻松处理 saga 中的并发,并跟踪当前状态。EShopOnDapr 参考应用程序使用 saga 模式和 Dapr Actor来实现排序过程。

工作原理

Dapr 提供了用于调用Actor 的 HTTP/gRPC API。

 

这是HTTP API的基URL:
http://localhost:daprPort/v1.0/actors/actorType/actorId/

 

  • daprPort: Dapr 侦听的 HTTP 端口
  • actorType:执行组件类型
  • actorId:要调用的特定Actor的ID

 

Actor管理每个Actor的运行时间和位置,以及在Actor之间路由消息的方式。如果一段时间未使用某个Actor,则运行时将停用该执行组件,并将其从内存中删除。Actor所管理的任何状态都将被保留,并在Actor 重新激活时可用。Dapr 使用空闲计时器来确定何时可以停用Actor。当在Actor 上调用操作时 (通过方法调用或提醒触发) ,会重置空闲计时器,并保持激活执行组件实例。

 

Actor API 只是公式的一部分。服务本身还需要实现 API规范,因为你为Actor编写的实际代码将在服务本身内运行。下图显示了服务和它之间的各种 API 调用:

Actor模型是如何让编写并发系统变得更简单的?

 

actor服务和 Dapr Actor之间的 API 调用

为了提供可伸缩性和可靠性,将在Actor服务的所有实例中对actor进行分区。Dapr placement服务负责跟踪分区信息。启动Actor服务的新实例时,会将支持的Actor 类型注册到placement服务。placement 服务计算给定Actor类型的更新分区信息,并将其广播给所有实例。下图显示了将服务扩展到第二个副本时发生的情况:

Actor模型是如何让编写并发系统变得更简单的?

 

Actor 处理单元编排服务 placement service

  • 启动时,Actor调用actor服务以获取注册的Actor类型和Actor的配置设置。
  • 将注册的Actor类型的列表发送到placement 服务。
  • placement服务会将更新的分区信息广播到所有Actor服务实例。每个实例都将保留分区信息的缓存副本,并使用它来调用Actor。

由于actor是在各服务实例间随机分发的,因此Actor 始终需要调用网络中的其他节点。

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

Actor模型是如何让编写并发系统变得更简单的?

 

调用执Actor方法

  • 服务在Actor上调用Actor API。请求正文中的JSON有效负载包含要发送到Actor的数据。
  • 使用placement 服务中的本地缓存的分区信息来确定哪个执行组件服务实例 (分区) 负责托管 ID 为的Actor 。在此示例中,它是 pod 2中的服务实例。调用将转发到相应的实例 3。
  • Pod 2 中的实例调用服务实例以调用Actor。如果Actor尚未并执行Actor方法,则该服务实例将激活该执行组件。

计时器和提醒 Timers and reminders

Actors 可以使用计时器和提醒来调度自身的调用。这两个概念都支持配置截止时间。不同之处在于回调注册的生存期:

  • 只要激活Actor,计时器就会保持活动状态。计时器 不会 重置空闲计时器,因此它们不能使Actor 处于活动状态
  • 提醒长于Actor激活。如果停用了某个Actor,则会重新激活该执行组件。提醒 将 重置空闲计时器

计时器是通过调用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"
      }'


此提醒将在5分钟后激发。由于给定时间段为空,这将为一次性提醒。计时器和提醒均遵循turn-based 的访问模型。当计时器或提醒触发时,直到任何其他方法调用或计时器/提醒回调完成后才会执行回调。

 

State persistence

 

使用 Dapr 状态管理构建块保存Actor 状态。由于执行组件可以一轮执行多个状态操作,因此状态存储组件必须支持多项事务。撰写本文时,以下状态存储支持多项事务:

 

  • Azure Cosmos DB
  • MongoDB
  • MySQL
  • PostgreSQL
  • Redis
  • RethinkDB
  • SQL Server

 

若要配置要与Actors 一起使用的状态存储组件,需要将以下元数据附加到状态存储配置:

- name: actorStateStore
  value: "true"

 

下面是 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"

 

总结

Dapr actors 构建基块可以更轻松地编写正确的并发系统。actors 是状态和逻辑的小单元。它们使用基于轮次的访问模型,无需使用锁定机制编写线程安全代码。actors 是隐式创建的,在未执行任何操作时以无提示方式从内存中卸载。重新激活actors 时,自动持久保存并加载actors 中存储的任何状态。actors 模型实现通常是为特定语言或平台创建的。但是,借助 Dapr 执行组件构建基块,可以从任何语言或平台利用执行actors 模型。

Actor 支持计时器和提醒来调度将来的工作。计时器不会重置空闲计时器,并且允许Actor 在未执行其他操作时停用。提醒会重置空闲计时器,并且也会自动保留。计时器和提醒都遵守基于轮次的访问模型,确保在处理计时器/提醒事件时无法执行任何其他操作。

使用 Dapr 状态管理构建基块持久保存执行组件状态。支持多项事务的任何状态存储都可用于存储执行组件状态。

参考资料:

https://www.cnblogs.com/shanyou/

https://docs.dapr.io/developing-applications/building-blocks/actors/

https://docs.dapr.io/reference/components-reference/supported-state-stores/

上一篇:思考:以卖票为例子思考Actor模型(3种卖票方案对比)


下一篇:如何优化 MySQL 语句