miller
发布于

深入解析actor 模型(一): actor 介绍及在游戏行业应用

原文地址 zhuanlan.zhihu.com

1 介绍

1.1 什么是 actor

对于刚接触 actor 的我,第一感觉就像 redis 一样,每个 actor 就是一个 redis 实例,都有自己消息队列,actor 相互通信通过将消息发给对方,消息发送进对方的消息队列,等待对方线程处理。来看看我们之前做项目的痛点。

游戏服务器通常分为多个服,每个服上有多个玩家。假设玩家与玩家数据交互操作,这时怎么避免数据竞争?两种解决方案:

1、将数据写入 redis

首先 redis 效率高,也是单线程模型,不存在数据竞争,但是也有它的不足之处,操作 redis 会受网络影响,即使再快也避免不了网络 io 的开销。go redis sdk 在序列化响应回来的消息时,会序列化成字符串,大量的操作会造成 gc 的压力。

2、加锁

这个就是很常用的做法,避免数据竞争,但是玩家其实大部分数据都是自身有的,属于玩家自己,如果每个玩家操作别人数据都加锁(例如扣血),在大型游戏如果王者荣耀这种实时性高的游戏,性能估计会很差。但是在面对 mongo 事务写冲突时,加互斥锁估计就无法解决了。这时候只能将粒度调大,调大在事务级别,这样性能就更差了。一旦业务处理耗时太长,所有相关玩家都将会感觉到卡顿操作。

有没有更好的解决方案?有,当然那就是 actor?

Actor 模型是 1973 年提出的一个分布式并发编程模式,在 Erlang 语言中得到广泛支持和应用。

在 Actor 模型中,Actor参与者是一个并发原语,简单来说,一个参与者就是一个工人,与进程或线程一样能够工作或处理任务。

来看看来自于维基百科的解释:

From Wikipedia:

The actor model in computer science is a mathematical model of concurrent computation that treats "actors" as the universal primitives of concurrent computation: in response to a message that it receives, an actor can make local decisions, create more actors, send more messages, and determine how to respond to the next message received.

The Actor model adopts the philosophy that everything is an actor. This is similar to the everything is an object philosophy used by some object-oriented programming languages.

“计算机科学中的 actor 模型是一个并发计算的数学模型,它将 actors 视为并发计算的通用原语:actor 可以做出本地决策,来作为其接收到的消息的响应,创建更多 actors,发送更多消息,并确定如何响应接收到的下一条消息。 Actor 模型采用的哲学是一切都是 Actor。这与一些面向对象编程语言应用的 “任何事物都是一个对象” 的哲学类似。”

在 Scala 和 erlang 语言有 actor 类似的设计理念。在计算机科学领域,Actor 是一个并行计算的数学模型,最初是为了由大量独立的微处理器组成的高并行计算机所开发的。

Actor 模型的理念非常简单:万物皆 Actor

在 Actor 模型中主角是 actor,类似一种 worker。Actor 彼此之间直接发送消息,不需要经过什么中介,消息是异步发送和处理的。在 Actor 模型中一切都是 Actor,所有逻辑或模块都可以看成是 Actor,通过不同 Actor 之间的消息传递实现模块之间的通信和交互。

actors 直接是隔离的,并不会共享内存,所以只能通过发邮件去和对方打交道。

Erlang 引入了” 随它崩溃 “的哲学理念,这部分关键代码被监控着,监控者supervisor唯一的职责是知道代码崩溃后干什么,让这种理念成为可能的正是 Actor 模型。

在 Erlang 中,每段代码都运行在进程中,进程是 Erlang 中对 Actor 的称呼,意味着它的状态不会影响其他进程。系统中会有一个supervisor,实际上它只是另一个进程。被监控的进程挂掉了,supervisor会被通知并对此进行处理,因此也就能创建一个具有自愈功能的系统。如果一个 Actor 到达异常状态并且崩溃,无论如何,supervisor都可以做出反应并尝试把它变成一致状态,最常见的方式就是根据初始状态重启 Actor。

简单来说,Actor 通过消息传递的方式与外界通信,而且消息传递是异步的。每个 Actor 都有一个邮箱,邮箱接收并缓存其他 Actor 发过来的消息,通过邮箱队列mail queue来处理消息。Actor 一次只能同步处理一个消息,处理消息过程中,除了可以接收消息外不能做任何其他操作。

如上图所示,多个 actor 通过发信息进行交流,actor 与 actor 之间可以交流,但是不会产生数据竞争。

1.2 actor 组成

1.2.1 状态(state)

指 actor 本身的属性信息,state 只能被 actor 自己操作,不能被其他 actor 共享和操作,有效的避免加锁和数据竞争

1.2.2 行为(behavior)

指 actor 处理逻辑,如果通过行为来操作自身 state

1.2.1Mailbox 邮箱

指 actor 存储消息的 fifo 队列,actor 与 actor 发送消息,消息只能发送到邮箱,等待拥有邮箱的 actor 去处理,这个过程是异步的。简单来说,有时间才处理,等我把前面任务先完成。

Actor 模型遵循下面的规则:

  • 所有的 Actor 状态是本地的,外部是无法访问的。
  • Actor 必须通过消息传递进行通信
  • 一个 Actor 可以响应消息、退出新 Actor、改变内部状态、将消息发送到一个或多个 Actor。
  • Actor 可能会堵塞自己但 Actor 不应该堵塞自己运行的线程

