游戏服务端架构的思考

我从业游戏服务端有10年了,阶段性做个小结,总结我对游戏服务端的理解,这次就谈下服务端架构以及如何优化。文章就以我的经历作为开头。

写在开头

10年前,我加入了一家pc端的游戏公司,那时候国内端游已经是走向没落的时候了,公司正在开发的游戏是mmorpg,玩法比较像魔兽世界,这款游戏他们在此之前已经开发5年了,导致美术资源在当时看来比较落后了,不像同期火热的页游美术那么精致。

但是我觉得,像魔兽世界这种游戏,玩家大地图的游走和打怪,不同职业的配合和牵制,玩家与玩家之间亲密的合作、激烈的对抗是能补足美术这点不足的,特别是巡游boss,一个肉盾抗几下就挂了,基本需要10-20人通力合作才能把 boss 打死。

很快,大量玩家来玩这个游戏,以及客服收集到的评价验证了我的想法。可令人没想到的是,开发问题很大,这个游戏上线期间每天都有1-2次进程崩溃,玩家数据回档。

很可惜,我不是这个项目的开发,我不了解这个游戏的架构,只是这件事在我的心里埋下了种子,让我萌生想要解决问题的想法。

全区全服架构

游戏需要一套可伸缩性框架,满足前期快速开发需要,也能满足中后期拓展,支撑海量玩家。这里补充一下,前期指的是游戏从立项开发到整包测试的时间。
架构图如下:

以上,玩家由负载均衡,分配到不同的 lobby server,玩家的基本游戏行为,都在 lobby server 完成。当玩家需要完成跨服玩法,才会去请求 cross server。

cross server 代表了一类跨服玩法,当不同的游戏业务需要可以定义多组不同的 cross server,比如 cross server 可以细分出 family server、node server、battle server 等等。
作者 没有开花的树
而多个 cross server 有时需要有一个 cross master 来统一管理,比如工会模块,需要统一管理工会数据、创建和解散等等。当然,cross master 可以不作为独立的server,由 cross server 之间竞争产生。

游戏开发前期,不要对 lobby server 做过多的业务拆分,切莫过度优化。除了全服共享的和单例的业务,其他能 lobby server 完成,就都 lobby server 完成。原则是先实现,后优化。

开发到上线前的阶段,就要对系统的热点和瓶颈做优化,重点关注 lobby server 中高cpu使用和高内存使用的模块,能优化则优化,不能就拆分出来,避免他们对资源的过多蚕食导致整个 lobby server 的性能下降,而且,他们拆分出来,可以实现更精细的资源管理。(怎么找系统热点和瓶颈,后面压测和监控讲到)

架构能否适用分区分服?

先说答案,分区分服可以考虑用全区全服架构,但不是完全合适。

很多分服的架构使用了单一进程,虽然他们选择了性能较好的机器,但机子有概率发生故障,进程也可能crash。新开的区服同时在线非常高,老的区服比较低,使用一样的机器配置会造成资源浪费。当然,分服可以是多进程多机器的架构,这就近乎全服架构了。

另外,分服游戏基本都会不定期开新服,不定期合并老服。这样,既想新服使用多机器,又想要缩减老服的配置,就显得不合理,一方面增加了开新服、合旧服的运维成本,另一方面无法应对突发的压力。这也是为什么分服架构多是单进程架构一个重要原因。单服容纳4-5k人同时在线,满了显示繁忙,不允许玩家登录(或者排队等空位登录),引导玩家选择顺畅的服务器。也有游戏的做法是同时开多个新服分流。
作者 没有开花的树
所以,使用全服架构好处有:
1. 新服和老服共用机器,机器的利用效率很高,不担心老服玩家太少造成资源浪费
2. 有更多机器应对新服开服的流量高峰,支撑更大数量的同时在线玩家
3. 开新服和合服不用做数据迁移,可以减少这部分的时间开销
4. 更适用跨服玩法的实现

当然也有代价。
1. 多人玩法要区分所属服务器,排行榜也要区分
2. 不同区服之间想控制玩法的先后更新,需要额外写代码;分服架构通过代码分支不同,可以区分,但也会增大开发者负担,代码分支差异越大,功能合并越困难
3. 玩家在不同区服创建了账号,需要区分,合服也要特殊处理;分服架构只需要在合服时做处理
4. 扩容、缩容、数据备份更复杂

