综述
早期互联网基础硬件设施与网络软件技术不完善,存在数据存储量少,传输速度慢等特点,在加上早期显示技术只支持文字显示,所以早期网络游戏是一些交互性弱,数据量少的文字交互游戏。但随着图形界面技术和互联网技术的发展,网络数据存储量和传输量增大,出现了多种多样的游戏,相应的服务器技术也越来越复杂。
本篇文章主要分为两个部分,第一部分是服务器历史和现在,这部分主要是房间型服务器架构的由来,主要目标是让大家知道现代服务器架构各个组件的由来以及作用,知道服务器怎么从之前的单服务器架构发展成现在复杂的多类型服务器架构,发展图如下。
第二部分是服务器的未来,这部分的主要内容是游戏如何上云、什么是云游戏以及云游戏工作原理。从服务器历史发展可以更好的理解现在服务器架构组成,同时也可以看出游戏服务器未来发展趋势。服务器发展趋势主要取决于玩家游戏需求以及上下游技术产业链。目前来看:游戏内容的越来越精细,互联网传输量的越来越大以及云计算的发展壮大,个人认为未来服务器发展是游戏上云以及云游戏。
单服务器进程
1978年,英国的一名学生编写了世界上第一个MUD程序《MUD1》,MUD1主要采用文字叙述方式,进入游戏后服务器通过文字告诉你在什么场景,你有什么数据等等交互,比如你输入往上走,程序会告诉你爬到了半山腰,如下图。
1980年《MUD1》程序接入ARPANET并且程序的源码也共享到了ARPANET,此后《MUD1》出现了众多改编版本,不断完善的《MUD1》也产生了开源的MUDOS。MUDOS采用单线程无阻塞套接字来服务所有玩家,所有玩家的逻辑请求都发到同一个线程去处理,主线程每隔一秒钟更新一次所有对象,玩家数据直接存储到本地磁盘。
多服务器进程
2000左右,网络游戏已经从文字MUD进入了全面图形化年代。游戏内容的越来越丰富,游戏数据量也越来越大,早期MUDOS的架构变得越来越吃不消了,各种负载问题慢慢浮上水面,传统的单服务器结构成为了瓶颈。因此将服务器拆分成多个游戏逻辑服务器(Game),将玩家分摊到各个游戏进程中,每个游戏逻辑服务器负责一定数量玩家的服务,如下图。
多类型服务器进程
早先参与网络游戏玩家较少,游戏内容也比较简单,所有的游戏内容逻辑都在同一个服务器进程中运行。随着图形化技术的发展,网络游戏玩家越来越多,游戏内容越来越丰富,越来越复杂,导致游戏数据也越来越大,因此出现不同类型的服务器进程来服务不同需求。
数据库的拆分
服务器的一个重要功能是存储玩家的游戏数据,以便下次玩家上线时能读取先前的游戏数据继续游戏。早前文字MUD游戏的数据持久化存储使用的是服务器本地文件存储,玩家下线时或者定时将数据存储在EXT磁盘中,这样的存储在逻辑上是没有问题的,但是玩家频繁的上下线会导致服务器频繁的I/O,导致负载越来越大,以及EXT磁盘分区比较脆弱,稍微停电容易发生数据丢失,因此第一步就是拆分文件存储到数据库(Mysql)去,如下图。
- 数据库代理进程(DB)
将数据库拆分出来后,游戏逻辑与数据存储分离,减少了系统的耦合性。如果各个游戏逻辑服务器(Game)需要取数据的话,可以直接连接数据库获取数据,但是当有大量玩家同时操作时,多个游戏服务器同时存取数据库,出现大量数据重复访问和数据交换,使得数据库成为瓶颈。于是在游戏逻辑服务器(Game)和数据库之间增加一个数据代理进程(DB),游戏服务器不在直接访问数据库而是访问数据代理进程,数据代理进程同时提供内存的数据缓存,缓存一些热点数据,减少数据库的I/O。如下图。
- 内存数据库
互联网技术的开源化,极大的促进了各类专用软件的发展和完善,开源技术出现了一种专用的内存数据库,这类专用内存数据库提供易用方便的存储结构体,而且拥有很高读取性能,因此游戏服务器引进这类专用内存数据库,用于缓存服务器热点数据。目前内存数据库使用比较广泛的是Redis和Memcached。引入内存数据库之后,数据库代理进程(DB)不在缓存数据,而是变为玩家上线时将玩家数据从Mysql数据库中加载到内存数据库中和定时将内存数据库中的数据存储到Mysql数据库中。
逻辑进程的拆分
上面的结构并没有持续太长时间,由于游戏功能类型的逐渐丰富,为了提升游戏各功能运行速度以及游戏体验,开始出现多种专用服务器类型,比如聊天服务器(IM),匹配服务器(Match)和排行榜服务器(Rank)等,将这些专用服务器独立出来有几个好处:
- 降低耦合性:如果所有逻辑在一个进程中运行,某个游戏功能出现故障的话,将会影响到游戏的所有功能,导致游戏不能继续下去。比如将聊天拆成单独的服务器,聊天服务器挂了的话,只会影响聊天,不会影响你的大厅逻辑、场景逻辑和其他逻辑。
- 分摊压力:像匹配这种功能通常需要大量的玩家放到一个池子里,然后从中选出几个能力值相近的玩家进行对战,这其中涉及大量的排序和比较运算,会导致CPU占用大,从而阻塞其他逻辑的处理。所以一般都会将匹配这样的功能拆成匹配服务器(Match)。
- 保证数据一致性:典型的就是组队这种,队伍由多个人组合到一起,每个玩家由于压力的负载可能被分配到不同的逻辑服务器(GameServer)中,如果在各自逻辑服务器中维护一个队伍数据,会存在数据不一致的情况,比较难处理,所以较好的方式是拆分队伍功能成组队服务器(Team),将队伍数据放到一个地方去。
拆分成不同服务器类型后,架构图如下:
拆分成各个不同的服务器之后,同一个玩家客户端可能出现与多个服务器同时进行交互,比如你组队时匹配或者匹配时聊天等,而且有些状态频繁变更,比如玩家频繁匹配和取消匹配。如果每个具体功能客户端都要连接一个对应服务器的话,连接的变更和数据的通信会变的非常麻烦,而且中间状态容易出错,所以开始拆分出一个网关服务器(Gateway)。
把客户端和服务器之间的网络功能单独提取出来,让客户端统一去连接网关服务器,客户端发送的数据由网关服务器转发到后端各个类型的游戏服务器。而各个游戏服务器之间的数据交换也统一到网关服务器进行交换。
消息队列
上面类型的服务器基本能稳定的为玩家提供游戏服务,但是扩展性非常的差,每个网关服务器(Gateway)要跟所有类型的所有逻辑服务器维持连接,一些大型游戏这样的逻辑服务都可能有好几千个,如果每个都维护一个连接的话,光维护这些连接就会消耗大量的内存和运算,而且每新增一个逻辑服务器都要跟所有网关服务器连接,扩展性很差。所以服务器之间的通信通常都会增加一个消息队列进程,专门用来服务器之间内部转发消息。
- 全局服务管理器(Master)
老版的服务器通常会有一类全局服务管理器(Master),全局服务管理器全服只有一个,会存放全服共享或者唯一的数据,早期的数据缓存功能也是由全局服务管理器来实现的,通常全局服务管理器还会存储和管理所有服务器的信息,比如服务器的IP地址、服务器的容量和服务器的增删等,还有就是全局服务管理器(Master)需要与每个服务器进行连接,用心跳维护服务器是否处在可用状态,所以服务器之间的消息转发由它来承担,全局服务管理器(Master)扮演着消息队列的角色。
- 分布式消息队列
一些分区分服的服务器架构,比如分上海服、北京服和杭州服等,一个分服同时在线人数可能不超过几万人,这样服务器架构差不多够用了。现在基本上很多大型游戏也使用这样的结构,但是对于全国同服或者全世界同服的大服务器结构下,这样的架构存在一个严重的单点问题,因为所有服务器间的消息都要通过这个全局服务管理器(Master)转发,这个服务管理器的压力非常大,只要它崩掉的话,那整个游戏都跑不起来,所以会将单点的消息队列换成分布式消息队列,以减轻单个消息队列的压力。现在外界使用比较广泛的分布式消息队列有kafka、rabbitmq和nats,这些都是工业级的消息队列,功能很完善,但游戏界为了性能和自定义化一些功能,通常是自己写分布式消息队列,比如将Master换成分布式Master。
消息队列一般使用订阅/发布(sub/pub)模式,每个服务器向消息队列订阅自己要监听的主题(Topic),如果自己有消息发布(pub)的的话,就向消息队列发布,不用管向哪个服务器发布。自己订阅自己需要的,自己发布自己要发送的,很好的解耦了服务器间的耦合。
服务发现
消息队列由单点转换成分布式,消息队列也会从服务管理器中脱离出来,专心做消息转发的功能,那服务管理器中剩下的功能怎么办?剩下的其实就是服务发现的功能:存储各个服务器信息(IP地址,容量,服务器ID,服务器类型等)和管理服务器的增删等功能。
怎么理解服务发现的功能?我举一个实际例子。假设你的游戏已经上线,服务器也正在运行中,到了某个节日,游戏做了一个活动,这个活动做的太好了,吸引了一大批玩家,大量的玩家导致你原有的服务器撑不住,需要在线加服务器,这时候就会出现几个问题:原有的服务器怎么才能知道新加了服务器?新加的服务器怎么知道老服务器信息?服务发现就是要解决这样的问题。
新加的服务器A向服务发现中心注册自己的信息,信息注册后,服务发现中心会将服务器A的信息广播给所有老的服务器,通知老服务器有新的服务器加进来,同时也会向新服务器A发送所有老服务器的信息。
服务发现本质上是一类数据库,这类数据库为了保证数据的高可用会支持容错性。容错性一般由数据库备份保证,也就是一份数据会保存在多个机器上,保证了容错性又会带来一个新的问题,那就是多个机器上的数据是否一致,业务逻辑应该用哪个机器上的数据,数据一致性由一类共识性算法解决,比较出名的是paxos算法及其简化版raft算法,这类算法比较复杂,这里不细说,这两类算法的工业级软件实现是zookeeper和etcd。
负载均衡
**什么是负载均衡?为什么要负载均衡?**每个服务器由一台计算机组成,单个计算机只有有限计算资源(CPU、内存和网络带宽),当大量玩家都往一个服务器中发送请求时,服务器因为计算不过来,大量请求数据积累超过内存,这样会直接导致计算机崩溃。虽然可以将服务器换成性能更好的服务器,但单个服务器性能受硬件的限制,计算的有限性很低,而且动态扩展性不强,比如线上要临时加计算资源时,不可能直接关服务器吧。所以一般都是通过软件方法,将输入的网络流量分配到多个计算机中,以平摊计算压力,这个软件方法就是负载均衡,总的来说负载均衡是高效的分配输入的网络流量到一组后端服务器。
一个游戏会有多个阶段需要用到负载均衡,这些阶段都有个共同点就是涉及到服务器的选择和变更,比如登录时要选择哪个服务器登录,RPG场景切换时服务器的切换,匹配成功进行战斗时等等,这些阶段都会涉及到负载均衡。负载均衡针对不同需求场景有不同的负载均衡方法,这里主要写登录服(Login)和网关服(Gateway)的负载均衡。
登录服务器的负载均衡
登录服务器是在线游戏的入口,没有登录服的话,你的游戏客户端进不去游戏,卡在登录页面。登录服的负载均衡方法大概可以分为两种,这两种方法与服务器的架构相关联的,第一类是分区分服的负载均衡,第二类是大服务器下的负载均衡方法,大服务器和分区服服务器主要的区别是:大服务器下所有人(全国或者全世界的人)可以一起比赛,不会出现同一个账号有不同数据,而分区分服的服务器是不同的登录服你的角色数据是不同的,比如你在北京服是100级,那你选择杭州服后是1级。分区分服和大服务器架构最本质的区别是是否公用了数据库。
- 分区分服
这类服务器会显示的让玩家选择在哪个服务器登录,比如在杭州服登录,比较典型的像王者荣耀这种就是使用这种方式,你进入游戏时会推荐几个空闲的服务器给你登录,如果选择已经爆满的服务器登录,会提示你服务器爆满,不能登录,这类负载均衡的方式是由玩家来决定的。
- 大服务器架构
大服务器架构下所有人都可以在一起比赛,在登录时也不会让你选服,只有一个登陆按钮,典型的像吃鸡这种游戏就是使用这种架构。那一个登录按钮是否意味着只有一个登录服务器?所有人都连一个服务器不会爆炸吗?其实登录服还是有多个的,只不过在登陆时客户端登录时程序会自动的帮你选择在哪个登录服登录,登录进去后,所有人共用同一组后端服务器和同一组数据库,所以全部人可以一起比赛。这类负载均衡方式是由客户端计算哪个服务器,计算的方法有很多,比较简单直接的就是在登录服中随机选择一个。
网关服务器的负载均衡
Login服务器是在线游戏的入口,通常为了更好动态的扩展服务器、减少向外界暴露内部服务器的入口以及承担更多人的登录逻辑,登录服务器所处理的逻辑很少,基本上是账号的验证、角色的创建和角色数据的加载,等账号验证完毕后,登录服根据负载均衡算法给玩家分配一个网关服务器,之后游戏的逻辑消息由网关服务器处理和转发。网关服务器的负载均衡方式通常是最小分配,也就是在一定阈值之上从现有的网关服务器中选择负载最小的一个。
登录成功后,客户端会和登录服务器断开,以减少登录服务器压力以供其他人登录。
战斗服务器
上面的服务器基本上是一个大型房间类游戏服务器的完整结构了,网关服务器用来转发消息,各类内部服务器用来处理各个游戏功能的逻辑。但是所有类型的游戏功能都要经过网关服务器来转发到后端服务器吗?比如游戏的实时战斗功能,像体育竞技,吃鸡,王者荣耀的战斗。如果这类对于实时性要求非常高的战斗功能也经过网关服务器的转发的话,产生的延时是非常高的,你释放一个技能,对面半天才掉血,这非常影响战斗体验。所以一般会有一类战斗服务器(Battle),客户端直接连接战斗服务器(Battle),不用网关服务器的转发。
分布式存储
由于大型服务器数据量比较大,最终的DB和Redis也会换成分布式数据库。
总结
上面主要是房间型服务器架构的发展过程,这个过程阐述了游戏服务器怎么从单服务器结构发展成现在的多类型服务器结构。比房间型服务器架构复杂的是MMORPG,这类游戏通常还会有一类场景服务器,专门用来处理游戏世界。虽然MMORPG服务器比房间型服务器要更复杂,但是各类服务器出现的原因是差不多的,只要懂了房间型服务器,其他服务器能很快理解。
Wind
Wind是一款面向云的高性能、高效率以及高扩展性的大型分布式游戏服务器引擎。Wind利用Python语言的简洁语法以及丰富的生态库来提高游戏业务的开发效率,针对一些对性能有要求的游戏业务功能(如实时战斗功能),Wind利用Golang的高并发特性来保证服务的高性能,同时Wind接入云的组件来保证游戏服务的动态扩展性,提高服务资源的利用率。
Wind是游戏服务器界首次结合go与python优点的服务器,如果Wind能解决你的问题的话,希望能帮Wind点个Star,如果不能解决大家的问题的话,也欢迎大家提Issue和Request,我会持续开发和完善Wind。