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服务和 Dapr Actor之间的 API 调用
为了提供可伸缩性和可靠性,将在Actor服务的所有实例中对actor进行分区。Dapr placement服务负责跟踪分区信息。启动Actor服务的新实例时,会将支持的Actor 类型注册到placement服务。placement 服务计算给定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 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/