45

麻将游戏后端架构里的多并发模型

 5 years ago
source link: https://www.tuicool.com/articles/BnyQvyV
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

写在前面

受到政策因素影响,经历了近三个月封闭开发的 汇闲麻将 最终还是没能成功上线。当前的感悟,创业的路上有很多槛,技术研发只是其中的一个槛。

这里仅以一名程序员的角色总结一下 汇闲麻将 的后端架构,也算是给过去三个月的自己一个交代。

jmuARb2.png!web

汇闲麻将的后端架构

信息收集

就我个人的方法论,拿到一个问题后,① 首先要做的是尽可能多地收集情报,② 然后分析情报,有必要的情况下还要进一步收集情报,③ 接着才是制定方案,④ 最后实施方案。

三个要求

在后端架构初期,负责产品的同事就给后端的架构提了三个老生常谈的要求:

  • 支持高并发(单台服务器至少大几百并发)
  • 高可扩展性(方便产品迭代添加新特性)
  • 支持平行扩容(多台服务器同时提供服务)

汇闲麻将后台中的核心对象

  • 玩家 :每一个进到游戏中的用户都是玩家,当然也包含陪真人玩家打牌的机器人玩家;
  • 牌桌 :每 4 个玩家加入到一个牌桌进行游戏;
  • 大厅 :每个玩家进入游戏后,会首先到达大厅,参与转盘、签到、每日任务等功能;
  • 其他 :其他一些小的对象,比如麻将、色子等,这些就都比较容易处理了。

需要考虑的几个关键问题

  1. 同一个玩家的不同操作需要是保序的,比如用户登录后才可以进行入牌桌的动作;
  2. 同一个牌桌上进行的操作也需要是保序的,比如入桌顺序、出牌顺序等;
  3. 需要控制协程(goroutine)的数量,避免恶性增长资源耗尽;
  4. 需要监控牌桌信息,采样牌桌状态从而便于查错;
  5. 玩家的断线重连,游戏状态的恢复;
  6. 机器人玩家的开发;
  7. 其他。

上面的几个问题并未涉及到麻将游戏的具体逻辑(比如算输赢、算番数等),架构做好后可以填充这些逻辑。

历史经验

  1. 为了避免并发问题,传统的麻将游戏后端,每个房间一个进程;例如菜鸟场、富豪场,每个场都是一个房间,房间里包含许多的牌桌,这些牌桌上的逻辑由一个进程轮转处理。
  2. 每个房间启动一个进程,并暴露对应的端口供客户端连接。
  3. 用户进入游戏的的逻辑步骤是这样的:① 用户首先登陆到登陆服务器进行登陆鉴权;② 用户拿着鉴权得到的秘钥连接到大厅服务器,进行转盘、签到、任务、选择房间入桌等操作;③ 用户选择房间后,连接具体的房间服务器(游戏服务器)进行游戏;④ 用户进行完游戏后,与游戏服务器断开,重新与大厅服务器建立连接,回到步骤②。

信息分析与结论

由于我的技术栈是 Golang,因此选定了 Golang 作为汇闲麻将的后端开发语言,分析问题的时候自然就带入了 Golang 的语言特性。

  1. 受开发资源(时间和人力)的限制,不拆分登录、大厅、游戏等模块,在一个代码库中进行开发,方便把控研发节奏,降低前期的运维难度;
  2. 同样的道理,游戏服暂不按照房间进行进程上的划分,所有的房间都在一个主进程下面(启用 Golang 的多线程特性),对房间里的牌桌进行动态调整(如果某个房间里的牌桌不够用,而其他房间里闲置的牌桌比较多,就临时“借一个”使用)。
  3. 每个牌桌挂一个 goroutine 处理牌桌上的信息(牌桌状态轮转、用户出牌吃牌等);
  4. 大厅的交互频次较低,只需挂一个 gotoutine 处理所有用户的相关动作;
  5. 玩家断线重连时,通过替代底层的 session 进行恢复;
  6. 数据入库时由专门的 goroutine 池负责写入,从而避免对游戏逻辑的阻塞(为此还专门写了开源项目 gochan );
  7. 为方便分析各个组件的状态,统一打印日志,并把日志收集到 ELK 中进行分析(为此专门写了开源项目 sugar );
  8. 其他。

汇闲麻将后端架构里的并发模型

3QvAFnj.png!web

在架构设计初期,我曾经尝试通过 的方式维持玩家、桌子等的信息一致性,后来编写代码的时候发现逻辑非常的啰嗦,很多操作都需要考虑到加锁与解锁,当业务逻辑稍微变得复杂后难以维护,还很容易出现死锁的情况。那几天正好看到一位同事在玩《异星工厂》,受里面的传送带的启发,构思出了最初的“游戏后端线程架构图”原型,如上图所示。

“不要通过共享内存来通信,要通过通信来共享内存”,这句话是 Go 社区中非常经典的一句话。上面的架构图的设计一脉相承了“通过通信维护对象状态”的思路。每个协程(goroutine)搭配一个传送带(buffer-channel),此协程只处理自己传送带上的逻辑(闭包)。 上图中每个圆圈都是一个协程,圆圈的周围则是配套的传送带,外界(其他协程)可以把逻辑封装放置在传送带上,然后被当前协程顺序进行处理。

具体的:① 每个用户与游戏服的长连接上面挂两个协程(goroutine),其中读协程(read)负责读取客户端传送过来的数据,写协程(write)负责写服务端返回的数据给客户端。② 读协程对用户数据进行拆包后,把请求打包成为任务放置到主协程(main)的传送带上(信道),主协程依次处理自己传送带上的任务,进行简单的逻辑处理后分发给相应的牌桌(table)(把逻辑打包成为任务放置到牌桌的传送带);③ 各个牌桌的协程依次处理自己传送带上的任务,并把响应的发送任务给写协程(write);④ 写协程负责统一把数据返回给用户;⑤ 对于不同房间(room)中桌子的分配、借还等,由一个总的房间协程统筹进行管理。

有了上面的并发模型图,模块的划分就变得有依据也更合理,差不多花了两个月的时间,汇闲麻将就部署到预发布环境进行测试了。最后因为政策限制没有能正式发布,还是非常可惜的。。。

小结

“不要通过共享内存来通信,要通过通信来共享内存”;在设计并开发完汇闲麻将的后端业务逻辑后,感觉对这句话的理解更透彻了。当然,这里并不是强调锁没有使用价值,其实在一些场合下使用锁会更合理,就像《 浅谈 Golang 中数据的并发同步问题(三) 》中所描述的那样。

参考


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK