Sergey Ignatchenko针对大规模多人网络游戏所撰写的开发与部署一书章章精彩,不过里面的内容远不仅适用于游戏。下面是这本书的最近一章:“关于服务器端架构、前端服务器与客户端随机负载平衡”问题。

  在前端服务器输入

  1. [Enter Juliet]
  2. Hamlet:
  3. Thou art as sweet as the sum of the sum of Romeo and his horse and his black cat! Speak thy mind!
  4. [Exit Juliet]


  这是用莎士比亚编程语言所编写的一段程序样例。

  我们的经典部署架构(尤其在使用FSM时)并不差,运行良好,但对于大多游戏来说仍有很大的改进空间。更具体来讲,我们可以在游戏服务器前面再另加一行服务器,如下图:

14025589kjmwubj98bswbb.jpg
  如图所示,与经典部署架构相比,只是在游戏服务器前面增加了一行前端服务器。这些新增的前端服务器旨在处理所有来自客户端的通讯:所有“那个玩家有没有连接上”这种讨厌的问题,所有客户端到服务器的数据加密(如适用),所有这些密码等等,所有相对奇怪的reliable-UDP协议(如适用),当然还有客户端与不同游戏服务器之间的路径选择信息——这里处理着所有客户端的通讯。

  此外,一般来说这些前端服务器在需要时会存储相关游戏世界的副本,并在游戏世界升级时扮演集中器(concentrator)的角色。也就是说:即便游戏服务器上有10万人同时观看某个比赛(比如某个比赛的决赛之类),只需要将更新发给几个前端服务器,而前端服务器会解决10万人的数据分配问题。在大型决赛中,当有成千上万人想要观看比赛时,这种能力非常方便。实际上无需将其制成直播视频——对现有的玩家来说并不方便,也贵得要命,但在客户端里是可以实现的。

  我们稍候会对前端服务器的实现进行一些讨论,不过现在先讨论最重要的部分:

  前端服务器必须能够在不对玩家造成重大不便的情况下就能简单更换。

  也就是说,如果任何前端服务器因某种原因发生故障,玩家最多能看到几秒钟的断开。虽然仍会造成混乱,但比这样的场景——“整个游戏世界宕机,我们需要从备份恢复”好多了。换句话说,无论何时前端服务器因某种原因而崩溃(更糟的情况下甚至产生“黑洞”),所有连接的客户端都会检测到这一情况,并自动重连到其他前端服务器上;在这种情况下,所有玩家能看到的只是瞬间断开(也很讨厌,但比游戏挂掉好太多)。

  前端服务器:好处

  每当我们增加复杂层级的同时,总会产生这样一个问题“我们真的需要它吗”?从我看到的情况,将可轻松替换的前端服务器放置在游戏服务器的前面很有价值,有着诸多好处。具体来说:

  ● 前端服务器从游戏服务器上分担了一些负载,并且很容易替换。

    ● 也就是说游戏服务器的数量可以减少了。

    ● 再加上前端服务器容易替换的特性,站点整体的可靠性得以增加;部分游戏世界服务器宕机的情况将越来越少。

  ● 前端服务器可以使用更廉价的那些,严格说来甚至无需使用ECC和RAID,但游戏服务器当然得使用这种的。如上所述,前端服务器很容易替换,因此如果发生故障,其复杂可以自动重新分配到其他前端服务器上。如果想要部署在云端,你可能会想给前端服务器考虑更廉价的配置(即便是来自不同的CSP[1])。

  ● 允许客户端有指向全站的唯一连接;这样做的好处包括:更好地控制与玩家终端的连接,从而可以控制来自不同数据流的优先级,消除那些难以分析的“部分连接”,并更好地隐藏站点的实现细节。

  ● 允许简单的客户端负载平衡(无需硬件负载均衡器等等)。想要了解更多信息,参见下面的“客户端负载均衡与大数字法则”部分。

  ● 在前端服务器上保存相关游戏世界的副本使得想要观看站点上某些比赛(比如大决赛之类的[2])的用户数量近乎无限;最重要的是,还不影响游戏服务器的性能! 此外通常无需为决赛组织任何工作,只要正确构建,系统便会按以下方式自主处理:

    ● 每当有人来观看某场游戏,客户端会向前端服务器发出请求;
    ● 如果前端服务器没有所请求游戏的副本,就会向相关的游戏服务器发出请求,同时更新到游戏世界状态;

    ● 从这时起,前端服务器会保持游戏世界的副本“同步”,向所有请求的客户端发送该副本(与更新内容);

    ● 也就是说从这时起,即便有10万个用户在观看该游戏服务器上的某场游戏,所有的额外负载都由前端服务器处理,不会影响到游戏服务器;

  ● 前端服务器允许稍后增加安全性(基本能够担任DMZ的角色)。

