i007.cc

i007.cc

优先队列-降维打击

想在C++游戏服务器中实现热更,数据缓存要如何做呢?

小弟最近需要为公司的游戏服务端框架增加一个热更+集群的方案.

由于我们公司的游戏服务器一直完全使用C++开发.
所以为了逻辑执行效率考虑.一直以来都是在服务器开启时从数据库里加载所有必要的数据到内存中(用户账户数据,各种公共数据比如地图,工会等等),在调用时就可以直接通过工厂类或管理类取到相应的类指针就能方便的操作->修改->存库了.但是这样带来的缺点就是在生产环境下每次更新维护,甚至每次紧急修复BUG都需要比较长的时间(因为每次更新完之后服务器启动要从数据库读取大量数据都非常耗时).所以就有了这个增加热更功能的需求.
集群的功能是这样的.以前比如说游戏要开10组服.那么所有的服务器配置就需要打开10套(比如每个服都有相应的网关/逻辑/社交/地图).但是最早开的服随着玩家流失可能剩下的人已经非常少了.所以每次单独维护它以及考虑到上面更新的问题(要加载大量数据,启动慢),所以希望能做到服务器是可以动态扩展以增加承载能力的.需要承载更多人的时候则在集群中增加节点即可.
目前经过我的初步研究方案有如下三种:
1.使用C++/Lua.用Lua写逻辑.这样如果只是逻辑改变的话就只用引擎重新加载Lua即可.我也研究了下云风的skynet.但是没有明白skynet是准备怎么做缓存的.是直接通过Lua调用Redis/MongoDB交互的?希望有在生产项目中用了skynet的朋友介绍下.
2.使用C++加载动态库写逻辑.逻辑更新的话则重新加载动态库即可.使用主框架来缓存数据.动态库每次向主框架请求需要操作的数据.由主框架负责和数据库交互.
3.增加一组缓存服务器.逻辑服务器只负责逻辑.每次操作都从缓存服务器请求相应的数据.操作完再还给缓存服务器保存.以达到每次维护只更换逻辑服务器.而且玩家连接到任何一组逻辑服务器都不影响操作的目的.
目前遇到主要的纠结在于:
1.数据应该如何缓存?对于复杂的业务操作可能需要大量的缓存数据以支持判断查找修改(比如大规模混战/国战)等等.那在这种情形下.如何才能确保能最快捷地从缓存中取到大量数据.
2.缓存服务器是自己写还是采用现有的NoSQL.比如Redis,memcached之类.如果用这样现成的NoSQL,如何保存一些比较复杂的结构数据.
3.以上所述的几种方案中哪种比较靠谱?

获取各位前辈有什么好的游戏服务器解决方案能够支持热更+集群动态扩展的还望指教.
希望各位游戏界前辈/同行/兄弟姐妹不吝赐教.

——————————————————————————————————————————————–
2016.08.01更新.
感谢各位的回答.在此补充一点大家可能有误会的细节.
1.为什么会有大量的缓存数据?
我也做过其它类型的游戏,有很多冷热数据有很明显界限的游戏我也用过redis等等的缓存.比如RPG,可以明显的分为在线/不在线.一个不在线的玩家基本除了社交相关系统.其它数据几乎都是不可能被需要到的.比如一些类RPG的卡牌和匹配模式的游戏,玩家和玩家交互基本是1对1的联系.基本上主要的操作都不会涉及到大量的数据存取.只需要对排行榜以及社交服务专门做就好了.
但是碰到困难的架构,主要所需要支持的游戏类型要求玩家之间非常频繁而且强烈的交互,包括不在线的玩家(基于地图的经营策略游戏,如Game of War,Clash of Kings这一类,但是比这些还要复杂).其他玩家的很多数据会被大规模的,极频繁的存取到.比如在世界地图上扫地图,还有大规模的混战,其实相当于是RPG游戏里所有的玩家都可以下一个挂机命令不在线由服务器完成自动挂机战斗甚至智能寻路等操作.所以可能一个操作或者一个定时器消息里需要大量的操作多个玩家的多种数据.而且这些玩家未必在线.都需要缓存速度快容量大以及便于跨模块存取数据.
2.根据各位的回答.我感觉TSF4G是个看着还比较靠谱的思路,利用共享内存挂载的方法实现快速重启是可以满足需要的.但是TSF4G似乎没有一个比较完善的资料.都只是宽泛的介绍了下概念.我主要是没有想明白怎么利用共享内存把如此大规模的内存挂载到一个新进程上去.比如我有一个管理类里面保存了一些指针,新进程起来从共享内存里加载这个管理类,里面保存的指针岂不是已经变成野指针了么?莫非只能从逻辑上重新构建所有保存的指针数据么?

