游戏服务端的高并发和高可用


游戏服务端的高并发和高可用
文章图片
文/水风本文首发知乎
https://zhuanlan.zhihu.com/p/342953318
用通俗的方法来描述一个好的服务端架构 , 最基础也是最重要的就两点:支持百万玩家同时在线 , 不出问题 。 这两点也就分别对应了高并发和高可用 。
上篇文章介绍的是游戏服务器的一些优化方法:《某百万DAU游戏的服务端优化工作》 。
这篇文章系统的介绍游戏服务端中的高并发和高可用 。
高并发和高可用是一个相辅相成的工作 , 当我们支持百万玩家同时在线时却无法保证服务器稳定可用 , 那高并发支持就无从谈起;而如果当玩家数量较多时服务器就常常出问题 , 那也不能称为高可用 。
1水平扩展
水平扩展时高并发和高可用的基础 , 通过支持水平扩展 , 我们理论上可以通过增加机器获得无限的承载上限 , 从而支持高并发;在此基础上 , 若某个进程出现异常 , 其他进程可以替代其提供服务 , 从而实现了高可用 。
以下图为例 , 对于不支持水平扩展的架构 , 游戏服务器中只有一个战斗进程为所有的玩家提供战斗服务 , 这里存在两个问题:1.一个进程最多只能使用一个一台机器的计算资源 , 存在性能上限 。 2.若这个进程或者所在机器/网络发生异常 , 那么整个系统就不可用了 。

游戏服务端的高并发和高可用
文章图片
不支持水平扩展
水平扩展有两种常见的实现模型:
大厅服和所有的战斗进程进行全连接 , 需要访问战斗服务时去管理器中查询服务所在的进程地址 , 然后直接去访问进程 。 (左图)在战斗进程前面挂一个路由 , 路由记录每个战斗所在的战斗进程 , 相关请求会转发到对应的进程 。 (右图)
游戏服务端的高并发和高可用
文章图片
水平扩展的两种模型
1.1有状态vs.无状态
从进程内存中是否保存状态的角度可以将服务分为有状态和无状态:
有状态服务:进程内存中保存状态 , 比如战斗服务将战斗信息(玩家角色状态、小怪状态等)保存在内存中 , 玩家操作或者战斗逻辑会改变战斗信息 。 由于游戏中状态比较复杂 , 业务改变状态频率比较高 , 游戏大部分的业务都是用有状态服务的方式提供 。 无状态服务:服务只处理流程 , 不保存数据 , 一般数据会保存到后端db中 , 这种服务的逻辑一般会有很多db操作 。 这种类型的服务在互联网web行业用的比较多 , 游戏中常见的比如充值、登陆等 。 (无状态服务在执行一个流程处理中可能会有一些临时变量申请内存)无状态服务本身不保存状态 , 所以进程crash也不会丢失信息 。 此外 , 下文将介绍 , 由于使用随机分配的路由方式 , 无状态服务对异常的容忍更加好 , 所以 , 从高可用角度 , 无状态更加好 。
但是 , 由于无状态不保存状态 , 所有状态操作都是数据库操作 , 就造成了开发成本更高(代码写起来更复杂)、数据库压力更大 , 所以无状态并不适合所有服务 , 一般对于状态简单明确的服务 , 可以优先使用无状态 , 比如好友服务 。
1.2路由策略
对于有状态和无状态服务 , 他们使用的路由方式也不同 。
对于无状态服务 , 一般使用随机分配的路由方式 。 随机分配的路由方式有很大的好处 , 如果某个进程crash了或者网络出现了故障 , 我们只需要把这个进程从路由中去掉 , 对后续的请求不会有影响 , 只会影响此进程当前正在处理的逻辑 。
有状态服务的路由需要明确每个请求给哪个进程处理 , 给其他进程其他进程因为没有相关状态信息也无法处理 。 比如上文提到的战斗服务 , 路由根据战斗ID将相关请求发给对应战斗所在的进程中才能处理 。 路由一般使用取模或者一致性哈希 , 一般会优先使用一致性哈希而不是取模 , 防止故障引起抖动 。
1.3举个栗子
下图是某游戏架构的简化版模型 , 真实的服务器比这个复杂很多 , 这里主要是为了举个例子 。

游戏服务端的高并发和高可用
文章图片
集群可以分为三类:支持水平扩展的有状态服务、支持水平扩展的无状态服务、不支持水平扩展的单点服务 。
其中 , 支持水平扩展的有状态服务和无状态服务的进程数量能占到项目的90% , 单点服务很少 。 在这种架构情况下 , 我们游戏的承载上限瓶颈在于单点服务 , 而单点服务逻辑相对比较简单 , 承载上限很高 。 此外 , 支持水平扩展的服务进程出现异常只会影响此进程所服务的玩家 , 具有较高的可用性 。