游戏服务器的架构演进漫谈:从茅草屋到智慧城市的技术江湖

243 阅读19分钟

游戏服务器的架构演进漫谈

架构演变:从「单房间小店」到「智慧城市」的进化史

单服务器进程:石器时代的「茅草屋」(1978 - 1980)

image.png

1978 年诞生的《MUD1》,宛如村口那独一无二的杂货铺。在这个虚拟世界里,服务器就如同杂货铺的掌柜,身兼数职,一边招呼玩家(处理请求),一边用算盘记账(存数据)。玩家发出 “up” 指令爬楼梯,掌柜掌柜得先在账本画个√,再慢悠悠告诉所有人 "楼上有只野猪"。要是 10 个玩家同时发出指令, 掌柜能把账本记成鬼画符 ——EXT 磁盘停电丢 20% 数据的名场面。

从技术层面来看,当时采用select()实现非阻塞 IO,就好像掌柜竖着耳朵,试图同时接听多个客人的订单。玩家数据直接写入 EXT 磁盘,某次停电导致 20% 数据丢失(1980 年实测)。这样的架构下,并发上限大约只有 50 人,一旦超员,指令响应延迟会超过 1 分钟,这对玩家来说,就如同在店铺点单后,要苦等 1 分钟才能拿到商品,体验极差。1980 年《MUD1》开源后衍生出的 MUDOS,首次实现了 “客户端 - 服务器” 交互模型,这一创新为后续如《网络创世纪》等游戏的发展奠定了坚实基础。

迭代过渡:从「茅草屋」到「分科店铺」的必然

2000年左右,网络游戏已经从文字MUD进入了全面图形化年代。游戏内容的越来越丰富,游戏数据量也越来越大,早期MUDOS的架构变得越来越吃不消了,各种负载问题慢慢浮上水面,传统的单服务器结构成为了瓶颈。

即当《MUD1》杂货铺从最初的 “村口唠嗑” 小聚,摇身一变成为 “全镇赶集” 的热闹场所时,原本只能容纳 50 人的茅草屋,刹那间要塞进 5000 人。这场景,就如同庙会时杂货铺被汹涌人潮挤破门槛,掌柜的算盘珠子都要快被按飞了。也因此到了 2000 年,《传奇》的老板果断一拍桌子:“把铺子拆成布庄和铁匠铺!” 这绝非任性之举,而是 EXT 磁盘被海量数据挤到快要冒火星的无奈之举。单服仅能承载 50 人的小破店,终究难以招架图形网游如潮水般的人潮,只能被迫扩张成商业街。

多服务器进程:小镇里的「分科店铺」(2000 年前后)

image.png

《传奇》大刀阔斧地把服务器拆分成 “比奇布庄”“盟重铁匠铺” 等。这种 “分科店铺” 模式的分科逻辑,是依据地图进行分区,类似小镇依照不同街道开设分店,布庄就专心卖衣服,铁匠铺一门心思打武器。玩家跨区域就像拎着购物袋在不同店铺间穿梭,从比奇到盟重的 “包裹邮寄”,往往需要耗费 300ms。在 2002 年春节期间,竟有 10% 玩家的装备在邮寄途中离奇 “被山贼抢了”(实际上就是数据丢失)。从技术本质来讲,这种方式解决了单服 500 人上限的难题,全服 10 家店依靠`playerId % serverCount`这种哈希分流方式,就如同按照衣服颜色给顾客派号,引导顾客前往不同店铺。
迭代过渡:从「分科店铺」到「专卖店」的专业裂变

分科店铺虽说能实现分流,但当玩家既要在布庄买衣服,又要组队打怪时,就会发现布庄老板压根不懂组队记账这档子事。这便是 “多类型服务器进程” 诞生的关键契机。分科店铺是 “按地段分科”,而专卖店则是 “按业务分专业”。2001 年,《热血传奇》率先拆分出聊天铺、匹配铺、组队铺等。自此,买衣服和组队打怪彻底变成了截然不同的两码事。

多类型服务器进程:商业街的「专卖店」(2001 年起)

多类型服务器进程强调按业务类型实现专精。以《热血传奇》为例,它拆分出的聊天铺(IM)专门负责玩家唠嗑交流,即便这部分出现故障,也丝毫不会影响玩家购买装备等其他操作,大大降低了模块间的耦合度;匹配铺(Match)专职处理比武招亲等对战匹配事宜,使得 CPU 占用率从原本的 80% 大幅降至 30%,有效分摊了系统压力;组队铺(Team)统一管理队伍账本,有力避免了不同店铺记录混乱,确保了数据的一致性。打个比方,分科店铺就像是 “一条街混杂着卖衣服和武器”,而专卖店则是 “服装店、武器店、茶馆等各司其职”,玩家在服装店挑选衣服时,再也不会被铁匠铺四溅的火星烫到。

