-
Notifications
You must be signed in to change notification settings - Fork 45
zhihu gameserver reading notes 1
landon edited this page Dec 14, 2021
·
1 revision
注:杭州小白工作室#忍者必须死3的一些服务器端#学习笔记(作者:水风)
https://zhuanlan.zhihu.com/p/342953318
游戏服务端的高并发和高可用
- 水平扩展有两种常见的实现模型
- 大厅服和所有的战斗进程进行全连接,需要访问战斗服务时去管理器中查询服务所在的进程地址,然后直接去访问进程
- 在战斗进程前面挂一个路由,路由记录每个战斗所在的战斗进程,相关请求会转发到对应的进程
- 无状态并不适合所有服务,一般对于状态简单明确的服务,可以优先使用无状态,比如好友服务
- 有状态服务的路由需要明确每个请求给哪个进程处理,给其他进程其他进程因为没有相关状态信息也无法处理。比如上文提到的战斗服务,路由根据战斗ID将相关请求发给对应战斗所在的进程中才能处理
- 集群举例
- 集群可以分为三类:支持水平扩展的有状态服务、支持水平扩展的无状态服务、不支持水平扩展的单点服务
- 在这种架构情况下,我们游戏的承载上限瓶颈在于单点服务,而单点服务逻辑相对比较简单,承载上限很高。此外,支持水平扩展的服务进程出现异常只会影响此进程所服务的玩家,具有较高的可用性
- landon:有状态进程如果异常甚至宕机,那么会影响该进程玩家。短期内如果进程可以重启,那么路由策略还可以指定该机器。如果超过一段时间,则需要路由到新进程或者已有进程。为保证玩家体验,需要可以快速指定路由策略(前提是保证内存数据已保存,即执行新的路由之前等待即时存储完成)。极端情况下比如进程卡死这种(比如频繁fgc),那么此时无法执行存储请求,TODO 需要另外一种数据存储策略,保证不回档(日志、dc、diff、合并)
- 在我们游戏的服务器集群中,三分之二左右的进程是处理玩家个人逻辑的进程(玩家集群,很多游戏项目叫大厅服务器)。每个进程处理一部分玩家的业务逻辑,通过shading将玩家分配在不同的玩家进程中
- 可以通过增加个人逻辑进程数量提升服务器承载量,我们支持不停服增加或减少进程即动态扩容缩容。这些进程之间就是平等的,不同进程之间没有强依赖关系。当一个进程crash时,他不会影响其他进程的玩家
- landon:TODO 增加进程伸缩测试,新导流玩家可以导入到新的进程(扩容)。不过缩容的话,需要测试,因为涉及到一系列流程。比如缩容的机器先从路由策略移除,已有的玩家不能执行操作,等待数据存储完毕,踢人,玩家路由到新进程等
- 除了玩家进程,还有战斗进程、家族进程等类似进程可以这么设计
- 上面提到的都是有状态服务,我们需要记录每个玩家/战斗/家族在哪个进程中,此外,若进程出现异常,虽然不会影响其他玩家/战斗/家族,但当前进程中玩家/战斗/家族都会不可用,而且会丢失一些数据
- landon:1.记录每个玩家/战斗/家族在哪个进程中,这个就是单点的服务 2. todo 还是要根本解决丢失数据
- 我们将部分服务使用无状态实现,比如登陆、支付、好友、部分排行榜等。由于无状态服务具相对于有状态对异常更友好、动态扩容缩容模型 更简单,因此有对于一个新的服务我们优先考虑使用无状态,若状态较复杂才考虑使用有状态服务实现
- 游戏服务中难免出现一些单点服务,比如玩家管理器、集群管理器、家族管理器等,这类服务不具扩展能力,是游戏服务器的承载瓶颈。此外,也不具有高可用性,如果出现异常会导致导致整个游戏集群不可用
- 单点服务逻辑普遍简单(复杂逻辑我们都要支持水平扩容),性能承载普遍较高
- 当然,单点服务也可以改为支持水平扩展的,只是工作量的问题。理论是来说,是能完全消除单点的,只是对于大部分项目来说性价比不高意义不大
- landon:1. 引入注册中心 2. TODO 路由策略是否保存在调用客户端(即通过注册中心拉到客户端后,根据路由规则拿到节点后保存到客户端内存。比如从网关路由到玩家集群,那么网关到玩家的路由信息可以保存在当前网关上,最好也保存到redis。因为比如玩家断线后,可能切网关或者网关异常,那么此时可以优先从redis拿到路由节点并缓存,不过这里redis相当于存储所有玩家的路由节点了。要处理过期)。3. 公会的路由策略也可以保存在redis,根据公会id路由。4. 关于推荐公会这种,个人建议也可以通过redis做,这样任何一个公会节点都可以做公会推荐,不必用一个专门的单点来做 5. TODO 还是需要想办法消除单点或者尽量避免单点
- 我们提到的单点,如果不好消除或者消除成本很高,可以通过垂直扩展把这个逻辑放在高配机器上,提升单点逻辑的承载
- 对战斗服进行性能优化,比如使用C++写高消耗模块
- 消除系统单点的前提是消除逻辑单点。武器生成一个全服唯一的ID以标识此武器。这个ID可以使用一个自增的ID,此时就造成逻辑单点。但如果武器生成频率很高,因为游戏中所有逻辑都需要去一个地方去申请这个ID,那就可能产生瓶颈 landon-这里可以使用类似雪花算法
- 游戏的玩家服务一般都是有状态服务,玩家上线时将数据从数据库读到内存中,在线期间读写数据都是直接操作内存,下线时或隔段时间去落地到数据库。这种方案大大降低了数据库读写操作,对数据库压力会小很多 landon-还是要解决宕机回档问题
- 多集群中需要解决的一个问题是跨集群通信问题,集群内一般是进程间全连接,但集群间如果进程全连接会造成拓扑混乱连接数量爆炸的问题,因此集群间通信一般使用消息总线,所有的集群通过消息总线进行通信
- landon:这个集群可以理解为比如大区的概念或者比如腾讯游戏微信和QQ的概念。正常来说集群内业务是独立的,但是比如匹配等一些逻辑是跨集群的,即涉及到跨集群通讯。
- 在游戏业务场景中,玩家的在线和时间、活动等关系很大,不同的时间在线数量可能有几倍几十倍的差别
- 对于预期内的高流量,可以通过提前做好扩容来进行承载
- 对于非预期的瞬间高并发,可以通过排队系统将流量卡在系统外,动态扩容后再慢慢的进入游戏中
- landon 1. 排队系统应该做成一个基础组件,反应整体服务器的负载情况(后端节点是区服无关的,扩容后,那么排队靠前的就可以进入新节点)2. 引入serverless,可以通过云原生本身的能力来解决一些流量变化较大的场景,不过这部分偏无状态服务(比如高峰的聊天翻译、战斗复盘)
- 服务降级:战斗逻辑简化,比如国战时一般只要玩家觉得场面热闹就差不多了
- 对于高并发,水平扩展表示我们可以通过增加机器/进程提高承载量。对于高可用,是说当机器/进程出现异常或者崩溃时,不会影响集群的整体可用
- 异常监控、异常处理(一个消息超时后如何处理,是重试还是忽略。如果一个进程不可用,我们需要将此进程踢出集群)、服务恢复:对于有状态,可以将状态迁移到其他进程提供服务。常见的恢复方案比如将所服务的玩家直接踢下线,然后重新登陆、服务降级:服务降级常见的排队系统、关闭指定功能等 landon 1. 有状态服务恢复/迁移比较麻烦,TODO 需要一套完备的流程测试 2. 服务的超时可以底层增加重试机制
- 应该将大功能尽量的拆成一个个小的服务,每个服务只负责一小块功能。Skynet也提供了比较好的Service模式,不同的Service可以放在一个进程中,也可以放在不同的进程中 landon TODO 测试vert.x verticle,这种服务的运行方式很好,测试运行一下skynet,对比一下vert.x
- 通过服务隔离和灰度发布,也是为了将高风险的服务进行隔离,让它即使出现了问题也不要影响到系统的整体可用
- 对于有状态服务,支持水平扩展的进程可以做到一个进程出现异常不影响其他进程提供服务,但这个进程crash了会导致这个进程提供的服务不可用,并且造成内存中的数据丢失等问题
- 使用主从复制
- 游戏服务器使用此方案写业务逻辑的较少,有些集群管理节点(非业务逻辑)会使用此方案 landon-单点服务可以考虑这个主备方案,游戏业务逻辑服务如果用此方案则对于部署成本以及可能的一定性能影响。
- 需要注意redis等服务的主从切换等,导致网络连接断线,因此,我们必须在逻辑中处理网络中断并重连。在网络断线重连阶段,必然导致某些db请求失败。在数据落地场景中,需要判断每次db请求是否成功,若不成功进行重试并且要保证请求的幂等性以防止请求多次执行 landon 我们这边实际用的是mongo和redis,在tdr中会要求测试节点切换是否会业务造成影响
- 平衡好开发成本和人力成本
- 越复杂的系统越容易出现问题,如果没有足够的人力去测试、维护和迭代,还不如用更简单的方式实现,反而出问题的概率更低
- landon 这个说的比较有道理。目前我们这边有一个slg项目,服务就有一些过度设计,导致前期部署成本和财务成本都比较高
- 并不是要求对于所有的异常都能容灾,那是不可能的
https://zhuanlan.zhihu.com/p/341855913
某百万DAU游戏的服务端优化工作
- 使用skynet开发. landon:三国志三略版也是用的skynet开发
- 进程级别的重启或滚动更新:若进程重启不会影响到整体游戏集群,那么可以通过重启进程来更新代码。比如我们的登陆服务器,可以先通过负载均衡器将流量从某一个登陆服转给其他的,然后等没有流量后重启,然后依次重启其他的。玩家逻辑进程也是如此,可以先不在分配新玩家登陆某玩家进程,然后将当前进程玩家迁移到其他进程,然后重启之,依次执行滚动更新即可。
- landon 这种进程更新方式可以尝试一下。这种好处就是不像传统方式停服更新。TODO 发布方式(蓝绿发布、滚动发布、灰度发布、红黑部署),即如何避免传统的游戏停服维护,让玩家体验更好
- 详尽的日志
- landon 这个不多说,之前的经验甚至可以打印客户端的所有请求参数
- 对于战斗,最好的方案是回放录像,日志一般是次选项
- 监控和报警
- landon 这个不多少,包括道具监控,避免刷道具
- 容错:保底逻辑
- landon 防御式编程
- 异步提交
- 消息队列 landon 不要影响玩家卡顿,换句话说不要阻塞当期玩家线程,之前世界boss做过相关优化
- 消除单点和水平扩展
- 比如我们之前家族管理器管理所有家族的信息以及相关的逻辑,后来就扛不住了。然后我们抽象出来了familymaster和familynode,每个familynode管理部分家族,familynode可以无限的水平扩展。familymaster依然是单点,但是他只是记录每个家族在哪个familynode上面,所以承载上限很高 landon:玩家、公会等都可以类似处理
- 上文提到的familymaster依然是单点,但只管理familyid到familynode的信息,这个信息我们就可以存在redis中,然后每次读写都去操作redis,这样就达到了理论上的无限扩展
- 一般来说,游戏服务器并不要求完全的消除单点,因为需要做很多额外的事情,要么增加开发成本,要么增加运维成本。所以,只要我们的单点承载上限超出游戏玩家量的需求,就可以了。不要过度优化 landon:需要实际测试单点的性能上限,如果满足要求,则简单是第一原则
- 我们的玩家逻辑、战斗逻辑和家族逻辑都是可以水平扩展的。而只有一些管理全服信息的逻辑(比如维护玩家再哪个进程上)才会放在管理器里,管理器是服务器的单点,也是服务器承载量的瓶颈 landon-这个管理全服的数据信息如果放在redis,那么相当于无状态的
- 功能解耦和隔离
- Skynet提供了比较好的模块解耦模式:service模式,skynet中每个service就可以对应一个物理意义上的服务,而每个service就是一个线程,同进程service之间具有一定的隔离。而不同service可以放在一个进程,也可以放在不同的进程,提供了不同的隔离级别. landon TODO 测试一下skynet vs vert.x
- 游戏的玩家个人逻辑应该放在一个服务中.其他功能可以适当的拆分,比如好友服务、聊天服务、排行榜服务等.
- 将服务进行分组,同组服务放在同一进程。比如玩家服务、家族服务、登陆服务等。这个主要是将不同的核心服务进行隔离,也考虑容易管理。对于性能消耗高的服务,进行隔离。防止打满CPU影响其他服务。对于不稳定的服务,进行隔离
- 引入超时
- skynet把集群看作一个整体,所以通过
skynet.call
调用其他进程函数并等待返回默认是无限等待的,没有timeout.这样就导致若某个模块卡顿或者出现了异常,就会导致集群雪崩,影响到所有的功能.比如我们游戏的chat模块,曾因为某些问题导致进程卡顿,而玩家登录都会去注册和拉取聊天消息,进而导致玩家无法登录,也无法正常游戏- 业务需要处理超时问题,一般有两种方案:重试或忽略。对于有些关键逻辑,需要写重试逻辑,重试要保证幂等性。对于不重要的逻辑,可以忽略,比如发一个聊天消息。建议尽量忽略,重试逻辑写起来很麻烦,而且容易出问题
- 引入超时后,应该将游戏系统进行分割,核心业务不使用超时,不然写超时处理逻辑会非常麻烦。非核心业务加入超时,将核心业务和非核心业务进行解耦
- landon: 如果业务逻辑引入重试确实写起来很蛋疼。但是提到的聊天这种非核心业务,不能因为其出现问题而影响游戏核心业务逻辑
- 部分数据转存redis
- 如好友关系
- landon:这个在我们的架构中,就是这样做的,redis做部分存储功能。但是确实会出现1部分存mongo,1部分存redis,当回档时会出现不一致的状态。这个需要业务逻辑需要做兼容处理
- 灰度测试环境
- 灰度环境是线上环境,和测试服具有本质区别。因为直接承载线上玩家
- landon 这个在我们这边有专门的灰度发布流程,通常是和版本更新一起做的
- 动态扩容和缩容
- 对于我们这种玩家个人逻辑进程需要处理的事情多一些。我们关进程时会分步执行,先将此进程标记为新玩家不可进入,过段时间后再将非战斗状态的玩家踢下线(此步骤玩家无感知),最后强制踢下线所有玩家(此时玩家已经极少),基本做到了玩家无感知。 landon-无感知是因为前面挂了一个网关
- cache
- landon:这个划分很有意思。消费者/生产者/二者之间/三方如redis,以全服排行榜为例