Actor 分布式并行计算模型: The Actor Model for Concurrent Computation

分布式计算技术之Actor计算模式

The Actor Model for Concurrent Computation

背景介绍

计算机CPU的计算速度提高(频率的提高)是有限度的,我们能做的是放入多个计算核心。为了利用多核心的计算机,我们需要并发执行。但是多线程的方式会引入很多问题和增加调试难度。

我们有个替换的方案,叫做Actor模型。

两种分布式计算模式: Actor 和流水线

分布式计算的本质就是在分布式环境下,多个进程协同完成一件复杂的事情,但每个进程各司其职,完成自己的工作后,再交给其他进程去完成其他工作。当然,对于没有依赖的工作,进程间是可以并行执行的。

分布式进程那么多,如果需要开发者自己去维护每个进程之间的数据、状态等信息,这个开发量可不是一般得大,而且特别容易出错。那么,有没有什么办法可以让开发者只关注自己的逻辑呢?

答案是肯定的,Actor 计算模式就能满足你的需求。也就是说,你可以把数据、状态等都扔给 Actor。

什么是 Actor?

Actor 类似于一个“黑盒”对象,封装了自己的状态和行为,使得其他 Actor 无法直接观察到它的状态,调用它的行为。多个 Actor 之间通过消息进行通信,这种消息类似于电子邮箱中的邮件。Actor 接收到消息之后,才会根据消息去执行计算操作。

那么,Actor 模型又是什么呢?

Actor 模型,代表一种异步消息模式的分布式并行计算模型。在 Actor 模型里,每个 Actor 相当于系统中的一个组件,都是基本的计算单元。

这种模型有自己的一套规则,规定了 Actor 的内部计算逻辑,以及多个 Actor 之间的通信规则。

Actor 模型的计算方式与传统面向对象编程模型(Object-Oriented Programming,OOP)类似,一个对象接收到一个方法的调用请求(类似于一个消息),从而去执行该方法。

OOP 因为数据封装在一个对象中,不能被外部访问,当多个外部对象通过方法调用方式,即同步方式进行访问时,会存在死锁、竞争等问题,无法满足分布式系统的高并发性需求。

而 Actor 模型通过消息通信,采用的是异步方式,克服了 OOP 的局限性,适用于高并发的分布式系统。

Actor模型

Actor模型是处理并行计算的概念模型。它定义了系统部件行为和交互的一些规则。使用这个模型的最著名的编程语言是Erlang。

模型中一个Actor是一个基本的计算单元。它接受消息然后基于接到的消息做一些计算。和面向对象编程有些类似,一个对象被调用(接收到一个消息),基于调用方法(接受到的一个消息)做处理。区别是actor之间是完全隔离的,不共用内存区域。actor的私有状态不会被另外一个actor直接改变。

actor作为群体存在,单一的actor不是actor模式。在actor模型中,actor是唯一组成部分,actor带有地址以便互相发送消息。

actor按次序处理消息,比如你发送三个消息给一个actor,它们不会被并发处理。如果你想让这三个消息得到并发处理,你需要创建3个actor,然后分别发送给它们。

接受到的异步消息存在于actor内部的一个队列中,我们可以把它形象化的叫做邮箱(mailbox)。

Actor模型定义

一种分布式并行计算模型。 该模型有自己的一套规则,规定了Actor的内部计算逻辑以及多个Actor之间的通信规则。Actor模型通过异步消息模式,实现分布式系统的高并发

Actor计算模式

Actor模型的三要素:

  1. 状态:Actor组件本身的信息
  2. 行为:Actor的计算处理操作
  3. 消息:Actor的消息以邮件的形式在多个Actor之间通信传递,每个Actor都会有一个自己的邮箱.

Actor工作原理

工作流程

Actor2从MailBox队列中依次取出Actor1和Actor3发送的消息执行相应的操作

消息传递流程

Actor的行为

当一个actor接受到消息后,它可以做如下事情:

  1. 创建更多的actor
  2. 发送消息到其他actor
  3. 指派对下一条消息做什么处理。

最后一条定义了actor如何操作内部状态。例如一个计算器作用的actor,初始状态是0,处理到加1消息时,它不改变内部状态,但分派下一条消息处理时,状态是1.

失败可容忍 Erlang语言中有个“由它失败”的思想。就是你不可能考虑到所有导致失败的问题,与其绞尽脑汁处理这些问题,不如让它自然失败,然后指派给失败处理者处理(例如恢复到稳定状态),在actor模型中,这是可行的。

