目录
前言
我所理解的服务器架构
什么是否部署架构
部署架构的职责
进程业务职责
网络链接及通讯方式
与客户端的连接方式
服务器之间连接关系
数据落地以及一致性
数据库的选择
数据访问三级缓存
数据分片
读写分离
分布式数据处理
负载均衡
热更新
配置更新
合服
合服的痛点
解决合服数据重叠问题
总结:
前言
本人做游戏多年,当时从开发棋牌游戏开始阴差阳错进入游戏行业,服务器架构做过很多套,我曾经的小伙伴拿着我设计的架构带到新的公司,都有一个不错的表现。但是发现一个现象,后面设计的架构永远比前面设计的更为优秀或良好,也许随着时间的推移,个人技术以及新技术与理念的引入、新的业务场景等多种原因,让我们不得不对架构做一定的调整。
我经常问我的求职者一个问题,你怎么理解服务器架构,架构两个词从字面上理解很好理解,如果对于一个系统工程了说,它可以理解为系统工程的顶层设计,如果是一个简单的单一功能,那它是单一个功能总体设计思路。 但是我们今天所讲的架构是一个承载百万人同时在线的架构设计,注定它不是一个简单问题。它包含着技术开发人员所需要掌握的技术需要的广阔性和深度,并拥有强力的设计理念。
我所理解的服务器架构
服务器架构我的理解为三个方面,它包含部署架构和逻辑底层架构、业务架构。
什么是否部署架构
部署架构是在考虑业务职责和各个场景的节点隶属关系、连接关系以及业务分割的服务器的概括性架构,它确定了服务器进程之间的部署关系。
部署架构的职责
在这一层,我们应该抛开编程语言的限制,考虑进程业务职责,网络连接形式、数据落地以及一致性问题、扩展性问题、负载均衡问题、稳定性问题、热更新问题等
进程业务职责
进程业务职责我们应该考虑,根据业务进行功能拆分,并确定进程应该做什么和它主要功能是什么,是我们赋予各个进程具体职责。
以游戏服务器为例,它应该需要为用户做登录认证的登录服;职责为统一网络连接和安全的网关服;为任务和养成系统等玩家具体玩法需要的游戏服;担负着全服排行榜和ID分配等全局功能功能的世界服;需要好友、聊天 等功能的社交服;需要保存数据的数据库服等
网络链接及通讯方式
与客户端的连接方式
当我们确定了业务职责时,那么用什么样的通讯方式就是我们具体要考虑的问题。首先我们
需要思考一个问题,我们的业务那些进程需要与玩家进行连接呢?在游戏中一般,登陆服和
网关服是需要与玩家进行连接。
登陆服往往通过http的协议进行验证登陆,让然也可以选择TCP方式,以及后期发展的
websocket的方式。websocket我们可以看着是http的变种,其实它最核心的还是http。
当然根据实际业务场景有可能选择udp。帧同步的战斗服就是可确认的rudp协议方式。
服务器之间连接关系
在服务器部署架构中,我们要确认的一项重要内容之一就是多个服务器进程之间能够直接通
讯。某个业务进程是被动连接还是主动连接?是一个连接还是多个连接?是一对多的关系还
是一对一的关系?
在连接关系中,我们同时要考虑服务器进程之间用何种方式进行通讯。我最近设计的一套服务
器架构,通过zookeeper进行服务器配置更新,每个服务器有自己的一个服务器节点ID,服务
器节点ID通过zookeeper获得自己配置信息,同时将本节点注册到zookeeper上,其他节点监听
到某个节点注册信息,zookeeper按照配置规则进行节点自动连接,一切做到自动化。
在数据库的设计方式,我将数据功能设计成SDK的方式,数据库节点是作为server端,而使用
数据库的为Client端,任何节点想要数据库的操作直接使用数据库SDK就可以,非常方便。
数据落地以及一致性
数据库的选择
对于业务,我们应该充分分析它的属性以及业务当量,根据业务属性和当量我们选择合适我们自己需要的数据库类型,通用的数据库有mysql、orcale、mongoDB
但们也有各自的缺点,MySQL、Oracle和MongoDB各自的缺点如下:
- MySQL:功能相对较少,并发控制能力有限,数据安全性较低。
- Oracle:劣势在于成本较高,不是开源数据库,需要购买许可证才能使用。
- MongoDB:不适合存储关系型数据,数据一致性相对较弱,且没有类似SQL的查询语言,查询功能相对较弱
对于游戏开发,游戏本身的数据库我可能选择mongodb,因为完美的将player玩家对象整个丢进我们的数据库中,简单并且高效实现。当然在window系统下,你也可以选择sqlServer,但是发它只能在window操作系统下运行。
数据访问三级缓存
在早期的数据访问和存储是进程直接操作数据库的。由于数据库数据往往保存在磁盘中,存储和访问效率就往往是非常低,尤其是访问量比较大时容易造成拥堵。
所以后来为了提高访问效率,在用户个人数据方面,为了提高访问数据的效率可能我们需要用到三级缓存机制,即:memory、redis/memcache、db数据落地三层设计。
对于热点数据,首先我们从memory中查找,没有在查找redis或memcache,最后才查找database。
数据分片
如果一个业务逻辑都在某个进程中,那么数据一致性相对就简单了许多。那么更多的考虑的是因为单点会不会出现执行效率和性能的问题以及如何提高承载上线,可是问题并没有那么简单。
例如:游戏服的社交往往处理好友、聊天、帮会等通用数据,如果在拥有天量用户的游戏中,我们不可能一个服务器节点保存所有的这些数据。可能进行数据的在进程的分片,数据如何分片就需要我们做一定的考量。同样我们的角色数据不可能只有一个数据库,可能需要架设数据库服务器集群来做处理,同样表格数据也需要分片等等,让访问数据更为高效。
分片算法:
读写分离
当然我们不可能所有数据都进行分片,比如拍卖行数据以及处理。拍卖行数据这个系统的特性是用户基数达到一定量及的时候,读数据的压力比改数据的压力会大很多,在设计初期我们可能需要考虑读写分离。
读写分离适用于程序使用数据较多,而更新较少、查询较多的情况。此时,设计主从主从同步,可以减少数据库压力,提高性能。
拍卖行或者商行,更新相对较小,访问量较大,当我们在全局服更新数据时,同步更新到其他服,玩家访问拍卖行或者商行时,访问的是本服的即可,这样减少了全局服的压力。
数据库数据的IO(分库分表)
在服务器性能瓶颈中往往数据库的IO会拖累整个优秀的程序设计。
以MySQL为例:数据库中的表超过1000000条记录时,效率会受到多种因素的影响。以下是一些可能影响数据库效率的关键因素:
硬件性能:
- 磁盘I/O:SSD比传统的HDD更快。
- 内存:足够的RAM可以确保更多的数据和索引被缓存在内存中。
- CPU:强大的CPU可以更快地处理查询。
数据库设计:
- 表结构:正确的数据类型、适当的索引和分区可以提高查询性能。
- 索引:为经常用于查询条件的列创建索引。
- 规范化:避免不必要的数据重复。
查询优化:
- 查询语句:避免使用SELECT *,只选择需要的列。
- 避免全表扫描:使用WHERE子句和索引来限制查询的数据量。
- 连接优化:尽量减少表之间的连接,特别是当连接条件没有索引时。
数据库配置:
- 缓冲池大小:例如,InnoDB的缓冲池大小应足够大,以容纳大部分的数据和索引。
- 日志和二进制日志设置:这些设置可以影响写入的性能。
并发性:
- 高并发读写可能会导致性能下降。
- 使用连接池可以减少创建和关闭连接的开销。
数据分布:
- 如果数据分布不均,某些查询可能会更慢。
网络延迟:
- 如果应用程序和数据库不在同一台机器上,网络延迟也可能成为问题。
了解了更多的数据库引起瓶颈问题,除了设计修改硬件和配置属性时,我们应该避免大的表格的出现;同时,尽可能避免只有一个全局数据库的可能性。
所以应当对于大表格数据进行分表,对于一个数据无法承载的应当通过集群和分库的方式解决数据IO的瓶颈。
分布式数据处理
服务器数据的落地不应该仅仅考虑当前问题,而要考虑后期扩容或随着时间的变化服务器减少问题。不应该只考虑本线程问题而考虑跨线程问题;只考虑本进程而不考虑跨进程问题。
在实际运用中,有可能有多个进程进行同样的逻辑处理,只是处理不同的数据而已。例如:社交服处理的都是社交相关的逻辑的数据,但是一个玩家的涉及需要修改的数据只能在单个社交服处理,不能两个社交服都可以修改一个玩家的同一份数据。
一句话总结,对于数据落地,我们尽可能让数据安全地保存,同一份数据的修改同时有且只有一个地方进行修改,并用一切手段提高访问和落地的效率。
负载均衡
负载均衡是决定了游戏稳定和承载上线的总要环节,作为服务器后台开发,那一定要知道我们的瓶颈在哪里,要知道单个进程的承载和单个服的承载,要对服务器前期要有个预设值,通过这个这个预设值来进行总体的实际。服务器的瓶颈通用的有网络IO,数据库IO、内存、CPU性能等问题
我们不能实现一个服务器只有单一节点的运用。如果有,那么它的体量也大不到哪里去。所以我们要知道如何进行负载均衡,如何对服务器进行横向分布,如果在游戏上线前期没有做充分的准备,出现问题都有可能是对项目致命的问题。我们不仅仅考虑正确的情况下,同时我们要对外部危险攻击做好相应的准备。
在这里分享两个例子:
第一个案例:
有一天我曾经下面的一个小伙伴去其他公司负责整个地方棋牌项目。棋牌项目的特点就是被ddos攻击的重灾区,同样类型的游戏,市场就那么大,你抢别人的蛋糕,不有点硬技术可不行。当时小伙伴打电话给我,问怎么解决?最终我发现它的登录和网关是没有做负载均衡的。对方一直打他们的login服务器和网关,登录和网关它们没有做负载均衡,这种情况只能先上高防,后期把负载均衡加上,出现类似的情况,将新玩家导入到新的服务器,让ddos攻击有高防的机器,这样影响就小了很多。当然还有很多技术细节,这里就不一一列举了。由于小伙伴在这块考虑不足,负载均衡没有加上,最后这个项目刚开始一周就胎死腹中。
第二个案例:
我面试过应聘我们后端小主程的一个小朋友,他们公司做了一个大IP项目,腾讯发行他们家的游戏,社交、工会、跨服战等功能都在单个全局服,最后发现就是这个单个全局服成了整个游戏服务器的败笔。腾讯当天导量300多万,直接将服务器整崩溃了。腾讯推的量一般是短时间巨量,当导入巨量项目组接不住时,腾讯不会再给你导入多少量了,这个项目实在可惜。
以两个失败的案例告诉我们,一个差的架构可以间接杀死一个好的项目,服务器架构的最大体现往往在游戏上线的那刻尤为重要。负载均衡没有做好,可能造成项目不可挽回的损失,给我们巨大的流量和推广也无法接住,着实可惜。
为了实现负载均衡,我们可以通过nginx、动态服务器列表、设计上扩容组合实现。在登录时,一定要设计排队功能,当有大当量用户突然访问时,最起码有一个保底机制不至于出现登录挤兑情况影响。
Nginx(engine x)是基于 C 语言实现的一个高性能、轻量级的 HTTP 和反向代理 Web 服务器,同时也提供 IMAP/POP3/SMT服务。
Nginx 既可用作静态服务器,提供图片、视频服务,也可用作反向代理或负载均衡服务器。Nginx 作为反向代理,当代理后端应用集群时,需要进行负载均衡。
Nginx 提供了对上游服务器(真实业务逻辑访问的服务器)的负载均衡、故障转移、失败重试、容错、健康检查等功能,以一种廉价有效透明的方法扩展了网络设备和服务器的带宽、增加吞吐量、加强网络数据处理能力、提高网络的灵活性和可用性。
Nginx 具有高并发连接、低内存消耗、低成本、配置简单灵活、支持热部署、稳定性高、可扩展性好等优点,这些优点都得益于其优秀的架构设计(模块化、多进程和多路I/O复用模型)。
热更新
服务器热更新是指在不停机或者不关服的前提下,对服务器上的应用程序或系统进行更新。它的好处在于能够保证系统的高可用性,避免了系统在更新过程中的不可用现象,并且将系统升级的周期缩短到了最小。
实践证明,每次重启服务器,都有可能一定概率造成玩家的流失,现如今获取一个付费用户是多么的不容易。更新可以分为,配置更新、代码更新、合服等
配置更新
配置更新是最频繁的更新,极限情况一天可能都要更新好几次,如果每次都要重新启服来进行更新,损失太大了。频繁的启服对用户的体验很不友好,同时让人不敢在游戏中消费。
所以配置的更新一定是热更新实现。当配置需要更新的时候,可以通过GM命令通知各个服务器,进行配置的重新装载。
代码热更新
对于代码,并不是所有代码都支持热更新的,对于Java它所支持的热更程度为代码的函数和成员变量都不能改变,但是函数内部的实现能够变化。如果是代码C++和Lua的结合,能够比较友好的实现代码热更新。所以代码的更新也取决与所选择的逻辑编程语言。
对于语言的限制技术上往往是有无力感的,因为对于技术的我们没有办法做什么。但是我有一种策略是可以将影响做到最小,当我们需要更新代码的时候,可以将现有玩家快速切换到更新好了的服务器,同时将当前服务器关闭进行更新,等当前服务器更新成功后再让新玩家进入当前服务器。当然这种情况玩家并不会无感,但起码影响小了很多。
合服
合服的痛点
肯定奇怪,合服为什么属于热更新范围内。其实合服也是更新的一部分。
合服通用做法是将需要合服的服务器关闭,同时进行数据库合并。但是并没有那么简单,因为1服和2服的数据往往不相通的,那么某些ID作为数据Index(索引)就有可能重叠问题。
比如:玩家数据库表格以playerId作为index(索引),采用自增的方式,1服如此,2服如此,自然就会有重叠的地方。而且之中情况是连锁反应的,因为玩家的道具、任务表格都是以playerId作为关键index(索引),这就一一去重,非常麻烦,即使我们客户通过工具来实现,实际起来非常的费劲,总有特例需要处理。
解决合服数据重叠问题
能够有一个长效机制让我们合服不怎么麻烦呢?答案肯定是有的,ID的唯一性其实给了启发。如果有一个机制能够让ID在不同进程之间产生不同无法重叠的ID,那么是否不是就可以解决ID重用问题?
是的,我们可以参考雪花算法。雪花算法生成的ID为64位整数,具体的格式如下:
0 | 0000000000 0000000000 0000000000 000000000 | 00000 | 00000 | 000000000000
其中,第1位为符号位,固定为0;接下来的41位为时间戳(毫秒级),记录了生成ID的时间;然后是10位的机器ID,5位数据中心ID和5位工作机器ID,用于标识不同的机器;最后是12位的序列号,用于表示在同一毫秒内生成的多个ID的顺序。
实际使用时,每台机器需要配置一个唯一的机器ID,以保证生成的ID不与其他机器生成的ID重复。同时,需要注意时钟回拨的问题,即当本地时钟发生回拨时,可能会导致生成的ID出现重复或者乱序的情况。
当然这只是抛砖引玉,还有其它类似算法,这里就不一一详举了。
我们在给角色命名或修改名字的时候,同样会出现类似的重叠问题。这以问题的解决就有待于读者去思考了。
总结:
在部署架构阶段,我们考虑的是我们架构设计的愿景,或者说是我们向往架构的设计的蓝图。告诉我们将要做什么,确定我们最重要的目标和方向,划分职责。犹如宪法在法典中的意义一样重要,它是指导思想。接下了的逻辑顶层架构设计以及业务架构设计都要依照部署架构的设计而进行落地。
我们应该抛开语言而考虑每个进程需要做什么,它们之间的连接关系,数据落地和分割,瓶颈问题的解决,这样从全局上考虑问题,思路就非常清晰了。