Contents

actor 模型介绍和感悟

为什么有这篇博客

一切源自于我去年的毕业设计,我毕业设计的课题是《基于unity打造的第三人称设计游戏》。刚一看到这个题目的时候你一定想到,这和actor 和 网络有什么关系吗?确实,按照毕设的要求,是不需要这个游戏有网络模块的设计的。但是嘛,因为我快毕业那会非常想找一家游戏公司的后端进行实习,未来的职业规划也是往游戏后端的方向发展的。所以我自告奋勇的跟导师说这个游戏我要加上网络模块(其实之前有过类似的游戏协议序列化和网络同步的经验)。

选定课题没多久,我就在广州找到了我的第一家实习公司,是一家做大型mmo的游戏公司(刚去他家的时候还微微鄙视他家的框架,现在看来简直是我无法企及的神)。 东家用的libevent 写的网络库(其实从这就可以看出是一家成立很早的公司了),然后暴露给lua调用,逻辑全是用lua写的,由于lua不支持真正的多线程所以东家用的多进程,利用rpc解决进程同步的问题(当时初出茅庐,这个地方狠狠的坑了我一把,也导致我给东家留下了第一个不好的印象)。所以我很自然的学到了protobuf 和 lua这样的技术。 有了这两个技术之后,我便开始着手自己毕设的后端框架了。去网上找了一通资料,发现skynet(一款基于actor模型开发的框架)这个框架,我立刻被skynet的actor模型给吸引了,并且立志我也要基于actor模型写出一个自己的框架。

四个月后,由于我总是在忙自己的毕设,给东家留下了第二个不好的印象,我被开了。被开的时候,我的老大把我拉到办公室跟我说了很多让我受用的话。在反复的思考他说的话之后,我开始学习go语言。并且在离职的那段时间里,我写了自己的golang 协程池,写的时候知道golang已经有一个非常厉害的协程池叫ants,所以写完之后和ants battle了一下,但是发现我只在百万以上级别的性能上能够领先ants, 但是内存占用却完完全全的输给了ants,而且ants还有动态伸缩等等一些我没有的特性。我还是菜了点😂。但是这个经历却让我尝到了绞尽脑汁解决问题的甜头, 也让我在实践中学到了很多的东西。 个人认为实践和看书必须相互结合,先看书,再实践,实践完了再看书又会收获新的东西,只有实践后的只是才能真正的内化。

介绍自己写协程池的经历是因为,在写协程池的时候,我了解到了golang的协程调度器GMP。这个调度器和skynet内部的线程池非常的类似,我可以基于这个GMP去写我的后端框架。

Actor模型

Actor模型(Actor model)首先是由Carl Hewitt在1973定义, 由Erlang OTP 推广,其 消息传递更加符合面向对象的原始意图。Actor属于并发组件模型,通过组件方式定义并发编程范式的高级阶段,避免使用者直接接触多线程并发或线程池等基础概念。

Actor模型=数据+行为+消息。

  1. actor模型是一种天然的分布式模型,actor可以分布在不同的地方,可以是本地,也可以分布在不同的网络主机上。
  2. 强隔离性,actor之间只能通过每个actor的mailbox来协作,一个actor不可以直接操纵另一个actor,只能通过写信的方式通知另一个actor自己要做什么。也就是说,actor与actor默认且强制要求使用类似物理隔离的主机那样使用rpc的方式协作。
  3. 位置透明,抹平本地与非本地的差异,actor模型中无需关注一个actor部署在了哪个地方。
  4. 异步,由于通过mailbox来实现协作,所以发送者在将消息推送到接收者的mailbox后,若无需等待结果,就可以直接去做别的事情。剩下的就可以交给接收者让它自己去消费这条消息了。
  5. 无锁,同样由于基于mailbox实现协作,消息是具有顺序的,我们无需关注资源竞争带来的数据一致性问题(你不能直接操作actor,只能告诉它你要做什么)
  6. 天生支持负载均衡,而且对水平扩容的支持非常好(actor 强迫你对服务进行划分,做成微服务那样)
note

这里提一嘴在我面试现东家的时候,老大和我探讨actor模型时,给我出的一个题目。当时回答的不是非常好,所以这次写博客总结一下。 当时老大在听完我介绍actor模型后,就问我,由于actor和actor之间是通过mailbox进行协作的,每个actor的状态都是自维护的。现在每个玩家登陆,我们就会实例化一个actor出来,如果我现在想广播一条协议给全服在线的玩家,我就需要给每个玩家的actor的mailbox推送一条指令,这个是非常消耗资源的。这种情况,你要怎么解决?

这个问题其实问的主要是,当我要让大量的actor进行相同的操作的时候,就会遇到需要向每一个actor的mailbox插入消息的情况。假设现有3w的玩家在线(这个已经很多了),如果消息的大小非常小的时候(比如只有byte级),如果一条消息100byte,那广播一次的成本是2929KB 也就是差不多3M的样子。但是如果广播的消息比较大 达到了K级别如果一条消息的1k,那广播一次的成本是 30M, 但是100k的话,成本就是3G。

但是实际的情况是,一条协议能过突破1k,已经很不容易了。 假设一条协议有10个字段,每个字段占8Byte(64位),也才80byte,100个字段也才突破1k。虽然消耗上来说,会比不用actor的要大,但是并非无法接受的程度。

假设真的遇到了那种需要广播一条非常大的协议(假设一条协议他突破了100k?),我们还可以从框架的层面去解决这个问题。这里给出几个解决的思路。

  • 假设我们现在遇到了群发协议的情况,其实正常的框架设计是不可能让玩家的agent actor(一个玩家会有一个叫做agent的actor)去收发协议的,而是在客户端和 agent actor之间用一个叫做router的actor进行一个中转,这个router负责编码/解码协议,并推送给指定的agent actor或者客户端, 它管理了玩家的socket。 所以如果是广播协议,其实不用通过agent actor. 直接通过router, 让router拿到每一个在线玩家的套接字去群发就好了。所以遇到这种需要发送给一类actor的情况,优先考虑是否能通过这些actor 的 manager来解决,不得已的情况下才需要给每个actor 分发message。

  • 还可以把协议做成指针,指针的指针的传送成本是非常小的,一招鲜吃遍天😂。skynet中就有一个这样的服务,专门用于广播消息。不过实现的细节是比较复杂的可以去看一看skynet的新组播方案 这里我先立个flag 等我的框架写到这部分的时候再细细研究

  • 再不济,你就把这种需要世界广播的业务(比如世界聊天)用单独的服务器承载,把业务划分出来,让单独的服务器去做。

actor模型的实现除了skynet,akka,proto.actor(有两种实现一种是 c# 还有一种是 golang实现的 golang实现的还不稳定) 还有在游戏中广泛使用的erlang语言,天生从语言层面就支持actor模型

要想自己造一个actor的轮子你得清楚网络数据传输的解码/编码,熟悉消息队列。