Actor 模型如何满足现代分布式系统的需求?

如前一主题「为什么现代系统需要新的编程模型」所述,常见的编程实践不能合适地满足现代系统的需求。幸运的是,我们不需要放弃我们所知道的一切。相反,Actor 模型以一种原则性的方式解决了这些缺点,允许系统以更好地匹配我们的构思模型(mental model)的方式运行。Actor 模型抽象允许你从通信的角度来考虑你的代码,这与大型组织中人员之间发生的交换没有什么不同。

使用 Actor 允许我们:

  • 在不使用锁的情况下强制封装。
  • 利用协同实体对信号作出反应、改变状态、相互发送信号的模型来驱动整个应用程序向前发展。
  • 不要担心执行机制与我们的世界观(world view)不匹配。

消息传递的使用避免了锁和阻塞

Actor 不调用方法,而是互相发送消息。发送消息不会将线程的执行权从发送方传输到目标方。Actor 可以发送一条消息并继续其他操作,而不是阻塞。因此,它可以在相同的时间内完成更多的工作。

对于对象,当一个方法返回时,它释放对其执行线程的控制。在这方面,Actor 的行为非常类似于对象,它们对消息作出反应,并在完成当前消息的处理后执行返回。通过这种方式,Actor 实际上实现了我们设想中对象的执行方式:

actor-interacting

传递消息和调用方法之间的一个重要区别是消息没有返回值。通过发送消息,Actor 将工作委托给另一个 Actor。正如我们在「调用栈的假象」中看到的,如果它期望返回值,那么发送 Actor 要么阻塞,要么在同一线程上执行另一个 Actor 的工作。相反,接收 Actor 在回复消息中传递结果。

我们模型中需要的第二个关键改变是恢复封装。Actor 对消息的反应就像对象对调用它们的方法“反应”一样。不同之处在于,不同于多个线程“突出(protruding)”到 Actor 中并对内部状态和不变量造成严重破坏,Actor 的执行动作独立于消息的发送者,并对传入消息依次作出反应,一次一个。当每个 Actor 按顺序处理发送给它的消息时,不同的 Actor 同时工作,以便 Actor 系统可以同时处理硬件所支持的尽可能多的消息。

由于每个 Actor 最多只能同时处理一条消息,因此可以不同步地保留 Actor 的不变量。这是自动发生的,不使用锁:

actor123

总之,当 Actor 收到消息时会发生以下情况:

  1. Actor 将消息添加到队列的末尾。
  2. 如果 Actor 没有执行计划,则将其标记为准备执行。
  3. 一个(隐藏的)调度程序实体获取 Actor 并开始执行它。
  4. Actor 从队列前面选择消息。
  5. Actor 修改内部状态,向其他 Actor 发送消息。
  6. Actor 没有预约(unscheduled)。

为了完成上面的行为,Actors 有:

  • 一个邮箱(消息结束的队列)。
  • 一个行为(Actor 的状态、内部变量等)。
  • 消息(表示信号的数据片段,类似于方法调用及其参数)。
  • 一个执行环境(一种机制,它让具有消息的 Actor 对其消息处理代码作出反应并调用它们)。
  • 一个地址(稍后将详细介绍)。

消息进入 Actor 邮箱。Actor 的行为描述了 Actor 如何响应消息(如发送更多消息和/或更改状态)。执行环境协调线程池以完全透明地驱动所有这些操作。

这是一个非常简单的模型,它解决了前面列举的问题:

  • 通过将执行与信号分离(方法调用转换执行权,消息传递不这样做),可以保留封装。
  • 不需要锁。修改 Actor 的内部状态只能通过消息来实现,一次处理一条消息,在试图保持不变时消除竞争。
  • 任何地方都没有使用锁,发送者也不会阻塞。数百万个 Actor 可以有效地安排在十几个线程上,从而充分发挥现代 CPU 的潜力。任务委托是 Actor 的天然执行方式。
  • Actor 的状态是本地的而不是共享的,更改和数据通过消息传播,消息是映射到现代内存架构的实际工作方式。在许多情况下,这意味着只传输包含消息中数据的缓存线,同时将本地状态和数据缓存在原始核心上。相同的模型可以完全映射到远程通信中,其中状态保存在机器的 RAM 中,更改/数据作为数据包在网络上传播。

Actor 能够优雅地处理错误情况

由于我们不再拥有在相互发送消息的 Actor 之间共享的调用栈,因此我们需要以不同的方式处理错误情况。我们需要考虑两种错误:

  • 第一种情况是,由于任务中的错误(通常是一些验证问题,如不存在的用户 ID),目标 Actor 上的委派任务失败。在这种情况下,由目标 Actor 封装的服务是完整的,只有任务本身是错误的。服务 Actor 应该用一条消息回复发送者,并显示错误情况。这里没有什么特别的,错误是域的一部分,因此错误也是普通消息。
  • 第二种情况是当服务本身遇到内部故障时。Akka 要求所有 Actor 都被组织成一个树形的结构,即一个创造另一个 Actor 的 Actor 成为新 Actor 的父节点。这与操作系统将流程组织到树中的方式非常相似。就像处理过程一样,当一个 Actor 失败时,它的父 Actor 会得到通知,并且它可以对失败做出反应。另外,如果父 Actor 被停止,那么它的所有子 Actor 也将被递归地停止。这项服务称为监督,是 Akka 的核心概念。

parents-supervise-children

一个监督者(父级节点)可以决定在某些类型的失败时重新启动其子 Actor,或者在其他失败时完全停止它们。子 Actor 永远不会默不作声地死去(除了进入一个无限循环之外),相反,他们要么失败,他们的父级可以对错误作出反应,要么他们被停止(在这种情况下,相关方会被自动通知)。总是有一个负责管理 Actor 的实体:它的父节点。从外部看不到重新启动:协作 Actor 可以在目标 Actor 重新启动时继续发送消息。

现在,让我们简单介绍一下 Akka 提供的功能。


英文原文链接How the Actor Model Meets the Needs of Modern, Distributed Systems.