结论,分服架构还是全服架构,更像是一场博弈。

单点问题

游戏架构优化主要就是解决单点问题:单点过载和单点故障。如果没有要求,只要一个game server就好,支撑所有玩家。

因为单台机的硬件性能是有上限的,而且随着硬件的提升,价格涨的越多;再者,机子可能会发生故障,因此,需要多台机子备用。

所以,需要解决单点问题,主要有这几个:

1. lobby server单点

lobby server就要拆分为多个 server,玩家由负载均衡分到其中一个 server,同时,利用玩家id,确保玩家同时只在其中一个server。每个 server 都承担一定数量的玩家,超过设定的玩家需要排队等待空位进入,或者客户端重连,再由负载均衡进入其他 server。

既然提到负载均衡,就简单说下,如果自己搭设服务器,主要有两种方案,nginx 和 lvs,建议使用后者,可以支撑更大数量的并发连接。如果要支撑百万级别以上的并发,需要使用硬件负载均衡,如F5等。像我公司使用了腾讯云和阿里云的服务器,负载也使用他家的CLB和SLB,号称可以支持千万级并发。

以上,我们提到了单个负载是有并发上限的,所以可以设置多个负载,绑到同一个域名,通过dns负载均衡,将玩家分到不同的负载上。另外,业务上还可以在 server 拒绝玩家登录时返回一个可用的负载。
作者 没有开花的树

2. db 单点

db 要解决单点问题,有 2 种方案:
1、主从复制,读写都在主库完成,从库用于主库故障备用或者恢复
2、副本集,多个副本库,其中一个作为主库读写;主库挂掉后,从其他副本选出主库

以上,无论是1和2,还是无法解决主库单个点的并发压力。因此,在这个基础上,使用分片,根据玩家id不同,将不同玩家的请求分散到不同的分片上。

大表分小表
有些游戏还这样处理,对玩家id取模,将一些大表都拆分成固定的几个表,可以分在不同的数据库中。虽然能解决问题,但始终不是很好的办法,如果表还是太大了要继续拆分,维护的成本很高。

我也思考了这种方式下,怎么实现动态扩容。
1. 选择在线人数较少的时间点扩容;
2. 开始做数据迁移,设定扩容标记(表数量要成倍增加,分担一半的数据到新表);
3. 取得所有玩家id,对玩家id取模结果和原来不同时,则认为该玩家数据需要迁移;
4. 迁移时玩家一个一个处理,利用分布式锁,屏蔽这个玩家在数据迁移时登录,数据先复制到新表,再删除旧表记录;
5. 如果有玩家登录,先获取分布式锁,发现自己的数据需要迁移则先完成迁移(新号跳过);
6. 数据迁移结束后,取消扩容标记

一顿操作下来,我还是推荐使用成熟的数据库分片方案,别自己造轮子。

内存数据库
利用内存数据库 cache 缓存一部分数据,减少db操作。常用的内存数据库有 redis 和 memcached‌ 等,推荐前者,可以使用 redis cluster 来实现数据分片。

我之前项目使用 redis 缓存了玩家所有的数据,玩家读数据时先读 redis, 读不到再读 db, 读到后写到 redis,如果玩家有修改,则先后更新到 redis 和 db。

这是一种方法,但不推荐。

一褒一贬。好处是,缓存玩家所有的数据,主要作用在登录时能加速 load 数据,减缓停机开服时海量登录请求对 db 的冲击。坏处是,缓存过量的玩家数据,会导致 redis 内存占用过高,提高运营成本。玩家数据大部分只有自己读写,有很少一部分的公共数据才需要其他玩家看到,如排行榜显示玩家昵称、头像、等级、货币等,这部分才是最需要 redis 缓存的数据。而且玩家读到数据后,读写都在进程内完成,不会实时写到 db。

那么,做法是直接读写 db 吗?

是这样,但不完全对。
1、玩家数据可以分表,但不要分太多个表,不然登录读 db 请求次数会很多;单表的数据也不要过大
2、控制整个 server 并发 db 请求的数量,避免 db 压力过大引起整个集群的性能下降
3、单表字段过多时,考虑作为整体写入。字段过多时,如果不需要当做索引,可以打包成一个字段;另外,mongodb 写入表所有字段时 replace 优于 update
4、减少 db 写频率。玩家数据非实时写回,采用定时检查数据有变更才回写;数据字段可以冷热分离,将频繁变动的字段整合到一张表

