弹指一挥间从事游戏相关的开发工作已经十多年。在开发了六年多几经波折差点放弃的 starrydb.com项目也迎来了1.0版本的上线。虽然演示版本以及集群功能早在17年1月份就 开发完毕但修改和完善计划目前仍然排的满满当当,导致1.0上线一推再推至今仍然不甚满 意。 说起最初开发这个项目的初衷就像那盏迷雾中的绿光让人兴奋不已,在此也希望因为我 的执著而被伤害的人幸福。以下引至《thegreat gatsby》 “Gatsby believed in the green light, the orgastic future that year byyear recedes before us.It eluded us then, but that's no matter--tomorrow we will run faster, stretch outour arms farther.... And one fine morning---- So we beat on, boats against thecurrent, borne back ceaselessly into the past.” 或许人们还没有意识到,游戏服务器将是继计算机操作系统之后,现代软件工程中最复 杂的计算系统。经过wow和无数游戏公司的崛起让这个系统得到了飞速发展。最初的游戏服 务器引擎可以追述到1992年的mudos。这是一个简单的文字对话系统,但可以多人在线。 因为交互简单对性能要求不高,普通的服务器就可以支持上千人同时在线。在接下来的很多 年时间内,mudos代表着游戏服务器引擎的经典原形。我们可以把这类服务器引擎统称为经 典游戏服务器引擎。 经典的游戏服务器引擎的核心构建就逻辑服务器,所谓逻辑服务器就是对用户的输入数 据进行处理,产生输出数据返回给用户。见图1 因为游戏逻辑服务器的崩溃问题和内存使用量不稳定,出现服务器不稳定进而导致数据 丢失。服务器急需一个稳定的硬盘存储服务。MySQL出现在了逻辑服务器后面替代逻辑服务 器的硬盘。见下图2 
因为mysql数据库写入和读取得速度不快,数据操作需要等待时间的过长,在硬盘数据 库之前又加入了内存数据库作为快速读取的缓存服务器。
也许你看出了其中的问题,为了解决逻辑服务器不稳定丢失数据的问题,引入了缓存服 务器和数据存储服务器,但增加了系统的复杂性,导致开发难度增加进而导致逻辑服务器更 加复杂更加容易崩溃。人们花了很长的时间和想了很多的办法提高逻辑服务器的稳定性。见 下图3 
因为服务器崩溃的主要原因是内存指针的泄漏,为了解决崩溃的问题产生了各种脚本语 言替代指针型语言。可服务器功能越来越多,越来越复杂,崩溃,卡死,内存泄漏像逻辑 服务器上空的阴云挥之不去。 服务器第一要求是稳定,但仍然不能阻止人们对服务器功能无尽的渴求。于是产生了新 的想法拆分逻辑服务器扩展服务器承载能力。见下图4
到这里经典游戏服务器发展到了巅峰,其数据处理的复杂度远高于同期的任何服务器系 统。 为何说其处理数据的复杂度高于同期的任何服务器系统,因为同期的服务器系统都是服 务于现实生活,而游戏服务器是服务于纯粹的虚拟世界。例如说邮寄物品这个事情,在游戏 服务器就是一个指令,也许在聊天过程,也许在战斗过程。没有现实中邮寄的等待过程,需 要游戏服务器立刻就返回。并且可以发生在任何功能服务器上。例如聊天的过程中给对方一 个物品,这在现实中是绝对不可能发生的。 因为功能和功能之间不是绝对的独立,经典的服务器系统随着功能的划分越来越细,每 个服务器之间的通信就越来越复杂。这样让人联想到了复杂低效的官僚系统。有大量的请求 都浪费在了通信和复杂的沟通上,而且每个开发人员要小心奕奕,不知道哪个逻辑会搭错崩 溃掉。 在六年前的一个夜晚,看着眼前这越来越庞大复杂的怪兽,我像极了唐吉坷德向着风车 挥舞着长矛。在某一瞬间一个想法进入大脑,为什么不能一个服务器就把一个玩家的所有请 求都处理好呢?这样就像给每个玩家配备了一个专职的秘书,任何需求都交给秘书去做,涉 及和其他玩家沟通的事情,秘书会找到其他玩家的秘书进行沟通。见下图5