数据库进化:从「草纸记账」到「支付宝」的逆袭

阶段 1:MySQL 换「印刷账本」(《梦幻西游》2003 年)

image.png

服务器的一个重要功能是存储玩家的游戏数据,以便下次玩家上线时能读取先前的游戏数据继续游戏。早前文字MUD游戏的数据持久化存储使用的是服务器本地文件存储,玩家下线时或者定时将数据存储在EXT磁盘中,这样的存储在逻辑上是没有问题的,但是玩家频繁的上下线会导致服务器频繁的I/O,导致负载越来越大,以及EXT磁盘分区比较脆弱,稍微停电容易发生数据丢失。因此需要拆分文件存储到数据库(MySQL)去。

类比以往用 EXT 草纸记账,要是写错了,只能用口水艰难擦拭修改。到了 2003 年,《梦幻西游》果断换上了带复写纸的印刷账本(MySQL)。这一更换效果显著,1000 次记账操作从原来需要耗费 500ms,锐减至仅需 50ms,机房那恼人的噪音也从持续不断的 “咔嚓咔嚓” 声,变成了相对温和的 “嗡嗡” 声。2005 年,引入 InnoDB 复写纸后,交易出错率更是从高达 5% 骤降至 0.1%,掌柜终于不用再气得拿算盘砸自己脑袋了。

迭代过渡:从「印刷账本」到「账房先生」的无奈

2005 年《魔兽世界》上线后,玩家数量呈爆发式增长,100 个玩家同时查账的情况屡见不鲜。印刷账本被频繁翻阅,甚至出现掉页的尴尬状况。在这种极端压力下,掌柜实在没辙,只能赶忙请来账房先生(数据库代理)帮忙救急,说道:“你先把热门账目记在算盘上!” 这情形,就如同超市收银员忙到几乎晕厥,无奈之下只能找个临时工先帮忙记流水账。MySQL 纵使性能强劲,面对十万人同时疯狂翻账本的 “疯狂” 场景,也着实扛不住,账房先生便成了不得已而为之的救命稻草。

阶段 2:账房先生的算盘缓存「数据库代理进程」(《魔兽世界》2005 年)

image.png

账房先生(DB 代理)主要负责三件关键事情:其一,把玩家背包等常用信息记在算盘(内存缓存)上,这样玩家购买药水等操作时,账房先生直接查看算盘便能快速处理;其二,每晚关店后,账房先生会将算盘上记录的数据抄回账本(异步持久化),确保数据的长期保存;其三,当玩家打开背包时,账房先生会先扒拉算盘查看缓存数据,如果算盘上没有相关记录,再去翻账本(数据库)查找,并且顺手将查询结果记录在算盘上(缓存 300 秒),以便后续快速查询,有效避免了缓存击穿问题。

 迭代过渡:从「账房先生」到「支付宝」的降维打击

2016 年《阴阳师》开服,玩家抽卡频率高得惊人,账房先生的算盘都快被拨得冒火星了。掌柜灵机一动,一拍大腿:“给每人发支付宝!” 于是,Redis 就如同突然普及开来的手机支付一般横空出世。90% 的零钱交易,玩家直接扫码就能瞬间完成,账房先生瞬间 “失业”。这并非账房先生工作不努力,而是时代变了,移动支付的出现,直接终结了算盘记账的旧时代。

阶段 3:Redis 化身便捷电子钱包(《阴阳师》2016 年)

image.png

Redis 在《阴阳师》中扮演着便捷电子钱包的角色,主要有三大作用:玩家抽卡、购买体力等高频操作,直接通过 Redis “刷手机” 就能完成,无需再像从前那样费劲地翻存折(MySQL);90% 的零钱交易都在 Redis 这个 “支付宝” 中完成,使得存折(MySQL)的压力大幅降低 60%;玩家登录游戏时,从以往 “翻存折” 需要等待 2 秒,到如今 “刷手机” 仅需 300ms,在 2018 年京都决战这种超大规模活动期间,即便抽卡队列排满了长安街,玩家也几乎感受不到卡顿。

逻辑进程拆分:专业化分工的「城市部门」

image.png

迭代过渡:从「商业街」到「CBD」的城市化

当游戏服务器从最初的 “小镇商业街”,逐步发展演变成繁华的 “城市 CBD”,玩家逛一次游戏世界,就如同市民在现实城市生活一般,需要跑聊天馆、比武场、装备店等多个地方。2004 年,《魔兽世界》的玩家就曾吐槽:“逛一圈游戏世界,要敲十次门,比逛故宫还累!” 鉴于此,城市管理者(架构师)果断建立起 “城管大队”(网关服务器),并规定:“所有玩家进城必须先过安检,由我来帮你们传话!”

网关服务器:城市的「边防检查站」

image.png

网关如同城市的边防检查站,主要执行三件重要安检任务:仔细开箱检查玩家的包裹(请求),某手游通过这种方式成功拦下了 90% 的作弊包裹(外挂);按照 “长度标签” 等规则分拣包裹(粘包处理),避免把袜子和武器等物品装错箱;通过三层安检(场景 - 对象 - 网络)机制,确保 10 万玩家同时进城也不会出现拥堵情况。

分布式消息队列:城市「地下管网」

当前的发展规模基本能稳定的为玩家提供服务,但是扩展性非常的差,每个边防检查站(网关服务器)要跟所有类型的所有逻辑店铺(服务器)维持连接;一些大型游戏这样的逻辑服务都可能有好几千个,如果每个都维护一个连接的话,光维护这些连接就会消耗大量的内存和运算,而且每新增一个逻辑服务器都要跟所有网关服务器连接,扩展性很差。所以服务器之间的通信通常都会增加一个消息队列进程,专门用来服务器之间内部转发消息。

迭代过渡:从「独口水井」到「自来水厂」的基建升级

image.png

老版的服务器通常会有一类全局服务管理器(Master),全局服务管理器全服只有一个,会存放全服共享或者唯一的数据,早期的数据缓存功能也是由全局服务管理器来实现的,通常全局服务管理器还会存储和管理所有服务器的信息,比如服务器的IP地址、服务器的容量和服务器的增删等,还有就是全局服务管理器(Master)需要与每个服务器进行连接,用心跳维护服务器是否处在可用状态,所以服务器之间的消息转发由它来承担,全局服务管理器(Master)扮演着消息队列的角色。

早期的 Master 服务器,就像村里唯一的一口水井,承担着全村的供水任务。在 2020 年某游戏春节活动期间,这口水井突然 “爆管”,导致全服断水 5 分钟,玩家们提着水桶愤怒砸门。痛定思痛之下,城市下定决心进行基建大升级:“挖自来水厂,铺设地下管网!” 分布式消息队列就如同新建成的自来水系统,即便上海服的水管出现爆裂,北京服的水依旧能够通过备用管道正常输送过来。对于服务器通信而言,城市供水绝不能仅仅依赖一口井,服务器通信同样不能只依靠一个 Master。

分布式消息队列:从「村长派水」到「自来水厂」

image.png

分布式消息队列主要实现两件关键事情:为每个店铺(服务)接上专属水管(订阅 Topic),例如茶馆开通 “聊天水管”,比武台开通 “匹配水管”,确保各个服务之间的消息传输互不干扰;ZooKeeper 如同认真负责的户籍警,运用 Raft 算法仔细核查户口,保证北京玩家的信息不会被错误记成杭州黑户,维持数据的一致性和准确性。

服务发现 「户籍系统」

image.png

消息队列由单点转换成分布式,消息队列也会从服务管理器中脱离出来,专心做消息转发的功能,那服务管理器中剩下的功能怎么办?剩下的其实就是服务发现的功能:存储各个服务器信息(IP地址,容量,服务器ID,服务器类型等)和管理服务器的增删等功能。

怎么理解服务发现的功能?我举一个实际例子。假设你的游戏已经上线,服务器也正在运行中,到了某个节日,游戏做了一个活动,这个活动做的太好了,吸引了一大批玩家,大量的玩家导致你原有的服务器撑不住,需要在线加服务器,这时候就会出现几个问题:原有的服务器怎么才能知道新加了服务器?新加的服务器怎么知道老服务器信息?服务发现就是要解决这样的问题。

新加的服务器A向服务发现中心注册自己的信息,信息注册后,服务发现中心会将服务器A的信息广播给所有老的服务器,通知老服务器有新的服务器加进来,同时也会向新服务器A发送所有老服务器的信息。

服务发现本质上是一类数据库,这类数据库为了保证数据的高可用会支持容错性。容错性一般由数据库备份保证,也就是一份数据会保存在多个机器上。

image.png

分布式存储

由于大型服务器数据量比较大,最终的DB和Redis也会换成分布式数据库。

image.png

负载均衡:流量的「交通管制」全解析

什么是负载均衡?为什么要负载均衡?每个服务器由一台计算机组成,单个计算机只有有限计算资源(CPU、内存和网络带宽),当大量玩家都往一个服务器中发送请求时,服务器因为计算不过来,大量请求数据积累超过内存,这样会直接导致计算机崩溃。虽然可以将服务器换成性能更好的服务器,但单个服务器性能受硬件的限制,计算的有限性很低,而且动态扩展性不强,比如线上要临时加计算资源时,不可能直接关服务器吧。所以一般都是通过软件方法,将输入的网络流量分配到多个计算机中,以平摊计算压力,这个软件方法就是负载均衡,总的来说负载均衡是高效的分配输入的网络流量到一组后端服务器。