作者 没有开花的树

3. 其他 server 单点

无状态 server
比如对外的http server如果并发请求量过大,也要解决单点问题,可以起多个http server,利用玩家id作为分布式锁,确保不会同时修改同一个玩家数据。

单例 server
比如排行榜 rank server,需要一个server承担计算,另外起若干个 rank server 以主从备用,通过竞争 master 来满足服务的高可用。

架构测试

这里讲架构正确性和稳定性的测试,分为常规测试和压力测试。

常规测试

单进程开发和多进程开发是有区别的,就算开发用的测试服,部署也要至少两个 lobby server 同时负载,以求更多次数的验证游戏业务在多 server下的准确性。

主要验证几个逻辑:
1、不同 server 顶号登录,上一次登录的数据是否写成功,还是说被还原了
2、全局数据确保不同 server 看到的结果是一样的
3、玩法进行过程中,不同 server 客户端看到是否一致,或者差异是否在可忽略的范围内

压力测试

压测是架构优化必不可少的环节,模拟真实玩家行为,在大规模并发请求时,考察业务的准确性和服务器的处理极限,为后续线上部署机器提供依据,另一方面,压测还可以检测出系统薄弱环节和缺陷,明确优化目标。
作者 没有开花的树
几个压测的点:
1、 模拟大量玩家在线,正常游戏随机行为,如部分玩家反复登录登出、部分新玩家建号、参与游戏主要玩法等,测试出单个 server 在指定机器配置下的承载上限
2、 所有功能模块通讯协议,记录协议qps
3、 全服通知,如全服邮件、全服奖励、全服广播、全服聊天等
4、 数值准确性,如不断加金币或减金币,验证加减法计算结果,测试超过数值上限以及数值溢出的情况,重点是压测http server改玩家数据以及qps
5、 数据容量,通过gm将玩家的所有数据列表塞满,验证功能是否有数量限制,如背包、邮箱、好友列表等等,验证全部塞满后是否影响登录
6、 分线测试,测试大量玩家同时涌入一个场景的情况,是否有分线处理,以及分线处理性能
7、 同屏测试,测试大量玩家移动和状态同步
8、 第三方组件,测试组件性能,是否会引起进程crash

架构扩容缩容

游戏DAU不是一成不变的,伴随着运营活动的推广和退出,呈现了起起落落的情况,所以,在控制成本的要求下,需要不定期对集群做扩容和缩容。

扩容的话,主要加机器,增加 lobby server 节点,然后通过调整负载权重,分配玩家到这个机器上;
缩容的话,先调整负载权重,不再分配玩家连接到这个机器,等待这台机器已有的玩家都登出后,回收机器。

除了 lobby server 外的其他 server 节点,无状态的 http server 可以采用差不多的做法,但有状态的 server 要业务上支持,已分配 server 的继续使用,新分配的使用一致性哈希或取模。

针对多机器部署,我们使用了 jenkins+ ansible 的方案,增加和减少机器,需要调整 ansible 配置。

对于 db 扩容和缩容,不推荐动态操作,可以选择在线人数少的时间点停机操作。mongodb 扩容的操作比较成熟,增加新的分片服务器,使用增加分片命令就可以扩容,mongodb 会自动迁移分片数据。稍后,通过 status 命令确认操作是否完成。
作者 没有开花的树

架构监控

监控的作用是,一方面给架构调整提供依据,另一方面,可以做预警,发现数据异常时,通知开发和运维人员,立刻处理线上问题,避免事态进一步扩大,造成更大的损失。

1、监控所有机器的硬件使用情况,比如cpu、内存、网络、硬盘等,以及数据库的使用情况
2、监控所有进程的log输出,比如进程log需要定时输出玩家在线数量,每个玩家的内存使用情况,每个协议的平均处理时间和次数,重要消息或 cmd 的平均处理时间和次数等

写在最后

没有架构可以一成不变,适用所有游戏,都是根据实际需要做调整,以较合理的成本实现高可用和高并发。最后,欢迎交流和指正。

发表评论

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