Node.js 应该拥抱 Actor 模型

Node.js 是近年来服务器端开发工具中可以说最为成功的一个工具了,不仅仅利用 JavaScript 和 Reactor 模型来达到快速开发高并发应用的目的,也顺利入侵前端生态圈,前端开发各种必备的工具链基本都使用 Nodejs 开发。我也用 Nodejs 做过一些针对并发的项目如聊天室等等,但是始终觉得 Nodejs 的基础理念和各种框架还不足够抽象,以帮助我快速对此类项目进行清晰有条理的建模。当项目尺度越来越大之后, 为了达到更好的 Scalability 和高可用性,容错性,因此我越来越希望 Nodejs 能有类似于 Erlang 中的 Actor 模型的框架来实现这些目标。

以往解决并发,往往是一个进程服务一个链接,但是进程占用资源过多,无法很好的伸展。后来变成了一个线程服务一个链接,虽然可以开很多线程,但是频繁的上下文切换消耗过多的 CPU 时间,线程之间的状态共享又容易出问题,所以也无法很好扩展。

Reactor 模型利用事件机制,在 IO 密集的场景中,解决了两者的问题。由于 IO 密集的场景下,大部分链接都是在等待传输等,只有少数链接处于活跃状态,所以可以通过事件的方式,仅仅处理需要处理的那部分,来减少资源的开销。

但是Reactor 模型也不是没有缺点,在 Nodejs 中:

  1. 单线程,难以利用多核优势,一旦阻塞了事件循环,会导致所有任务都会延迟。
  2. 过多的回调导致难以组织代码
  3. 回调函数打乱了控制流程,让处理异常变得麻烦

Actor 模型的基本概念

同样是由于线程和进程模型的一些缺陷,Erlang 之父Joe Armstrong在解决电信交换机上的高并发问题的时候,提出了 Actor 模型。简单的说,每个 Actor 封装了自己的逻辑和状态,互相之间不共享任何状态,只能通过消息(Message)来互相通信。每个 Actor 有一个自己的邮箱(Mailbox),是一个存放消息的队列,Actor 会一个一个处理每一个消息。

_JC@ABJ3(7{)]Y1VF@3PH58

Actor 之间不共享状态,就减少了很多多线程中的锁的问题。顺序处理消息,也保证了 Actor 内部不容易出现竞争条件(Race condition)。

当然,Reactor 模型和 Actor 模型并不完全排斥,Actor 模型底层也是可以使用 Reactor 的。

Actor 具有天生的分布式特性,我整理了一张表:

Actor Process Machine Service
标识 唯一 ID 进程 ID,端口 IP 地址,Mac 地址 IP,域名,服务发现
状态 不共享 通常不共享 不共享 不共享
交互 消息 信号,管道,网络 网络 网络
容错 Supervisor 机制 supervisord, monit,等 heartbeatkeepalived Cluster 内建,DNS,服务发现

其实 Actor, Process,Machine,Service,在很多方面都是一脉相承,正因如此,内置了 Actor 模型的 Erlang 才在云时代大放光彩。

虽然 Nodejs 号称是云时代的编程工具,但实际上在这方面做得并不特别好。

那为何 Nodejs 应该加入对 Actor 的支持?

清晰的单元

首先,Actor 本身只是将一个逻辑处理单元给具体出来,即便我们的 Nodejs 程序没有使用 Actor 模型,实际上在处理并发的时候,依然会隐含着这种逻辑处理单元。Nodejs 中是通过闭包来隔离内部状态,通过事件/回调函数来处理消息。随着代码量的增加,为了让代码更加结构化清晰化,我们通常依然会封装出一些自己的接近 Actor 模型的对象。虽然我们现在有 Promise,未来有 async/await,但是这主要是封装一次『任务』,而非整个逻辑。

Actor模型理念跟面向对象一致

Actor 模型其实本质上也是符合面向对象编程的理念的,面向对象编程中

  1. 对象也是同时封装了状态和行为的单元。
  2. 对象和对象之间通过『消息』进行互相操作

其实在面向对象语言的鼻祖 Smalltalk 中,方法并不叫方法,而是叫『消息选择器』(Message Selector),故名思议就跟函数式语言中,对消息结构进行模式匹配一样。

JavaScript 同样是一门面向对象的语言,是完全可以和 Actor 模型结合起来的。