注1:要记得你仍需保持一流的连通性;
注2:由于大决赛是吸引注意力很好的方式,这种做法明显能够提供竞争力。

  前端服务器:服务器延迟与玩家间延迟的差异

  关于设置前端服务器的负面影响,我只能想到这两个。第一个是由前端服务器所带来的额外延迟。具体来讲,我们是在讨论从应用层面的客户端所发送的数据包,经前端服务器处理,进入前端服务器的TCP协议栈[3],再离开游戏服务器的TCP协议栈,到达游戏服务器应用层面所需的时间(再加上反向重复这一步骤所需的时间)。

  让我们查看一下这个额外生成的延迟。从我的经验来讲,如果所使用的通讯层数据库相当优秀,应用层面的前端服务器的处理时间会在几微秒[4]。然后从前端服务器到游戏服务器会建立点对点TCP连接,这类连接的延迟(通过10G以太网)在8µs左右[Larsen2007]。将这两个延迟的时间加在一起,并乘以2得到往返延时(RTT),这个数字显示延时仍远远低于100µs。然而,还有进一步的原因(诸如延时开关,不同OS之间的差异,游戏之间的差异等)让人无法确认延时会低于100µs——也就是说,也许能达到,也许不能。另一方面,如果实现时足够仔细,前端服务器所带来的延迟会相应减少,在大多情况下可减少到1ms。

  总结:

  ● 如果可以忍受1毫秒左右的额外延迟,就别再担心额外延迟的问题了,启用前端服务器吧;包括所有类型的游戏,唯一可能的例外是大型多人在线射击游戏(MMOFPS)。

  ● 如果可以忍受的延迟远低于1毫秒的话(难以想象,因为这个数字比1/60秒/帧的更新时间还要低一个数量级,不过在MMOFPS世界里什么都有可能)——就要多考虑一下了,尝试在实践中找出延迟种类;如果经过实验,确实无法接受所带来的延时[5],就可能需要放弃前端服务器了。

  ● 具体情况可能有所不同,无法作出保证,不包括电池。

  如果某些前端服务器过载,或运行的硬件差异很大,那么使用前端服务器的第二个(依我来看相当理论化,不过一般要看具体情况)潜在问题就会产生,那些连接到负载较低的前端服务器的玩家延迟也较低,从而在比赛中更有优势。

  一方面来说,在真实世界的部署中,我并未发现有实际区别——也就是说,按我的经验如果某些前端服务器过载,这意味着大多数已经达到90%以上的负荷,这是应当避免的。另一方面,无论根据实际情况还是理论上,你都可能受到这种影响,尽管除了MMOFPS我尚未看到现实案例。

  如果这类玩家之间的延迟差异成为案例(只有成为真正的问题时),对于与某些前端服务器相连接的某些游戏世界的玩家来说,可能需要实现某类关联(更多相关信息请参见“关联”部分)。然而,大规模关联可能会抵消前端服务器所带来的多数好处,因此如果需要在所有游戏中执行关联的话,不架设前端服务器也许更好一些;只在游戏的一小部分中实现关联(比如“高规格比赛”)带来的麻烦会更少一些,请查看后面的“关联”部分)。

注3:没错,在多数情况下我主张在服务器间通讯中使用TCP连接,参见上面的“服务器间通讯”章节。另一方面,UDP也是可以使用的。
注4:注意这可能成为非普通情况,此外我已经实现了。
注5:理论上你可能还想实验一下无线带宽技术什么的——完全适用于FSM架构,因为其中的通讯与其他部分的代码相分离,但一般来说不值得尝试。

  客户端随机平衡与大数字法则

  一旦设置了前端服务器,就会产生问题“如何确保所有前端服务器保持同等负载”,即典型的负载均衡问题。一般来讲,在至少近20年间负载均衡都是个很大的话题。从中引申出的三种常见技术分别是:DNS负载均衡、客户端随机平衡还有服务器端(一般是基于硬件的)负载均衡。而针对最后一种,随着制造硬件设备技术改进,毫无疑问至少在企业中普及度正在增长。我们来深入探究一下这些负载均衡的解决方案。

  DNS负载均衡

  DNS负载均衡是基于传统DNS请求的。每当客户端请求站点前端地址(以IP地址显示)时,就会向相应的DNS服务器发送请求。如果按DNS负载均衡来配置DNS服务器的话,就会向不同的DNS请求来源返回不同的IP地址,因按循环轮流(round-robin)的模式[6]而得名。

  在不同的web服务器使用均衡浏览器时,DNS负载均衡有两个主要的缺点。首先将DNS服务器与请求路径缓存在一起会有问题(这是DNS处理的标准做法)。也就是说,即便服务器按均衡模式忠实返回所有的IP,其中一个返回的IP可能被Big Fat DNS服务器缓存(试想Comcast或ATT),并分配给成千上万个客户端;在这种情况下,被缓存的“幸运”IP可能获得倾斜。对网络服务器使用DNS均衡负载的第二个问题在于,如果其中一个服务器宕机,普通的网络浏览器不会尝试列表中的其他服务器,因此一般来讲网络服务器范围中的均衡DNS无法提供服务器容错功能。

  幸运地是,因为我们有客户端,可以同时轻松地解决这两个问题。此外这些技术也对你基于浏览器的游戏奏效(在JS加载完毕并开始运行后)。

注6:严格来讲要稍微复杂一些,因为DNS数据包中含有服务器列表,但由于近乎所有人都会忽略所返回的数据包中其他条目,只关注了第一条,因此与每条请求只返回一个IP相差无几。例外情况就是服务器可以自行作出选择,参见“客户端均衡”

  客户端随机平衡

  为了改进DNS均衡,人们使用了一个简单的办法。我们不再从服务器端所有内容中循环返回,而是直接将同一张服务器列表返回给所有客户端。这张列表可能会硬编码到客户端中(我个人就用了这个办法,大获成功),或者通过DNS来分配这张列表,但要按照简单的IP列表匹配相应名称的形式,用getaddrinfo() 或类似命令来检索。用哪个办法无关紧要。

  一旦客户端获得IP列表,一切都变得非常简单。客户端从IP列表中随机获取,尝试连接随机选中的IP。如果连接不成功或者丢失,客户端再尝试另一个随机IP,尝试重连。

  失败案例包括:

 

  1. int myrand() {//DONT DO THIS!
    • srand(time(0));
      • return rand();
        • }

  这里我们将与时间最关键的东西(一般是游戏世界服务器)迁移到终端用户旁,降低相应数据中心附近用户的延迟情况。维护这样的基础设备确实令人头疼,不过这种做法是可取的。因此如果真的担心延迟情况,可以按照这种方式来部署。提醒一句:如果这样做,会出现“区域服务器”——同样也有自己的问题:你需要确保该区域的客户端只与相关的前端服务器连通,数据中心间连通的安全性也会成为很大的问题,等等;不过还是可行的,但这种做法仅限于真正有此需要的时候。

  关于关联

  在某些情况下,你可以决定需要一种“关联”,以便某类用户(一般是在特定游戏世界的用户)连到特定的前端服务器。

  注意:在提到前端服务器的时候,“关联”的概念与在网络服务器的均衡器中所使用的经典概念(一般指的是“持久性”或“粘性”)并不相同。在网络世界中,持久性与粘性指的是从同一个服务器到同一个的客户端(以处理会话与各客户端缓存)。但对于前端服务器来说是不同的,一般指的是前端服务器到游戏世界的关联(对于玩家,或者对于玩家和观看者),而不是客户端到服务器的关联(参见“前端服务器:服务器间延迟与玩家间延迟的不同”部分)。

  技术上来说,实现前端服务器到游戏世界的关联并不难,但真正的问题在于部署关联之后。简单来说,只要使用关联的游戏世界数量不大,就会很顺利。换句话说,一旦有大量玩家使用关联规则进行连接,不同的前端服务器间想要达到合理的均衡就变得十分困难。在没有关联时,由于大数字法则均衡实现近乎完美;在引入关联规则后,就会导致分配平衡倾斜,关联所影响的玩家越多,距离理想分配状态就偏离地越远,因此在达到负载均衡的同时管理这些规则可能成为很大的挑战。

  底线:尽可能避免关联。

  前端服务器:实现

  现在我们来讨论一下如何实现前端服务器的部署。如上所述,前端服务器的关键属性是,在发生故障时易于替换。为了实现这一点,

  必须确保在所有前端服务器上没有原始的游戏世界换句话说,前端服务器应当只有原始游戏世界状态的副本,而原始的游戏世界状态则保存在游戏服务器上。

  如果使用普通的订阅/发布器(或状态复制器)之类的东西,也无需太过担心,但如果在前端服务器中引入了任何定制逻辑的话,要非常小心,因为易于替换属性可能会导致重要资料遗失。

  前端服务器:QnFSM实现

  前端服务器的一个实现是在纯粹的Queues-and-FSM架构下实现,如下图:

1402554ngsp8rzg6gntn3y.jpg
  这里在游戏服务器方面,TCP-与UDP-相关的线程,与在“QnFSM下实现游戏服务器架构”部分中的描述非常类似;由一个或多个路径选择数据线程(每个路径选择数据 FSM中至少有一个线程)负责分发所有的数据包,并缓存数据(比如“游戏世界”数据)。我们来深入讨论一下这些路径相关的FSM。

  路径选择数据 FSM每个路径选择数据 FSM都有自己要处理的数据(与更新)。例如,这样一个路径选择数据 FSM可能包含一个游戏世界状态。另一个路径选择数据 FSM可能处理点对点数据包的分发工作,而数据包是往返于玩家与游戏服务器之间的。路径选择数据FSM所处理数据类型一般来说会有三种类型:

  ● 普通连接处理器:处理点对点通讯,包括玩家输入与服务器之间的连接;

  ● 普通发布/订阅处理器:缓存与处理普通但结构化的数据,比如可用游戏列表——如果允许玩家选择游戏的话;

  ● 特殊游戏世界处理器:如果需求功能普通处理器无法解决的话,缓存与处理游戏世界数据。在很多情况下,可以不需要特殊游戏世界处理器;但如果想要执行某类服务器端过滤的话(比如服务器端的战争迷雾),使用它可以避免将数据发送给不应看到的用户,这样就无法从服务器端破解移除战争迷雾了——在这样的情况下,特殊游戏世界处理器非常必要。

  在同一个路径选择数据线程中有不止一个路径选择数据FSM也是可能的(并且通常可行的),由于线程数量很大,这样能够减少不必要的负载(以及不必要的线程切换)。如何将这些路径选择数据FSM结合到特定线程中——很大程度上取决于游戏,不过一般来说,普通连接处理器速度很快,可以放在一个线程中。对于普通发布/订阅器以及特殊游戏世界处理器来说,分发到不同的线程中应当考虑到一般负荷与允许的延迟。经验法则(一般来说):每个线程的FSM越多,延迟越高,线程相关的开销越小;不幸的是,其他依赖大多要根据游戏来定。

  路径选择数据Factory Thread

  路径选择数据Factory Thread的主要责任就是,根据来自TCP/UDP线程的请求,创建路径选择数据线程(与相应FSM)。路径选择数据FSM的典型生命周期如下:

  ● 一个TCP/UDP FSM需要发送某些消息(或者提供某些状态的同步);如果路径选择数据FSM中没有数据,需要在导入消息到自身缓存时发现这一情况。

  ● TCP/UDP FSM向路径选择数据Factory Thread发送请求;

  ● Factory FSM(在适当的路径选择数据FSM中)创建路径选择数据线程;

  ● 从适当的路径选择数据FSM中发出消息,返回请求的TCP/UDP线程;

  ● Factory FSM报告消息队列的ID:

    ● TCP/UDP FSM向适当的队列发送消息(使用ID而非指针,允许确定“recording”或者“replay”)。

  ● 每当路径选择数据 FSM并非必须时,TCP/UDP会向Factory FSM报告:

    ● 如果是最后一个需要该路径选择数据 FSM的TCP/UDP FSM,那么Factory FSM可能引导适当的路径选择数据线程撤销该路径选择数据FSM。

  在游戏服务器和客户端中的路径选择数据FSM

  我得承认,个人很喜欢这些路径选择数据FSM。因为喜爱,我不止将它们用在前端服务器上,还用在游戏服务器和客户端上,尽管严格来说不应该放在那里;在简化工作上,它们确实帮了我大忙,让所有通讯都统一起来。不过是否在游戏服务器上和客户端时选用它们,就是你的选择了。

  前端服务器总结

  总结一下:

  根据经验法则,前端服务器是个好东西。尤其是:

  ● 减轻了游戏服务器的负载;
    ● 让系统设备价格更低(因为前端服务器比较便宜);
    ● 也提高了整体系统的可靠性(因为前端服务器很容易更换);
  ● 促进了单一客户端的连接(一般来讲是好事);
  ● 促进客户端负载均衡;
  ● 出现大事件时,接待10万+的观看者变得很简单(事实上没有限制);
  ● 缺点仅限于额外延迟,而这种延迟一般都在亚毫秒级别;

  一般对于游戏来说,客户端负载均衡是最好的选择:

  ● 唯一可能的例外是基于网络的部署架构——可能需要服务器端均衡器;
  ● 需要避免大规模关联;

  CDN类分布是有可能的,但有雷区;
  前端服务器可以通过QnFSM架构实现。

csdn编译

声明:游资网登载此文出于传递信息之目的,绝不意味着游资网赞同其观点或证实其描述。

锐亚教育

锐亚教育,游戏开发论坛|游戏制作人|游戏策划|游戏开发|独立游戏|游戏产业|游戏研发|游戏运营| unity|unity3d|unity3d官网|unity3d 教程|金融帝国3|8k8k8k|mcafee8.5i|游戏蛮牛|蛮牛 unity|蛮牛