actor之间的隔离性导致actor失败不会影响其他actor,监控者可以对自然失败的actor做直接处理而不会带来连锁问题。这让“自愈系统”成为可能,就是说一个actor异常后,监控者可以恢复一致性,可能以初始状态重起actor。

分布性 actor发消息时不在乎目标actor是本地运行的还是运行在其他节点。试想,如果actor只是只有内部状态的邮箱,只对消息做出反应,那就没人在乎它在哪里运行,知道有个地址让消息可以到达即可。这让我们可以创建分布式系统,并且在节点失败时独立恢复而不影响整个系统。

进一步建议看一下Erlang和Elixir语言, JVM上的 Actor库Akka,基于Actor的框架Vert.x

Actor关键特征

优势

Actor 的通信机制与日常的邮件通信非常类似。因此,我们可以进一步总结出 Actor 模型的一些特点:

  1. 实现了更高级的抽象 Actor 与 OOP 对象类似,封装了状态和行为。但是,Actor 之间是异步通信的,多个 Actor 可以独立运行且不会被干扰,解决了 OOP 存在的竞争问题。

2.非阻塞性 在 Actor 模型中,Actor 之间是异步通信的,所以当一个 Actor 发送信息给另外一个 Actor 之后,无需等待响应,发送完信息之后可以在本地继续运行其他任务。也就是说,Actor 模型通过引入消息传递机制,从而避免了阻塞。

3.无需使用锁 Actor 从 MailBox 中一次只能读取一个消息,也就是说,Actor 内部只能同时处理一个消息,是一个天然的互斥锁,所以无需额外对代码加锁。

4.并发度高 每个 Actor 只需处理本地 MailBox 的消息,因此多个 Actor 可以并行地工作,从而提高整个分布式系统的并行处理能力。

5.易扩展 每个 Actor 都可以创建多个 Actor,从而减轻单个 Actor 的工作负载。当本地 Actor 处理不过来的时候,可以在远程节点上启动 Actor 然后转发消息过去。

不足

虽然 Actor 模型有上述的诸多优点,但它并不适用于分布式领域中所有的应用平台或计算框架。因为,Actor 模型还存在如下一些不足之处:

Actor 提供了模块和封装,但缺少继承和分层,这使得即使多个 Actor 之间有公共逻辑或代码部分,都必须在每个 Actor 中重写这部分代码,也就是说重用性小,业务逻辑的改变会导致整体代码的重写。 Actor 可以动态创建多个 Actor,使得整个 Actor 模型的行为不断变化,因此在工程中不易实现 Actor 模型。此外,增加 Actor 的同时,也会增加系统开销。

Actor 模型不适用于对消息处理顺序有严格要求的系统。因为在 Actor 模型中,消息均为异步消息,无法确定每个消息的执行顺序。虽然可以通过阻塞 Actor 去解决顺序问题,但显然,会严重影响 Actor 模型的任务处理效率。

  1. 可重用性低,业务逻辑的改变会导致整体代码的重写
  2. 工程上难以实现
  3. 不适用于对消息处理顺序有严格要求的系统

Actor 主要应用

1.Erlang/OTP: Erlang是面向并发的编程语言,OTP是Erlang技术栈中的标准库

2.Akka: 为java和scala构建高度并发、分布式和弹性的消息驱动应用程序的工具包

  1. Quasar(Java): 开源的JVM库

框架与语言

Actor 模型在 1973 年被提出,已广泛应用在多种框架和语言中。可以说,很多框架或语言支持 Actor 编程模型,是为了给开发者提供一个通用的编程框架,让用户可以聚焦到自己的业务逻辑上,而不用像面向对象等编程模型那样需要关心死锁、竞争等问题。

那么,到底有哪些框架或语言支持 Actor 编程模型呢?接下来,和你列举几个典型的框架或语言吧,以方便你参考。

  1. Erlang/OTP。Erlang 是一种通用的、面向并发的编程语言,使用 Erlang 编写分布式应用比较简单,而 OTP 就是 Erlang 技术栈中的标准库。Actor 模型在 Erlang 语言中得到广泛支持和应用,其他语言的 Actor 逻辑实现在一定程度上都是参照了 Erlang 的模式。实现了 Actor 模型逻辑的 Erlang/OTP,可以用于构建一个开发和运行时环境,从而实现分布式、实时的、高可用性的系统。
  2. Akka。Akka 是一个为 Java 和 Scala 构建高度并发、分布式和弹性的消息驱动应用程序的工具包。Akka 框架基于 Actor 模型,提供了一个用于构建可扩展的、弹性的、快速响应的应用程序的平台。通过使用 Actors 和 Streams 技术, Akka 为用户提供了多个服务器,使用户更有效地使用服务器资源并构建可扩展的系统。Quasar (Java) 。
  3. Quasar 是一个开源的 JVM 库,极大地简化了高度并发软件的创建。Quasar 在线程实现时,参考了 Actor 模型,采用异步编程逻辑,从而为 JVM 提供了高性能、轻量级的线程,可以用在 Java 和 Kotlin 编程语言中.