5 thoughts on “想在C++游戏服务器中实现热更,数据缓存要如何做呢?

  • willPost author

    作者:幸福的蠕虫
    链接:https://www.zhihu.com/question/38984789/answer/97625763
    来源:知乎
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    问题描述写了这么多,看得出题主是位认真负责的同学。本人不邀自来,仅凭个人经验略答一二,也算是交流经验探讨问题吧。 关于热更。题主提到游戏服务器启服时加载数据太多,导致启服时间慢。游戏服务器启服时从数据库中加载必要的全局数据是肯定需要的,据我的经验这个数据量虽然不小,但也还不至于到不能接受的地步,不知道题主能不能先从设计的角度考虑对数据做一些精简呢?游戏功能逻辑的热更新实际上是有不少限制的,因为这种更新往往不仅是服务器操作,游戏客户端也需要做相应的修改。最简单的情况是配表的更新,只需要更新文本文件还是比较好处理的。C++/Lua的方式可以在一定程度上实现对功能逻辑的热更新,相比C++动态库更灵活。 关于集群。游戏服务器的动态扩展是一个比较复杂的问题。一般来说游戏服务器都是按服划分,后期玩家流失之后,可以考虑合服了吧。 你说的三种解决方案,第一种C++写框架Lua等脚本语言写逻辑已经比较成熟;第二种方案我觉得不是很好,首先C++写游戏逻辑已经很复杂,而C++动态库其中的坑也不少,建议慎重。第三种方案缓存服务器,正是我们也采用的,虽然可能和你想的不完全一样,但我还是多说一些吧。 游戏服务器框架:游戏逻辑服务器—数据存储服务器—Database(MySQL) 数据存储服务器主要负责数据缓存,加载数据时首先在缓存中查找,如果未命中再到数据库中加载。保存数据时,玩家数据由逻辑服发送到数据存储服务器的缓存中,然后每隔一段时间,缓存中脏数据写入到数据库中。这也是考虑到游戏数据存储“读少写多”的特点。 数据存储服务器中缓存系统是自己设计实现的,开发语言是C#。首先我们对游戏数据进行了抽象和简化,主要支持查找更新删除等基本操作。缓存中的数据基本上是key-value的形式和具体的游戏功能逻辑无关。如果换成Redis的话应该也是可以实现的,Redis支持更丰富的数据类型。我们的缓存系统有个问题是如果数据存储服务器宕掉会导致数据丢失,现在正在考虑对缓存数据加一个备份,就是內建缓存数据再发送到redis或memcached中做备份保存,这样也可以支持加快服务器重启,新进程直接从redis或者memcached中加载数据,和共享内存的方案类似。发布于 2016-04-276​收起评论​分享​收藏​感谢​3 条评论​切换为时间排序Excalibur (提问者) 1 年前感谢回答不知答主有没有玩过类似列王的纷争(Clash of kings)或者战争游戏(Game of war)之类的战争策略经营游戏.我们主要开发的就是这一类的游戏.总之就是这一类游戏有个很麻烦的地方在于玩家间交互非常多而且频繁.每个人都可能会频繁的存取到其他人的数据.哪怕别人不在线.甚至哪怕他们都不在线..比如大地图上的逻辑.所有玩家在游戏的都要快速浏览地图.所以整个地图上的数据几乎都是要缓存的.这个基本上起码也是数十万条数据.玩家天生就在地图上,玩家的很多信息都要随地图显示,这些信息肯定要缓存的.玩家有多个部队可以操作.所有的军队只要有命令在地图上,哪怕他不在线也要对军队的行动,战斗等等做计算或开定时器.所以这部分数据必须是要的..所以一层层套过来数据就多了去了..简直相当于RPG里所有玩家都在世界地图上玩家还要有基本的机器人功能.再加上很多各种游戏都通用的公会等等大量公共数据的功能.基本只和玩家有关或者不会被频繁获取到的数据只有一半左右..囧..另外,请教下你们脏数据存kv结构怎么回写数据库的?把value数据再打散来组装SQL.还是干脆直接把value打一个包存库?赞回复踩举报幸福的蠕虫 (作者) 回复Excalibur (提问者) 1 年前对于策略游戏来说,在线和离线两种状态的差别就不是特别明显了。我们之前做的是ARPG类游戏,对于你的问题我的经验也不是很多,如果能找到比较好的解决方案还希望可以分享一下。缓存中的kv数据,其中的value是基于protobuf的字节流,在写入MySQL时,首先反序列化得到内存对象,再生成insert sql语句。我们做了一个DSL工具,相关代码都可以自动生成,所以用起来也比较方便。赞 查看对话回复踩举报严伟1 年前直接上Redis吧,可以把原来的数据缓存服务器改成数据缓存组件嵌入游戏服redis还支持过期,对于上上面的问题也容易解决赞

  • willPost author

    题主第三个想法靠谱。纠结的那几点取决于你的能力。大型成熟的SNS、电商和游戏,都是采用的缓存的办法。但至于自己写专用缓存,还是redis,memcache等,取决你你们的能力。如果能力不够,就用redis、mc等,如果能力足够,建议自己写专用缓存。性能,效率会提高很多很多。

    作者:swxlion
    链接:https://www.zhihu.com/question/38984789/answer/114210821
    来源:知乎
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • willPost author

    热更新并不一定要你不关闭进程来更新逻辑。如果你有gate服务器,那说明你的客户端不跟逻辑服直连,那你只要启个新的逻辑服进程,然后把之前的连接转到新启的服就好了。这样你也不用纠结服务器的启动时间。如果你的客户端与逻辑服务器直连,那可以考虑用lua之类的热更新方案。至于缓存,其实共享内存啊redis哪种方案都可以,建议单独一个进程处理读写数据库和缓存。这样可以让游戏逻辑服务器专注于处理游戏逻辑。

    作者:Zoffy
    链接:https://www.zhihu.com/question/38984789/answer/114269602
    来源:知乎
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • willPost author

    作者:欧文韬
    链接:https://www.zhihu.com/question/38984789/answer/114606850
    来源:知乎
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    就我知道的几个答一下吧。1. skynet只是按用户存了一份数据,并没有做大量的缓存,如果需要大量缓存还是要自己写缓存服务2. 最好不要用动态库的形式转移数据,不能跨平台是一定的,并且跨动态库维护数据很容易出问题,特别是涉及全局变量和静态变量的时候。单例模式的实现基本都是全局变量或者静态变量。3. 外部缓存服务是比较靠谱的,你自己能写当然最好,没时间写完整的话用redis什么的也行啊。具体哪些用现成的哪些自己写还是要根据具体项目需要的。我们就是一部分用redis,一部分自己写。但是一定要控制好淘汰机制。tsf4g:其实这货并没有规定你一定要用共享内存放数据。而且如果你实在想用共享内存放数据的话,很多都要手动处理。所有对象得自己维护索引,然后在进程重启后恢复索引。然后基本上你就和stl容器告别了,因为所有数据里都不能存任何地裸指针,当然你可以用boost的方法存偏移指针,或者按类型写支持共享内存和stl容器的allocator。很多用tsf4g的项目是用index的。如果你想用任何堆相关的算法,红黑树,链表等等,自己写。https://github.com/owent-utils/c-cpp/blob/master/include/MemPool/IdxShmMemType.hc-cpp/IdxShmMemTypeKV.h at master · owent-utils/c-cpp · GitHubc-cpp/StaticShmAllocator.h at master · owent-utils/c-cpp · GitHub比如这个就是当时用tsf4g的时候拿来存对象的,创建的时候写索引,进程重启的时候重新挂载索引。共享内存放数据还有一个比较麻烦的是,如果对象内存布局变化了,你必须能够检测到并且正确升级,这点很重要。所以我的建议是,除非你要写大型MMO游戏,否则用共享内存绝对性价比太低呀。最后说一下我们目前的做法吧。就是更新服务器的时候部署另一组,然后部署完后把路由切过去,最后通知所有老服务器的玩家断线重连。就完事儿了,部署的时候慢就慢一点呗。不过我们对不同类型的缓存有优化,会控制到它并不太大就是了。

  • willPost author

    用共享内存的话还有一个问题是防掉档,要么你在开一个CacheServer,本质是一样的,就是通过另一个进程维护内存,然而CacheServer也一样可能出问题,腾讯很多MMO支持了服务器down掉直接拉起恢复模式,主要麻烦的地方感觉在这里,否则STL完全和共享内存并存,无非原来直接存指针的地方我存索引偏移值再到共享内存多索引一次罢了
    赞回复踩举报
    Excalibur
    Excalibur (提问者) 1 年前
    非常感谢回答.我有两个疑问:
    1.按照你说的更新部署的方式,假设正在运行的服务器是A,新服务器是B.请问如何确保在B加载数据时A这时改变的数据能同步给B呢?莫非中间还有一层服务在这时同步两边的数据么?
    2.你们缓存服务大多都是同步存取的么?比如一个逻辑里要获取很多玩家的很多不同种类的数据,似乎异步存取的话会有非常多问题.
    赞回复踩举报
    欧文韬
    欧文韬 (作者) 回复Ultramanhu1 年前
    这就不是用stl了啊,你得造一遍STL的轮子啊。STL是不支持解引用时加偏移的
    赞查看对话回复踩举报
    欧文韬
    欧文韬 (作者) 回复Excalibur (提问者) 1 年前
    1. 这种数据有两种,一种是A直接转移给B。这种操作是原子的,不会存在问题,详情你可以看下redis cluster的resharding文档,原理是一样的。另一种就是A存到缓存,比如redis然后下线,B再去拉取,中间是CAS操作,和踢用户下线然后在另一处登入是一样的
    2. 既然是缓存,那必然有更新延时的,对只读缓存是允许短时间的落后数据的。而需要写的缓存,我们会控制在只有一处写,并且是CAS操作。

    所有操作都是纯异步,我们没有任何同步操作。
    赞查看对话回复踩举报
    Excalibur
    Excalibur (提问者) 回复欧文韬 (作者) 1 年前
    夜猫握爪.
    问题1意思明白了,非常感谢.
    问题2可能我们对缓存的定义上有歧义误会.我理解上的缓存应该是不能有延时的..比如玩家1对玩家2施放了一个技能导致玩家损失了一些东西,如果缓存有延时没有存完的话这时再读取使用这个数据不是就不对了么..还是说你所说的缓存层前面还有一个直接使用的数据,在需要修改的时候是先修改本地的数据再修改缓存的?
    赞查看对话回复踩举报
    Ultramanhu
    Ultramanhu回复欧文韬 (作者) 1 年前
    不需要重造啊,你可以在逻辑层做多查询一次啊
    赞查看对话回复踩举报
    Ultramanhu
    Ultramanhu回复Excalibur (提问者) 1 年前
    你说的这种场景是放在内存中,技能系统读取的数据应该是直接内存里可以读取的,缓存某种意义上是存需要入DB的工具,技能数据重启之后没有一般问题不大,你问题2的场景数据并不是都应该写入缓存的
    赞查看对话回复踩举报
    Ultramanhu
    Ultramanhu回复欧文韬 (作者) 1 年前
    反对A直接转移给B的方法,这个会导致同级别Server耦合度过高,服务器通信会变成网状,非常不利于运营(例如按顺序停服务器),赞同方法2
    赞查看对话回复踩举报
    欧文韬
    欧文韬 (作者) 回复Ultramanhu1 年前
    A直接转移给B的一般是同类型的Server,完全是故障转移或者数据转移。背后可以再走消息系统,不一定要建立网状的服务器的。可以有很多办法做网络结构收敛。tsf4g的tbusd就是其中一种方式不是?或者可以中间架设一个消息服务。上Apache Kafka或者 GitHub – antirez/disque: Disque is a distributed message broker 具体怎么选可以根据具体的量级,是否需要落地,是否能容忍丢失等等。
    赞查看对话回复踩举报
    Ultramanhu
    Ultramanhu回复欧文韬 (作者) 1 年前
    我是说同级别的Server啊,但是数据转移不应该这么发生,要做肯定是可以做到的,我的意思就是这么设计不好,远不如第二种好
    赞查看对话回复踩举报
    欧文韬
    欧文韬 (作者) 回复Ultramanhu1 年前
    那也可以有办法收敛啊,咱的libatbus就不会成网状(* ^_^ *)。其实第二种方式和走外部消息系统是一样的,只是顺带多了缓存这个功能。
    赞查看对话回复踩举报
    Ultramanhu
    Ultramanhu回复欧文韬 (作者) 1 年前
    这跟你用什么bus系统无关啊,不利于Server本身的分层设计罢了
    赞查看对话回复踩举报
    欧文韬
    欧文韬 (作者) 回复Ultramanhu1 年前
    所以说要看具体什么服务的,异步操作越多,容错的分支就越多,有一些情况走外部缓存的CAS的反而比直接转移过去要复杂得多。
    赞查看对话回复踩举报
    yinzx
    yinzx回复Excalibur (提问者) 1 年前
    你说的这种情况,比如一些pvp模式,是玩家匹配到之后,系统根据负载均衡分配一个对战server,玩家都连在同一个pvpsvr上,技能释放和位移都通过udp同步消息由pvpsvr中转到对方玩家
    赞查看对话回复踩举报

发表回复