来看看 actor 如何通信

  • actors 都有自己的邮箱地址,这个地址无论是本地还是跨服,是唯一的,actor 之间只能通过 message 进行交流,message 会发到接收 actor 的 mailbox,等待接收 actor 处理。如图 actorA 向 actorB 发送消息。
  • 多个 actor 向同一 actor 发送消息,按照时间顺序投递进对方 MailBox,图中 actorA 和 actorc 分别向 actorB 发送消息,按照时间顺序进入 actorB 的邮箱。
  • actorB ,actorC,actorD 分别在各自的线程运行,在 go 中就是 goroutine,每个 actor 各自处理各自邮箱中的任务,互不干扰,且开发者写业务不需要关心任何锁的问题,大大减少了心智负担和维护成本。
  • actor 能够根据到来的消息通过行为修改自己的状态,可以发送信息给其他 actor,可以创建新的 actor 或者新的子 actor。

1.3 actor 和 csp 区别

goroutine 和 actor 对比

  • csp 和 actor 最大的不同是 actor 需要知道接收方的地址,需要知道将消息传递给谁,而 channel 不关心接收方和发送方是谁,只关心传递介质,相当于 CSP 把发送方和接收方给解耦了。
  • csp 是同步虽然 buffer 支持有限量的异步,而且它关心接收者是否处理信息,没有处理就阻塞
  • actor 是异步的,只关心发给谁,不关心消息是否处理和传递通道,发送者不能假定发送的消息一定被收到和处理。Actor 模型必须支持强大的模式匹配机制,因为无论什么类型的消息都会通过同一个通道发送过来,需要通过模式匹配机制做分发
  • 还有一个极好的特性,可以轻松地允许 actor 在其网络上的不同计算机上存储,实现分布式。消息分组和路由是自动处理的。

actor 模式缺点:

  • 有些情况下需要流量控制。虽然这些可以在 Akka 中使用握手对话来实现,但代码会变得混乱,不再有效。
  • actor 没有拒绝交流的能力。当消息到达时,必须对其进行处理,或者将其放入缓冲区,以便稍后处理。goroutine 可以选择处于忽略其通道子集的状态。goroutine 可以选择监听全部或部分 channel,具体取决于其状态。一个示例可能是一个具有一个输入和一个输出 channel 的简单固定大小队列 goroutine 的实现。最初它是空的,因此忽略其输出 channel。最终,当它忽略输入 channel 时,它可能是满的。否则,它将在两个通道之间进行选择,以决定要做什么。因为 goroutine 可以忽略任何一个通道,所以它的逻辑表达得非常简洁。
  • Akka 完全依赖无限的输入缓冲区,永远不会耗尽内存,但这并不能保证。

CSP 方式的网络并发行为有一个重要的缺点:

  • goroutine 相互依赖会造成死锁,开发人员必须有意识的避免有死锁代码出现,这需要识别失败依赖关系并且修改。有比较出名的技术,那就是客户端 / 服务器模式
  • 假设将 goroutine 转换成 actor,非阻塞意味死锁不会发生,但是我们怎么保证队列不会溢出,因为 mailbox 是无限的,容量受计算机内存限制。死锁和无限缓存内存耗尽关系是等价的
  • goroutines 只能一台计算机中工作(尽管有任意数量的内核),不能在网络通信。这与 actor 不同;网络层实现 channel,它还不如其他基于 CSP 的通信,例如 JCSP for Java,它包括通过网络连接透明操作的通道,保留正常的同步语义。
  • 关于 goroutine 的最后一点是它们的终止是任意的。与 Occam(另一种 CSP 语言)相比,Occam 中的进程由其父进程 “拥有”,父进程等待其所有子进程终止。通过显式添加同步代码,可以使 Go 以更严格的 CSP 方式运行(如 Occam)。

Roland Kuhn Xitrum Scala web framework 创始人提的建议:

我没有使用 Go,因此我对该部分的知识有限。(他很谦虚)

  • Go channel 密切模拟了通信顺序过程的语义(Hoare,1978),而 Akka actor 实现了 actor 模型(Hewitt,1973)。
  • 两者都描述了通过消息传递进行通信的独立进程。主要区别在于,消息交换在 CSP 中是同步的(即,两个进程在其中传递消息的执行的 “接触点”),而在 actor 模型中是完全解耦的,消息传递未经发送方确认,并且可以在任意时间。因此,actor 之间享有更大的自由和独立,因为他们可以根据自己的状态选择何时处理传入的消息。程序员必须预见检查不同传入消息的正确顺序,以避免阻止程序进行。好处是 channel 不需要缓冲消息,而 actor 需要理论上无限大小的邮箱。
  • 将消息的发送与其在接收方的处理分离的好处是,在没有用户显示声明下,在不同的网络节点上传递消息变得很轻松。它还允许接收者不可用(例如,由于软件或硬件故障),而不会影响发送者,除了无法获得回复。

总之,channle 对于在严格控制的环境中协调并发执行非常有用,而 actor 为松散耦合的分布式组件提供了抽象。

2proto-actor 使用

github: https://github.com/asynkron/protoactor-go

原文写的非常好。 后续内容参见 原文

浏览 (2390)
点赞
收藏
评论