游戏后台数据存储解决方案
多人在线游戏是一种强数据、强逻辑的系统,这种系统存在大量的有状态数据需要同步存储。对于多人在线游戏这类复杂的系统,我们面对的是:
(1)业务逻辑系统的演化过程会转化并最终体现为系统数据的变化,同时系统数据的变化又会影响业务逻辑系统的演算过程。
(2)业务逻辑需求的增加相当频繁,在运营过程中极有可能对数据系统的数据结构进行变更,若数据系统的没有很好的可扩展性,将给业务逻辑的实现及数据的无损变更同步造成极大不便。
(3)业务逻辑对系统性能,并发要求很高,这就要求数据系统也要有良好的性能的支撑,能支持数据的高速同步。
本文档将说明在分区分服以及全区全服的游戏在数据存储方面遇到的挑战、难点以及解决方案。
1. 分区分服
在分区分服游戏类型中,我们一般采用下面的架构模型。
这种构架模型一般适用于基于TCP长连接的MMO RPG游戏。这种游戏的数据是有状态的,需要实时同步,以防止数据丢失。在这种情形下,若数据系统和逻辑系统粗糙的糅合在一起,将会增加系统的复杂性。同时游戏上线仅仅是开始,运营才是关键。对于数据的存储而言,因为采用共享内存,性能几乎不是一个主要要解决的问题,关键的问题是要解决扩展性的问题。
(1) 需求变更
运营过程中,需求变更、新增逻辑功能和玩法是很正常的 ,逻辑的变更必然会影响到游戏数据的变更
(2) 数据扩容
玩家上限、各种功能玩法(家族,帮派,邮件,市场等等)逻辑数据容量上限有可能随运营而爆满。
针对分区分服的游戏,设计了一种可同步的二维内存数据缓存阵列的实现方法及系统,使系统数据从业务逻辑中分离出来,从而简化业务逻辑开发复杂度。支持数据的纵向和横向维度扩展,支持数据的高速同步。
其中,每个子块称为一个chunk。逻辑数据块N中的chunk Nn表示逻辑数据块N有n块chunk数据子块。二维数据坐标(N,n)表示第N块逻辑数据块中的数据子块chunk n,记为chunk(N,n)。
系统各个组成部分的功能如下:
(1)共享内存数据缓存阵列
采用shm方式的共享内存,也可使用其他方式的共享内存,如mmap。共享内存数据阵列存放当前系统中的所有有状态数据。数据阵列由业务逻辑或数据同步代理进程进行初始化,这取决于哪一个进程先启动,一个进程进行数据阵列的初始化,另一个进程只需将阵列内存空间映射到自身进程内存空间即可。内存阵列首次初始化完成后,内存阵列的所有数据块处于空闲状态。在系统不断电,内存不被手动清除或破坏的情况下,数据阵列中数据将保持持续性和独立性。
阵列中的逻辑数据块的逻辑功能由业务逻辑决定,阵列本身不关注数据的逻辑。逻辑数据块分为动态数据块和静态数据块,逻辑数据块为动态数据或者静态数据块由配置策略决定。
动态数据块:数据动态加载、释放、同步,数据内存空间可循环使用。比如业务逻辑中的游戏玩家角色数据,角色的上下线就涉及数据的同步拉取和存储。
静态数据块:一次加载完成后始终不释放,常驻内存。这类数据采用定时机制进行数据同步,比如游戏中的市场交易大厅数据。
数据阵列的逻辑数据块的个数及大小由配置策略决定,可动态扩展。
每一个逻辑数据块N对应关系数据库中的一张表Table N。
每一个逻辑数据块子块chunk(N,n)对应关系数据库表中的一条记录Table(N,n)。
(2) 消息队列
负责存放业务逻辑发往数据同步代理进程的数据操作同步命令消息。包含两类消息:
a数据加载:通知从key-value系统中拉取数据,并置入本地数据阵列中。
b.数据释放:通知从本地数据阵列存储数据到key-value系统中,并释放在本地数据阵列的内存空间。
消息队列中的消息体包含操作类型(加载或释放),数据的key以及数据阵列中坐标(N,n)等信息,使数据同步代理进程获知要对哪一个数据块chunk(N,n)进行何种操作。
(3)业务逻辑
负责相关业务逻辑的处理,对数据的处理直接操作共享内存数据缓存阵列。系统启动时,内存阵列初始化完成后,业务逻辑提供数据初始化接口对内存阵列中逻辑数据进行初始化。运行过程中,通过数据操作接口直接对内存数据缓存阵列中的数据进行同步操作。
(4)数据同步代理进程
负责对共享内存数据缓存阵列进行数据同步管理。
a.处理业务逻辑的相关数据操作同步请求,比如数据往DCache写,并释放共享内存数据块;从DCache拉取数据,装载进共享内存数据缓存阵列。每个消息队列的请求都会由同步代理进程主线程发往进程中线程池的一个线程进行数据的加载或存储释放。
b.定时同步备份共享内存缓存数据阵列中的数据。
c.操作的数据都是结构化的二进制数据。
d.为提高数据的同步效率,数据的同步面对的是key-value系统,不直接操作数据库。在性能允许的情况下,也可直接操作数据库。
(5) 数据操作接口
两种数据操作接口:
a.数据块chunk申请绑定。当业务逻辑需要加载某个数据时,调用数据绑定接口。绑定接口从空闲数据块队列中分配一块空闲数据块chunk(N,n),并通过消息队列发送数据加载操作消息通知数据同步代理进程进行同步加载。数据加载完成后异步通知业务逻辑。
b.数据块chunk申请释放。当业务逻辑需要存储某个数据并释放数据空间时,调用数据释放接口,并通过消息队列发送数据释放操作消息通知数据同步代理进程进行同步存储。数据接口定时检测数据是否释放完毕,释放完毕后将chunk(N,n)加入空闲数据块队列,等待再次被分配绑定。
(6)配置策略
系统的配置策略。通过配置策略控制,支持数据的纵向和横向维度扩展,数据超时控制。
纵向扩展:逻辑数据块子块的大小变更;逻辑数据块子块容量的增减变更。
横向扩展:逻辑数据块数量的增减变更。
数据同步超时:通过配置数据同步超时时间实现。超时后,数据操作接口会返回业务超时状态码。
(7)Key-value系统
在游戏后台的数据系统中,一般采用block数据块的存储方式,为获得更好的数据访问性能,采用key-value的数据存储方式是不错的选择。在公司内部已经有成熟的key-value解决方案,cmem,tcaplus及Dcahe, 也有一些外部开源的组件比如memcache、redis、Tokyo Cabine等等。这些组件的性能参数和适用条件在KM上都有相关文章介绍。
Key-value组件系统向数据同步管理进程提供数据同步接口,同时定时将key-value内部的缓存数据同步到数据库做持久化,实现内存中的数据阵列持久化。内存数据阵列和数据库持久化表的映射关系如图所示:
业务逻辑里面的一个逻辑数据结构作为一条记录存储在数据库DB的表里面,存储模块使用一块固定大小的共享内存来缓存固定条数的记录在本地内存阵列里面,并且每条记录的大小是固定的,与逻辑数据结构大小对应。共享内存被划分为配置的n个chunk子块,一个chunk存放数据库表里面对应的一条记录。在chunk的前面有个数据头,数据阵列的逻辑数据块结构如图所示:
每个逻辑数据块的前4个字节存放每个数据块的大小。数据的头结构如下:
header
{
Key 数据对应的key,访问key-value缓存时使用
Ver 数据版本号
Time 修改时间
Status 数据的状态
}
每一个chunk有6种数据状态:
1.空闲(free):待分配
2.加载(loding):数据拉取加载中
3.正常(normal):数据加载完毕
4.修改(modify):chunk数据被修改
5.释放(remove):chunk数据同步并释放中
6.定时释放(tick_remove):数据chunk remove失败后,chunk置为本状态。通过定时控制,定时释放数据,直到remove成功。
Chunk 数据的状态变化如图:
当一个chunk分配出去时,就修改key,ver,time,status相应的chunk数据头部;chun
k被释放时就清除头部数据。当一个chunk被分配出去时,该chunk不会再次分配,除非该chunk被释放并且为free状态。
数据的动态同步和定时同步流程:
(1)动态数据的同步都异步进行,通过发送同步消息通知,并定时查询chunk头结构的标志来判断数据是否加载成功。
存储进程通过独立线程,定时轮询共享内存每个chunk的头部状态字段,对状态为修改(modify)的chunk数据存盘后设置为正常(normal)状态。对释放(remove)状态的chunk数据持久化成功后,初始化chunk的头结构,释放该chunk。当chunk数据有变化时,需要业务逻辑进程修改chunk头结构,设置chunk的头部状态为修改(modify)。
2. 全区全服
全区全服架构主要适用于社交策略型游戏,玩法上较轻度,注重离线型玩法,核心玩法集中在玩家间的异步互动交互。全区全服区别于分区分服一个明显特征就是数据存储模型上,如下图:
(1)对于分区分服,游戏分区的数据按分区隔离独立,互不影响,同时游戏的数据会load到各游戏分区的共享内存;后台由心跳和玩家的请求驱动, 心跳和请求处理的数据在本地都有cache。
(2)全区全服没有数据分区,gamesvr可平行扩展,所有的gamesvr共享一份DB数据;由玩家的请求驱动,玩家每一次请求都涉及到跨机器的数据访问。相对于分区分服,全区全服对数据的并发访问、安全和一致性带来了挑战。数据访问成了整个系统的性能瓶颈。
全区全服对数据的操作都基于本地cache,采用单进程毫无鸭梨;而分区分服需要实时的跨机器数据操作(每次数据访问耗时3ms),单进程的架构显然不能满足要求,需要对单进程架构进行优化改造,如下图:
原分区分服的逻辑主线程成为消息分发线程,同时游戏处理逻辑优化为多线程,单个消息的逻辑处理由单个线程完成,从而大大提高消息处理的并发能力;同时在数据访问方面做了优化,如下图:
(1) 分区分服由玩法决定一般的数据访问是读多写少,因此采用读写分离的策略
(2) 根据功能玩法将数据划分为互动数据和非互动数据。互动数据就是玩家之间可以互相修改的数据,非互动数据就是只有玩家自身能修改的数据。
(3) 同时在DB proxy层采用多线程读的方案,以提高数据并发读的处理能力
(4) 对数据进行串行化,根据UIN进行hash,单进程写入,避免数据冲突导致数据不一,对于互动数据,DBPROXY有逻辑计算功能,避免数据多点修改。
游戏的数据存储可以说是整个游戏后台架构的基础性工作之一,在充分分析游戏的玩法,功能和运营需求、采用合理的数据存储的模式将对后续的功能开发产生积极影响。