一个游戏会有多个阶段需要用到负载均衡,这些阶段都有个共同点就是涉及到服务器的选择和变更,比如登录时要选择哪个服务器登录,RPG场景切换时服务器的切换,匹配成功进行战斗时等等,这些阶段都会涉及到负载均衡。负载均衡针对不同需求场景有不同的负载均衡方法,这里主要写登录服(Login)和网关服(Gateway)的负载均衡。

登录服务器的负载均衡:「选餐馆」的学问

登录服务器是在线游戏的入口,没有登录服的话,你的游戏客户端进不去游戏,卡在登录页面。登录服的负载均衡方法大概可以分为两种,这两种方法与服务器的架构相关联的,第一类是分区分服的负载均衡,第二类是大服务器下的负载均衡方法,大服务器和分区服服务器主要的区别是:大服务器下所有人(全国或者全世界的人)可以一起比赛,不会出现同一个账号有不同数据,而分区分服的服务器是不同的登录服你的角色数据是不同的,比如你在北京服是100级,那你选择杭州服后是1级。分区分服和大服务器架构最本质的区别是是否公用了数据库

分区分服:自主选餐馆(如《王者荣耀》)

《王者荣耀》采用的分区分服策略,就像是玩家自主选择餐馆。玩家可以自行挑选服务器,系统会贴心推荐空位较多的 “餐馆”,而爆满的服务器则会挂上 “客满” 的牌子。从技术本质上看,这种方式实现数据隔离较为简单,但存在明显弊端,跨服社交变得困难重重,北京服的玩家无法与杭州服的玩家组队开黑。在 2023 年春节期间,《王者荣耀》推荐的 “成都 38 服”,由于玩家扎堆涌入,瞬间变成了 “成都堵服”,客服人员不得不连夜紧急加开 “成都 39 服” 来缓解压力。

大服务器架构:自助餐自动派桌(如《吃鸡》)

《吃鸡》采用的大服务器架构,类似自助餐自动派桌模式。客户端自动为玩家分配登录服,即便 50 万玩家同时涌入 “取餐”,也不会出现排队拥堵的情况。从技术本质来讲,全球同服共享数据库,通过 “随机 + 负载阈值” 算法,如同自助餐工作人员观察哪桌空位多就安排顾客入座一般,合理分配玩家。此外,某手游运用 “地域就近原则”,自动将北京玩家派到北京服,使得延迟降低了 40%,大大提升了玩家的游戏体验。

网关服务器的负载均衡:「高速收费站」选车道

网关服务器的负载均衡策略,类似于高速收费站引导车辆选择车道。通常采用 “最小连接数” 算法,智能选择负载最轻的网关。以某 MOBA 游戏为例,通过这种策略,成功将网关延迟波动控制在 5ms 以内。在《英雄联盟》团战高峰期,网关能够自动把玩家派到 CPU 占用率低于 30% 的服务器,确保玩家技能释放延迟稳定在 100ms,为玩家提供流畅的游戏操作体验。

战斗服务器:「VIP 通道」直连

在实时战斗场景中,为了最大程度降低延迟,提升玩家的战斗体验,游戏采取客户端直连 Battle 服务器的方式,绕开网关这个中间环节,就如同演唱会 VIP 可以直接走后台通道快速入场。以某 FPS 游戏为例,直连后开枪延迟从网关转发时的 150ms,大幅降低至 30ms。并且,战斗服务器还自带 “少管所” 功能,能够将 90% 的作弊请求拦截在 VIP 门外,有效维护游戏的公平性和竞技环境。

架构演进总结:从「村口集市」到「智慧城市」

多服务器进程成功解决了 “人太多挤爆店” 的棘手问题,就如同小镇逐渐扩展成繁华商业街,容纳更多的玩家和业务;多类型服务器针对 “业务太杂管不过来” 的难题,通过精细的业务分类和专业化处理,将商业街升级为功能明确的专卖店,提升了运营效率;负载均衡着力解决 “交通拥堵”,也就是网络流量拥堵的问题,把县城狭窄的马路拓宽成顺畅的高速公路,保障数据传输的高效和稳定。

服务器架构的发展历程,恰似城市规划建设的过程。起初解决有没有的问题(单服),满足基本的游戏运行需求;接着解决好不好的问题(多服),提升服务器的承载能力和运行效率;最后解决爽不爽的问题(负载均衡),为玩家打造流畅、稳定的游戏体验。而玩家永远是最为挑剔的市民,一旦游戏出现 “路太堵(网络延迟高)?拆(优化架构)!店太挤(服务器承载不足)?分(多服务器进程)!体验差(游戏卡顿、操作不流畅)?改(全面优化)!” 的情况,就会毫不留情地提出要求,促使游戏开发者不断推动服务器架构的创新与升级。