把前两点结合起来举例,在 Nodejs 中,一个 HTTP 请求往往会对应到自己的一个上下文 context,里面包含了请求request和相应response的对象。这种封装的计算单元就有了 Actor 模型的一些基本理念,像 Koa 中更是使用了 generator/yield(将来可能是 async/await)来简化控制流。

我们可以得到一个理想的单进程内的 Actor 模型

KQRGE}1IRHRI9_M8P{V}W9C

其实上图用一般的面向对象和类来理解也是非常简单的,对吧?

可以优化错误处理以及容错性

在 Node 中,由于控制流的混乱,很难确定出错的位置,有时候出错也会导致整个进程的 crash。而有了 Actor 之后,错误的范围就可以聚焦到 Actor 上,而不会像原来那样找不到出错的位置。同时,也可以引入类似于 Erlang OTP 的 Supervisor 机制。Erlang 的理念是 Failfast,但是Erlang 中的代码出错之后,会有监控者进行善后。

分布式

Actor 模型的理念具有天然的分布式潜力。如果在 Nodejs 中能有良好框架的支持,让跨进程的消息传递变得透明的话,那么在各种应用上的 Scalability 都会有极大的提升。一般 Web 服务的 nodejs 层那样是无状态的,横向扩展比较容易,加服务器就够了,而写长连接的 nodejs 应用比如聊天室的时候,为了实现 nodejs 层的横向扩展,不得不通过更后端的诸如 redis 来实现跨进程通信,更后端的压力更大,也由于中转导致延迟更大。甚至在某种程度上,我们可以实现在不同服务器之间的动态调度。

有了透明的分布特性之后,同时高可用性也更容易达成,我们可以将 Actor 的状态,跨进程/机器去进行 Replication,当某个节点 crash 之后,可以在 replication 的节点上重启相应的 Actor。

L$7%A7B]C8VKFN2BRJP8STJ

(图片来自网络)

解决方案

在众多 Actor 模型的解决方案中,使用 Fiber 是跟 Reactor 最为自然的搭配,因为 Fiber 就是封装了一个计算过程,并且可以根据情况将 Fiber 暂停和恢复,这就可以通过 Reactor 中的非阻塞的特性,利用事件机制将活跃的 Fiber 唤醒,将需要等待的 Fiber 暂停。我们可以很容易的通过对 Fiber 进行封装,在 Nodejs 中实现 Actor 模型。像 fibjs 就能很好的完成这一任务。

但我认为 Actor 模型并不一定非等需要使用 Fiber,只要内心包含相应的理念,即使只使用 Promise,async/await,也是完全没有问题的。

Virtual Actor

微软提出了一套新的理念称之为 Virtual Actor,非常接近于我所追求的框架和平台。

Virtual Actor在基础的 Actor 模型上增加了几点:

  1. 实例化后始终存在,自动 replicate 到其他节点。
  2. 不活跃的 Actor 可以进行垃圾回收或者被持久化。
  3. Actor 位置透明,任何节点之间都可以互相访问
  4. 自动 Scale Out

RMV7Y~QEWMZ_[}K93``VQ(Q

目前实现了 Virtual Actor 的框架有,微软.net 平台的 Orleans, Java 上也有开源的 Orbit。并且 Orleans 的平台在微软内部有很多的实际应用,包括 Halo 的服务器也应用了,并能达到上百万的在线。

相对于 Erlang,Akka 之类的基础 Actor 平台,Virtual Actor 更为简单易用,而 Erlang 由于更加底层,能够对自己的应用进行更好的调整。

如果 Nodejs 也能出一套类似于 Orleans 的框架,我觉得对于 Nodejs 实现更大系统有极大的帮助。

“Node.js 应该拥抱 Actor 模型”的2个回复

  1. actor不是erlang首创,它最晚是解决机器学习都得。actor模型本质上是更细粒度执行实体,把调度问题交给应用层而不是操作系统。
    文中提到的Virtual actor还是第一次听说,长见识了。捎带做个广告——公众账号,写程序的康德里面有几篇关于actor的欢迎一起交流。

  2. Israel cannot give up one inch of anything. Never.It would not end until every last part of the land was goaeeIsrn.l must stand firm and take a hard line.

发表评论

电子邮件地址不会被公开。 必填项已用*标注