高可用: 分布式集群容错

The Actor Model (everything you wanted to know...)

Our CPUs are not getting any faster. What’s happening is that we now have multiple cores on them. If we want to take advantage of all this hardware we have available now, we need a way to run our code concurrently. Decades of untraceable bugs and developers’ depression have shown that threads are not the way to go. But fear not, there are great alternatives out there and today I want to show you one of them: The actor model.

The model

The actor model is a conceptual model to deal with concurrent computation. It defines some general rules for how the system’s components should behave and interact with each other. The most famous language that uses this model is probably Erlang. I’ll try to focus more on the model itself and not in how it’s implemented in different languages or libraries.

Actors

An actor is the primitive unit of computation. It’s the thing that receives a message and do some kind of computation based on it.

The idea is very similar to what we have in object-oriented languages: An object receives a message (a method call) and does something depending on which message it receives (which method we are calling).

The main difference is that actors are completely isolated from each other and they will never share memory. It’s also worth noting that an actor can maintain a private state that can never be changed directly by another actor.

One ant is no ant

And one actor is no actor. They come in systems. In the actor model everything is an actor and they need to have addresses so one actor can send a message to another.

Actors have mailboxes

It’s important to understand that, although multiple actors can run at the same time, an actor will process a given message sequentially. This means that if you send 3 messages to the same actor, it will just execute one at a time. To have these 3 messages being executed concurrently, you need to create 3 actors and send one message each.

Messages are sent asynchronously to an actor, that needs to store them somewhere while it’s processing another message. The mailbox is the place where these messages are stored.

image

Actors communicate with each other by sending asynchronous messages. Those messages are stored in other actors' mailboxes until they're processed.

What actors do

When an actor receives a message, it can do one of these 3 things:

  • Create more actors
  • Send messages to other actors
  • Designate what to do with the next message

The first two bullet points are pretty straightforward, but the last one is interesting. I said before that an actor can maintain a private state. “Designating what to do with the next message” basically means defining how this state will look like for the next message it receives. Or, more clearly, it’s how actors mutate state.

Let’s imagine we have an actor that behaves like a calculator and that its initial state is simply the number 0. When this actor receives the add(1) message, instead of mutating its original state, it designates that for the next message it receives, the state will be 1.

Fault tolerance

Erlang introduced the “let it crash” philosophy. The idea is that you shouldn’t need to program defensively, trying to anticipate all the possible problems that could happen and find a way to handle them, simply because there is no way to think about every single failure point.

What Erlang does is simply letting it crash, but make this critical code be supervised by someone whose only responsibility is to know what to do when this crash happens (like resetting this unit of code to a stable state), and what makes it all possible is the actor model.

Every code run inside a process (that is basically how Erlang calls its actors). This process is completely isolated, meaning its state is not going to influence any other process. We have a supervisor, that is basically another process (everything is an actor, remember?), that will be notified when the supervised process crashes and then can do something about it.

This makes it possible to create systems that “self heal”, meaning that if an actor gets to an exceptional state and crashes, by whatever reason, a supervisor can do something about it to try to put it in a consistent state again (and there are multiple strategies to do that, the most common being just to restart the actor with its initial state).

Distribution

Another interesting aspect of the actor model is that it doesn’t matter if the actor that I’m sending a message to is running locally or in another node.

Think about it, if an actor is just this unit of code with a mailbox and an internal state, and it just respond to messages, who cares in which machine it’s actually running? As long as we can make the message get there we are fine. This allows us to create systems that leverage multiple computers and helps us to recover if one of them fail.

Next steps and other resources

This was a quick overview of the conceptual model that is the base of great languages like Erlang and Elixir and libraries like Akka (for the JVM) and Celluloid (for Ruby).

If I was successful in making you curious about how this model is implemented and used in the real world, this is the list of books that I read or am reading about this topic and can recommend:

  • Seven Concurrency Models in Seven Weeks: When Threads Unravel
  • Programming Elixir
  • Elixir in Action

And if you are interested in more details about the conceptual idea itself, I can’t recommend this video enough: