揭秘你所看不见的技术原理 - 游戏世界服

摘要


当前游戏世界万千不同,不同类型游戏各领风骚。单机巨作、即时战略(RTS)、多人在线战术竞技(MOBA)、第一人称射击(FPS),甚至简单休闲游戏都各自占领着特定的市场。但今天要写的并不是对游戏进行分类讨论,而是在网络游戏中会发现有一些游戏只需要输入账号密码登录后,并不需要选择所在的服务器,而且貌似全球玩家(如果排除欧服、亚服这类技术与法律瓶颈)都能任意组队游戏。那这些看似高大上的技术实现原理是什么呢?
本文主要是基于老东家研究的世界服架构,从业务的变化,架构的演变,优缺点以及所面临的问题等出发,非深入也非浅出地说明一下世界服的实现方法。

世界服的需求


世界服游戏的目的是让玩家感受不到服务器之间的隔阂,所有玩家都可以共同游戏。类似于欢乐斗地主没有房间的概念,所有玩家都在一个房间,可以任意开桌游戏。
当前有两种方法实现这种需求,一个是纵向升级,一个是横向升级。

  • 所谓的纵向升级,就是不断提升硬件水平,CPU足够强大,内存足够多,理论上一台服务器就能容纳上百万玩家同时在线。但这是理想状态,假如玩家再多一些怎么办?再升级硬件?再者玩家一多,所需要的带宽也增加,这种纵向升级是否能满足带宽要求,满足不同地域的玩家要求,满足不同运营商的玩家要求,我们直观上都已经认为是不实际的。即使在费用无限的情况下,几乎都无法满足所有需求

  • 所谓的横向升级,就是增加机器,让机器组成一个集群来完成本应该一台机器的任务。这样做的好处是方便后续升级,在当前机器容不下更多的玩家时,只需要多增加机器就好了。相比于纵向升级,横向升级突出了方便与价格优势。理论上可以无线扩展,即使全宇宙的玩家都想在一个世界玩,也可以通过无线机器扩展达到。但由于是服务器集群,如何保障集群的稳定性又是一个突出问题。

那横向扩张这么好,为什么市面上还有那么多游戏不做成世界服呢?其实不是不想做,上面的说法只是理论达到,实际中会有很多问题需要解决。这一次我主要是从负载均衡来讨论世界服。

世界服的实现


1. 传统游戏

传统的游戏是需要玩家选择自己的服务器。比如原先玩传奇时我是在七区,而魔兽世界时我又在四区耐奥祖服务器。这些都是由玩家来定的,玩家会依据游戏体验而自主选择所在的服务器。但依据我在游戏行业摸爬滚打发现,这样的选取很容易出现鬼服与卡服。某一些服务器会因为玩家过多而导致卡顿或者排队,而另外一些服务器则由于种种原因导致玩家数量稀少。在业务上,鬼服会让玩家失去游戏乐趣而流失;在技术上,一个能服务6000玩家的物理机,结果只服务了300人,这是极大的浪费。单服结构如下:
单台物理机服务器

2. 跨服游戏

随着技术的发展,玩家在游戏中不只是和相同服务器的玩家一起游戏,可能还会有一些来自其他服务器的玩家一起进行某一些玩法。类似魔兽世界推出的大战场,竞技场这些玩法,玩家是不知道一起玩的对手来自于哪个具体服务器,但是玩家都是在登录自己原服务器的前提下才能选择进行这类跨服玩法,在整体游戏结构上并没有太多的改进。跨服结构如下:
单服结构
我们发现物理机3是没有数据服务进程与数据库的,这是因为玩家是在他的宿主服登录(物理机1),在选择跨服玩法后,宿主服会将玩家的数据传递给物理机3,再让玩家登陆物理机3。因此实际跨服玩法服是不需要存储玩家数据,服务器的玩家数据都是来自于宿主服。整个流程如下:
跨服结构

3. 世界服游戏

既然有了玩家跨服游戏,那能不能让玩家没有服务器的概念,全服玩家都能玩游戏呢。既然有这个需求,那肯定有实现的办法。这里引入了微服务的思想,每一个服务提供一种功能,各个功能分散,这样能更好地实现世界服。类似dota2、lol、绝地求生等游戏,玩家除了选择世界上某一个大区外(由于网络、法律原因),不需要再选择具体服务器,只要在大区中的玩家都是随机匹配各自的队友或对手进行游戏。当然每一个公司世界服实现都有所不同,这里只是把我老东家当前的世界服架构展示一下:
世界服结构
世界服架构分为三层。

  • 第一层连接层。与玩家直连并进行直接通信的服务器。由于玩家的地域、运营商等不同,因此在连接层集群中,需要选择一台能为玩家提供最优服务的机器进行连接。

  • 第二层玩法层。玩家游戏对象所在的服务器,玩家直接进行游戏的服务器。玩家的各游戏操作都由这台服务器提供服务。

  • 第三层数据层。玩家数据存储所在的服务器,负责玩家数据的可持续化。在玩家下线、服务器宕机等情况下,大部分玩家的道具都能保持,数据层服务器功不可没。

连接层与玩家的网络连接是经过了外网,而连接层-玩法层-数据层则是由内网负责,这样最大程度保证了网络的顺畅,提高玩家的游戏体验。

世界服的技术点实现


我们从上面的图已经看到的世界服的实现方法。下面对世界服的每一层技术点进行一个讲解。

1. 连接层

从上面的图可以看出,连接层起到了承上启下的作用,对外为用户提供服务,对内需要与玩法服进行直连。因此连接层的网络相对复杂,连接数也多。
由于玩家具有地域、运营商属性,因此在玩家登录连接层时,需要游戏客户端与一台服务器分配一台最优的服务器为玩家提供服务。我们细化一下连接层结构如下:
连接层

  1. 在实际的生产环境中,我们将连接层的服务器接入不同的运营商,在玩家登录时,由客户端依据玩家所在的运营商以及地域寻找一台初始连接层服务器A进行登录。
  2. 玩家登陆后,服务器A会向连接层控制服通知玩家登陆,控制服依据特征值整合成函数 X = f(地域, 运营商, 各服务器当前负载),计算出X最优的连接层服务器B,并回传给服务器A,再回传给客户端,让客户端登录服务器B
  3. 客户端登录服务器B,玩家登陆连接层完成

这里我们看到,连接层控制服主要工作是保证各服务器负载均衡,同时又保证了玩家客户端所登录的服务器为最优解,保证玩家游戏体验。整个流程如下:
连接层登录流程

2. 玩法层

作为玩家直接游戏的服务器,服务器的稳定性与负载均衡就是一个非常重要的指标。若服务器经常宕机,我猜玩家对这个游戏也没有兴趣了,若服务器经常卡顿,掉线玩家也会觉得这个游戏太垃圾了。因此如何保证玩法服的稳定是本章节需要研究的内容。
首先将玩法层放大,如下图:
玩法层

  • 玩法服:玩家游戏对象所在的服务器,即玩家数据缓存体。在游戏过程中,对玩家数据的修改都是在玩法服进行的,等固定时间后,玩法服对象的数据会回传给数据层进行存储

  • 功能服:各功能提供者,例如工会、组队、战斗、交易等。作为微服务思想直接影响的架构,每一个功能服只提供单一的服务。若功能服突然宕机或者失去连接了,也只是影响了单独的功能,其他功能能正常进行。这样就能尽最大可能减少服务器故障对游戏的影响。

玩法服与功能服的联系,用我最喜欢的dota2来举一个例子。玩家在匹配对手时,其实他的游戏对象在玩法服(里面包含了玩家等级、积分、套装等),但是组队却在功能服-组队服中。组队服会将若干个玩家组成5v5后,将组队数据传递给功能服-战斗服中,玩家进入战斗服进行战斗。战斗胜利或失败后,数据会回传给玩法服,更新玩家的数据。整体流程如下:
匹配战斗流程

那玩法服与战斗服假若突然增加玩家过多,如果让服务器不至于卡顿呢?这里要做的就是尽量不要让玩家聚集在单独的服务器中。

  • 玩法服负载均衡:玩法服是最容易出现负载不均衡问题的服务器,由于有多少个玩家,就必须在玩法服创建多少个游戏对象,有可能玩家掉线时玩家游戏对象也不能实时卸载,因此游戏对象的个数肯定对过真实玩家个数。这么多玩家个数聚在一起多机器的压力还是比较大的。为了让玩家均匀分布在玩法服集群中,这里使用了一致性hash算法。至于一致性hash会在最后一章单独列出来讲解。我们通过遍历每一个玩法服玩家个数来控制每一个服务器在一致性hash中的节点数。玩家数量越高节点数越少。

  • 战斗服:由于战斗发生的时间有限,战斗并不存在于整个玩家的本次游戏生命周期中,因此战斗服不需要一致性hash来进行(一致性hash需要遍历每台服务器给中心服,需要多余的消耗与硬件开销)。而是使用了轮询机制,即组队服每匹配成功一次战斗,就会在战斗服集群中按顺序一个一个分配到相应的战斗服中。

解决了玩法服与战斗服的负载与稳定性问题,玩法层的技术点也就被攻破了。

3. 数据层

作为玩家数据存储的服务器,玩家数据是只增不减的。即使玩家删号了,数据记录也只是修改标记位而不是从数据库中直接删除。同时玩家上线时数据服会创建一个数据对象与其对应。而玩家下线后,数据对象会在数据存储成功后被释放。因此数据层的技术点有两个:负载均衡与数据稳定性。为了解决负载均衡,我们将数据层进行放大:
这里写图片描述

  • 数据稳定性:对于底层使用的数据库,需要选择稳定性高的。例如mysql、mongodb等数据库。除了数据库保证外,我们也要保证玩法服传回的数据能正常地存储在数据服,同时又不能让玩法服每一次传数据都写数据库,这样会造成数据库卡顿排队现象。为了解决这个问题我们创建了数据对象,玩家的数据从玩法服传递到数据服时,并不是立刻写入数据库,而是先缓存在数据对象中。数据对象会将数据定期写入数据库,这样减少了数据库读写频率。同时数据库更新与数据对象更新的频率是很低的。稳定性能在上线后的较短时间就得到很好地保证。游戏维护大多修改玩法内容,因此数据层与玩法层分开也增加了系统稳定性。

  • 负载均衡:玩家注册角色时,系统会分配给玩家id,同时指定一个数据服为他的数据存储服。一旦指定则不再改变,也就是游戏的生命周期中,除了公司对服务器进行合并或拆分等人为因素外,正常情况下玩家的数据所在的服务器是不会改变的。类似我们有出生地,出生地确定后我们的身份证号也就确定了。因此玩家在注册角色时,系统通过数据层控制服依据公式X = f(总数据数量, 活跃数据数量)中,最大的X值服务器作为玩家的出生地,并依据出生地分配ID。后续玩家登录时,只需要通过ID就能找到相应的数据服读取数据并加载到玩法服。
    我们发现公式为X = f(总数据数量, 活跃数据数量)。具体公式可以使用一致性hash,也可以使用自定义。只是说需要参考特征值总数据数量与活跃数据数量。为何要判断活跃数据量,因为玩家登陆就需要创建数据对象,若活跃数据量过多,数据对象也就或更多,占用的内存会更多,数据库的读写操作就会更频繁,同样会造成机器卡顿。

世界服几个例子


下面我们用玩家第一次登陆与普通玩家登陆操作来整合架构思路。

1. 玩家第一次登陆

我们会发现现在的游戏登陆总会进行读条,这读条背后有客户端需要加载资源(游戏画面越好,加载速度越慢)外,其实后台也在处理着大量的请求。现在我们就来揭秘其中的奥秘。整体如下图:
这里写图片描述
看上去是不是相当复杂,其实我们把流程梳理一遍就容易了。

1. 连接层登录阶段:
- 客户端选择连接服1进行登录;
- 连接符请求控制服进行调度;
- 控制服通过特征值运算,选择一个最佳的连接符,并将调度回传;
- 连接服1通知客户端,连接服2才是你的最优解
- 客户端断开与连接服1的连接,并重新登录连接服2

2. 玩法层登录阶段:
- 连接服2请求玩法层控制服进行调度,选择最佳玩法服
- 通过一致性hash算法,玩法层控制服选择最佳玩法服,并将调度信息通知连接服2;
- 连接服2向玩法服2发起登录,玩法服2创建虚拟玩家对象;

3. 数据层登录阶段:
- 玩法服2请求数据层控制服进行调度,选择最佳的数据服
- 通过公式X = f(总数据数量, 活跃数据数量)选择最佳数据服2,并将调度信息通知玩法服2
- 玩法服2向数据服2发起创建角色请求,数据服2创建角色
- 数据服2加载玩家数据,并将角色数据回传给玩法服2
- 玩法服2依据玩家数据创建角色

整个登录过程就完成了。这时候三个服务器都已经打通,玩家可以愉快地玩游戏了,进度条也会飞快地刷到底然后进入游戏。

B. 普通玩家登陆

有了游戏角色,那玩家在其他登录时读取进度条时会发生什么呢?我们再来揭秘一下整个过程,如下图:
这里写图片描述
在这里连接层与玩法层登录都是一模一样的。区别在于数据层登录

  • 玩法服2通过映射,找到玩家id
  • 通过玩家id,找到玩家所在的数据层服务器,并向数据服2获取数据
  • 数据服2回传角色数据

整个的登录过程也就结束了。

5. 一致性hash的简单介绍


负载均衡大量使用一致性hash方法。一致性hash的原理是将目标组定义为节点,组成一个环,如下图:
这里写图片描述
将每一个玩法服均匀地分布在环上面,当然每一个玩法服分布的数量可以不同,但也是均匀分布。分布的数量越多,选中的概率也就越大。
将一定的数值进行hash运算(一般使用当前时间),获得的hash落点如下:
这里写图片描述
此时顺时针旋转,我们选中的服务器则为玩法服1。当玩法服1玩家人数增加时,玩法服1在环中的节点数就减少,若此时顺时针玩法服1已经删除,则顺时针继续旋转得到玩法服2作为登录服务器。
这就是简单的一致性hash原理

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值