以购买物品为例 1, 玩家A向服务器C发送购买物品的请求,服务器C扣除玩家A的货币; 2, 服务器C将购买的请求发送给服务器D; 3, 服务器D扣除玩家B的物品并添加货币并返回结果给玩家B; 4, 返回交易结果给玩A;
这个设计模式称为“one object do something”,这个设计模式第一个好处就是降低了软件开发的难度。在按功能分割的服务器中,每个服务器上都是不同功能的代码。每个功能之间的联系是一种耦合关系。存在耦合关系的代码就存在不确定性的风险,每个功能看似单独修改都没有问题,但在一起互相配合就会出现各种不确定性。Starrydb的“one object do something”的模式中服务器C和服务器D使用代码是一样的。这种买卖关系无论是A向B购买还是B向A购买过程都是一样的。这样高内聚的工程在开发调试的过程中不容易出现错误。提高了开发速度和软件质量,降低了软件工程的不确定性。 Starrydb的“one objectdo something”的模式的另一个好处就是极大的方便服务器的扩展。因为每个服务器的配置都是一样的,当用户增加的时候只要添加新的服务器,就可以满足数据处理的需求。传统的经典服务器架构,按功能划分服务器结构的方式就会出现瓶颈,功能不能一直不断细分。见下图6
人类是社会动物,我们每个人都维系的着自己的社交圈子。每个星期给妈妈打电话,每年一次的家族聚会,晚上去酒吧认识新朋友,和老朋友去海边度假,在公司与客户谈判等等。如今社交的方式更是多种多样,通过网络和视频链接,我们更可以不受地域的限制。对于我们每个人来说,社交可以用如下几个方面来度量,联系人的总数量,每人的总次数,每次的时长,这三方面的乘积得到我们社交所投入总时间。假设我们在单位时间内投入的热情不变。那么我们在一定时间内投入到社交活动的热情是,人数*次数*时长*热情。 游戏服务器引擎也是一个小的虚拟社会,每个玩家都是这个社会的一份子。分布式的游戏服务器引擎的对象的性能也可以套用这个公式。某个对象访问其他对象(或被其他对象访问)的对象数量,每个对象的访问次数,每次访问的时常和cpu单位时间的运算能力。就可以推算出这个对象在集群中消耗的cpu运算能力。如果再乘以这个对象平均占用的内存数量就一个推算出一个对象在集群中总的消耗。 假定服务器每次处理的时长非常小并较为平均,cpu单位时间的运算能力也相对固定。那么每个对象的与其它对象的通信次数和通信对象的数量就成为决定服务集群承载能力的关键。 在不限制对象间通信的范围和每个对象通信的次数的极端情况下集群承载。服务器内玩家的数量为x,假设每个玩家都和剩余的所有玩家有社交关系,那么服务器每秒钟处理的消息数量为x²。假设服务器处理消息数量的上限为n,那么需要的服务器数量y=x²/n,承载的人数x=√(ny)。见下图7:
限制用户社交频率的情况下承载能力与cpu数量成正相关,直到到达单个cpu处理上 限。假设单个cpu处理上限是10人,服务器到10之后处理能力会陡然下降出现拒绝服务。 见下图8: 
限制用户社交频率的情况下承载能力与cpu数量成正相关,直到到达单个cpu处理上 限。假设单个cpu处理上限是10人,服务器到10之后处理能力会陡然下降出现拒绝服务。 见下图8:
在经典的游戏服务器引擎中,按功能划分的模块分别运行在不同的服务器上。这虽然也 可以称呼为分布式,但我们知道拆分不同功能到不同的硬件上运行,需要对原有软件系统进 行很大的改动。这种扩展不是线性的而是阶梯性的。见下图10:
系统承载能力的上限是硬件限制,随着系统功能的增加承载能力逐步下降。但每次提高 硬件上限就是拆分功能,都会让承载能力得到提高。随着硬件数量越来越多开发复杂度也越 来越高。
“Donot , for one repulse , give up the purpose that you resolved to effect .(WilliamShakespeare , British dramatist)”
我们看到想要通过添加服务器达到无限扩展必须要遵循基本的数学准则。限制单位时间 处理的人数或者减少数据交换的频次。这个结果虽然让人沮丧,但从另一个方面也让我们 看到分布式服务器无限扩展的可能性。到这里我们有了一个评价游戏服务器引擎的数学标准。 那么只要遵循这个标准写出一个稳定,高性能,可扩展的游戏服务器引擎就成为可能。 见下图11:
理想的游戏服务器引擎的承载能力和硬件性能成正相关,开发难度趋于平行。要达到这 样理想的状态,降低开发复杂度需要在三个层次上的保证。从下到上分别是稳定,高性能, 可扩展。见下图12 
稳定是游戏服务器引擎的基石。这个稳定的含义也是尽可能的减少耦合性,让计算运行 在最简单可靠的环境内。只有尽可能把数据的计算存储都放在同一台计算机上。减少每个计 算机节点间的数据交换。用最快的时间处理完每条数据请求。这就是数据和计算越近效率越 高系统越稳定。经典的游戏服务器引擎的数据被保存有三份。逻辑服务器一份,数据缓存一 份,硬盘数据库一份。在系统运行过程中确认数据一致性和完整性的工作就消耗了大半。而 且出现断线和宕机,很容易破坏数据的一致性和完整性。是潜在破坏服务器稳定性的因素。 高性能是服务器稳定之后追求的第二个目标。为了无限的接近硬件处理的上限,不浪费 硬件资源。这似乎和分布式系统是相互矛盾的,因为分布式系统就是要把数据和任务分配给 集群内的每台服务器,分配的过程必然带来损耗。但也不能转头把高性能寄托在优化开发库 上。因为我们知道服务器受限于cpu的处理能力。开发库中添加功能就会影响软件运行效率, 开发库分割功能就会增加数据复制成本。减少软件开发难度的根本还是加强内聚减少耦合, 减少工程的复杂度。 扩展性是游戏服务器引擎追求的最高目标,虽然数学准则告诉我们这样的追求对于分布 式系统有限制。但建立在线人数和功能的扩展与服务器硬件数量的线性关系还是非常的必要。 经典的游戏服务器引擎对地图服务器的扩展不能准确地称为建立了线性关系。因为玩家未必 会按开发者的意愿平均分配到每台地图服务器上。 在一个如此庞大的系统里对于数据的安全性又是如何保障的呢?见下图13 
能保障数据安全的并不是starrydb,starrydb只是一个系统功能的实现,对于数据安全 我认为任何系统都是不可靠的。Node可以宕机,link可断线只要假设硬件是不可靠的,那 么系统就是不可靠的。我们就要想办法用系统可靠的一方面去弥补不可靠的一方面。就目前 看真正能可靠的就是明确假定不可靠的前提下,在数据逻辑上做到互相检查互相校验。实现 操作数据逻辑的任务可重入可检查。 任务的可重入是检查数据完整性一致性的一种安全写法,可重入的意思是任务的相关数据是记录式的,例如存入1块钱,那么银行的记录是存入1块钱,当前余额为2块钱。对应就是两条记录,当前存入的数据,累加的数据。累加数据是为了方便读取。那么对应key-value就应当是3条数据。 Key: 任务号 ,value: 顺序id; 任务号是客户端生成顺序id是服务器生成的由小到大的顺序号,方便查询最后的任务。 Key :顺序id ,value: 加减值; 当前顺序id加减的数值。 Key :顺序id ,value :加减之后的总值; 根据上一个id计算出的总值方便查询。
可重入的意识是可以拿客户端的任务号重新检查当前任务是否已经产生顺序id是否已经写入正确的加减值,是否已经产生正确的总值,如果没有就重新产生记录。这样只要客户端的任务号不变同一个任务不会产生多次重复操作,保证数据的一致性和完整性。 如果任何情况断线导致任务中断,这个交易也会记录在册。 在用户界面就会显示这条交易失败。可以由发起方重新手动继续交易,直到交易完成。一个正常的交易有中断,失败,成功3种状态。 这个系统对崩溃的应对措施,如同是双向交易扣钱的同时获得物品,或则扣物品的同时获得钱。假设两个对象的数据都同一台计算机,这个计算机出现回滚。那么两个对象的交易数据同时消失,这笔交易就不存在。即不损失钱也不会被扣除物品。如果两个对象的数据分别在不同的计算机,其中一个计算机的运行失败。每个用户都是扣掉钱获得物品,或则扣掉物品得到钱,相当收支平衡。 对于单向交易的A对象扣钱,B对象获得钱。如果A所在服务器回滚到没有扣钱,那么B将凭空获得一笔钱。那么A在扣款之后要等待一段时间,服务器写入硬盘成功之后,这段时间估计10秒分钟到30秒。然后再发起给B对象获得金钱的操作。这样如果A没有将数据写入硬盘时,服务器宕机回滚数据,B也不会获得金钱。A可以重新发起流程完成汇款。 到这里我们有了一个分布式的,扁平开发复杂度的,可重入数据的,可线形扩展的游戏 服务器引擎。 |