基于skynet设计游戏服务端框架

skynet并不是一个开箱即用的服务端框架,游戏后端在开展业务时,需要根据自身业务特点,合理设计相应的服务端框架。在这里我根据自身的设计目标,写下各方面的选择与取舍。对于小型企业来说,一些商业化的软件带来的成本负担是不可接受的,所以在各种组件的选型都倾向于开源软件。

设计目标

设计一个高性能低成本的游戏服务端框架,兼顾数据一致性与可用性。这里的成本指的是开发成本,维护成本,机器成本的总和。高性能我将其排在考虑的首位,对于游戏来说,流畅的体验至关重要,而且高性能是低成本的必要条件。强一致性与高性能是矛盾的,所以我只需达到最终一致性的目标,这里的最终一致性,也可以进行部分牺牲,牺牲在服务器出现断电等不可抗力故障时,允许丢失玩家部分最新数据,玩家感知上就像一个操作之后游戏就卡住了(可能提示了操作成功)。当然,充值等关键数据时绝对不能丢失。出现这些问题可以进行后续补偿。可用性最后考虑,因为游戏服务在业务上允许停服维护。
框架类型:分布式框架,每一种类型的节点都有冗余节点,增强可用性。
设计指标:单服支撑3万人在线,并且满负载下P99消息处理延迟为10ms。

数据库选择

游戏服务端是数据服务,首要的问题就是选择数据库进行数据存储。关注点按顺序依次主要为:稳定性,可用性,性能,扩展性。
这里为什么把性能放在比较靠后的位置?
因为对于数据落地存储来说,出现问题会导致玩家回档,玩家无法接受。我不能牺牲最终存储的数据一致性来换取性能。可以牺牲的一致性是在落地存储之前的最新操作。所以整个框架的关注点优先级和局部组件的关注点优先级可能会不同。
这样排序的结果是,整个框架的性能瓶颈在于数据库的性能。
业内主要有mongodb和mysql两种选择,mongo优势在于扩展性好,灵活性强,表现在机器横向扩展非常容易,表字段频繁更改友好,这对于游戏后端业务来说十分匹配,缺点在于稳定性不足。mysql优势在于稳定性,支持事务,缺点在于不易扩展,对表字段增删改不友好。
综合考虑,选择mysql,主要看中其稳定性。性能方面,我个人更倾向于RocksDB数据库引擎,对有玩家数据缓存的框架来说,写入的频率远高于读,写倾斜的数据库引擎可以带来10x的整体性能提升。选择mysql也是因为mysql支持RocksDB引擎后,过渡会比较方便。

写异步化

这里将高性能与一个指标关联起来,就是P99请求处理时长为10ms以内,想要获得高性能,需要将游戏中的业务处理的各个部分进行提速。先将skynet actor业务流程进行拆分。

  • 收到业务请求的socket消息,在skynet中有专用线程用于接受socket数据,性能可以保证
  • 消息投递到gate服务,消息投递与处理依赖actor模式,无消息阻塞情况下,性能可以保证
  • gate服务中进行消息解析,消息解析速度与消息格式相关,一般的消息解析都具备高性能特性
  • 消息投递到玩家专用agent服务,消息投递高性能
  • agent服务中消息处理,消息处理与业务复杂度相关,如果在登录时已经加载玩家全量数据到agent服务中,那么处理流程全部为内存操作,无消息阻塞情况下,可以保证性能。
  • 写数据到数据库,与关系数据库mysql的交互操作,如果为同步写,那么会很慢,可能超过100ms。这里就是关键点,我们要转换为异步写数据库,对应Write Behind模式。先将要更新的数据 同步写入redis,在通过消息队列方式异步写入mysql。这里为什么要先同步写入redis,不能异步写入redis吗?主要考虑到redis数据异步写入失败场景,会破坏数据一致性,而redis同步写入性能很高(1ms左右),能够满足高性能的要求。
  • 消息返回,消息投递高性能

服务节点拆分

拆分服务的关注点在于性能,分为 网关服务,游戏逻辑服务,聊天服务,登录与支付服务。登录与支付服务合并在一起,是因为渠道的登录与支付一般是同时接入,并且性能也满足需求。
网关服务为 游戏逻辑服务提供负载均衡,本身也是无状态服务。

skynet游戏逻辑服务组织模式

每个玩家对应一个agent服务(skynet service),用来处理玩家逻辑。相比于按功能分服务的模式,劣势在于耗费了更多的内存(每个虚拟机都加在一遍逻辑代码),优势在于隔离了玩家逻辑,减少热点服务,单元测试更加便利。

游戏数据分类

游戏数据分类为 玩家私有数据 与 其他数据。玩家私有数据分为 玩家单表单条数据 与 单表多条数据。其中玩家私有数据最终落地存入mysql, 其他数据只存储在redis中。玩家私有数据如:玩家账号,玩家道具,玩家角色,玩家金币,玩家邮件等。其他数据如:全服邮件模板,公告,公会信息,组队信息,全服BOSS进度,服务器状态,活动等。

ORM映射

每张表都有一个固定的id字段作为主键,id由redis生成保证自增。每张表都有且只有一个 唯一索引,在 单表单条数据中,唯一索引 为 pid,在单表多条数据中,唯一索引 为 pid + id或者pid + cid 或者 pid + 自定义其他的id。这里我限制了唯一索引最多由两个键组成,方便数据获取api的编写。对于需要3个以上组成唯一索引的业务需求,使用 复合id(一个id包含多个字段信息) 解决。
表结构更改与维护,通过sql文件,手动维护 后端 表数据结构定义与mysql的表定义的一致性

缓存选择与缓存方案

缓存使用redis,使用cache-aside模式对玩家数据做全量数据缓存,使用redis消息队列,消息队列中的数据为单条数据的更改,可以算作是数据零散缓存。

消息队列

参考我的另一篇redis消息队列的文章 为什么要造轮子?

热更方案

使用 skynet自带的codecache方案,将数据与逻辑分离,可以很方便的更新agent服务中的业务逻辑代码。 其他的代码需要使用inject方式注入更新。不方便的更新还可以使用灰度更新机制。

通信协议

选择sproto,更加轻量

你可能感兴趣的:(服务器开发,游戏,数据库,架构)