您的位置  > 互联网

游戏服务端中的高并发和高可用的优化方法

文/水锋 本文首发于知乎

用通俗的话说一个好的服务器架构,最基本也是最重要的两点:支持百万玩家同时在线没有问题。 这两点分别对应高并发和高可用。

上一篇文章介绍了游戏服务器的一些优化方法:《》。

本文系统介绍了游戏服务器中的高并发和高可用。

高并发和高可用性是互补的任务。 当我们支持百万玩家同时在线却无法保证服务器的稳定可用性时,那么高并发支持就无从谈起; 而如果在玩家数量较多时服务器经常出现问题,那么就不能称为高可用性。

1 水平扩展

水平扩展是高并发、高可用的基础。 通过支持水平扩展,理论上我们可以通过添加机器来获得无限的负载能力,从而支持高并发。 在此基础上,如果一个进程出现异常,其他进程可以替代它。 它提供服务,从而实现高可用性。

例如下图,对于一种不支持水平扩展的架构,游戏服务器中只有一个战斗进程为所有玩家提供战斗服务。 这里有两个问题: 1、一个进程最多只能使用一台机器的计算资源。 性能是有上限的。 2、如果这个流程或者所在机器/网络出现异常,整个系统将变得不可用。

不支持水平扩展

水平扩展有两种常见的实现模型:

两种水平扩展模式

1.1 有状态与无状态

从进程内存中是否保存状态来看,服务可以分为有状态和无状态:

无状态服务本身不保存状态,因此如果进程崩溃,信息不会丢失。 另外,正如下面将要介绍的,无状态服务由于采用了随机分配的路由,对异常的容忍度更好,所以从高可用性的角度来看,无状态的更好。

但由于无状态不保存状态,所有状态操作都是数据库操作,这就导致开发成本更高(代码编写更复杂),对数据库的压力也更大。 因此,无状态并不适合所有服务。 一般对于简单的状态,对于明确的服务,可以先使用无状态服务,比如好友服务。

1.2 路由策略

对于有状态和无状态服务,它们使用的路由方法也不同。

对于无状态服务,一般采用随机分配的路由。 随机分配的路由方法有很大的好处。 如果某个进程崩溃或者网络出现故障,我们只需要把这个进程从路由中移除即可。 不会影响后续的请求,只会影响本进程的当前进程。 处理逻辑。

有状态服务的路由需要明确每个请求要被哪个进程处理,其他进程因为没有相关的状态信息而无法处理。 例如,对于上面提到的战斗服务,路由只能根据战斗ID将相关请求发送到对应战斗所在的进程来处理相关请求。 路由通常使用模或一致散列。 一般使用一致性哈希代替取模,以防止故障引起的抖动。

1.3 举个栗子

下图是某游戏架构模型的简化版。 真实的服务器比这复杂得多。 这主要是为了举例。

集群可以分为三类:支持水平扩展的有状态服务、支持水平扩展的无状态服务、不支持水平扩展的单点服务。

其中,支持有状态服务和无状态服务横向扩展的进程数量可以占到项目的90%,单点服务很少。 在这个架构中,我们游戏的负载限制的瓶颈就在于单点服务。 单点服务的逻辑比较简单,负载限制很高。 另外,支持水平扩展的服务进程出现异常只会影响该进程所服务的玩家,可用性较高。

有状态服务

在我们游戏的服务器集群中,大约三分之二的进程是处理玩家个人逻辑的进程(玩家集群,很多游戏项目都称为大厅服务器)。 每个进程通过将玩家分配到不同的玩家进程来处理玩家的一部分业务逻辑。

通过增加个人逻辑进程数可以增加服务器负载能力。 我们支持在不停止服务器的情况下通过添加或减少进程来动态扩展和收缩。 ,这些进程是平等的,不同进程之间不存在强依赖关系。 当一个进程崩溃时,不会影响其他进程的玩家。

除了玩家流程之外,还有战斗流程、家庭流程等类似流程都可以这样设计。

上面提到的都是有状态服务。 我们需要记录每个玩家/战斗/家族处于哪个进程。另外,如果进程出现异常,虽然不会影响其他玩家/战斗/家族,但当前进程中的玩家/战斗不会受到影响。 / 将不可用并且一些数据将丢失。

无状态服务

我们对一些服务采用无状态实现,比如登录、支付、好友、一些排名等。由于无状态服务对异常更加友好,并且比有状态服务有更简单的动态扩容和收缩模型,所以我们优先使用无状态服务一个新的服务,只有当状态更复杂时才考虑使用有状态服务实现。

点菜服务

游戏服务中不可避免地存在一些单点服务,比如玩家管理器、集群管理器、家庭管理器等,此类服务不具备扩展能力,是游戏服务器的瓶颈。 此外,它不具备高可用性。 如果出现异常,整个游戏集群将变得不可用。

单点服务的逻辑一般比较简单(必须支持复杂逻辑的水平扩展),性能负载一般较高。 比如我们游戏目前预计并发在线容量保守估计应该是50万。 这个时候我们相信我们的一些单点服务会满载,导致游戏无法继续扩展。

另外,单点服务数量较少,出现异常的可能性很低。 我们的游戏上线两年来,只经历过两次机器宕机,影响的是非单点进程,并没有影响整体游戏集群的可用性。

当然单点服务也可以改成支持横向扩展,只是工作量的问题。 理论上来说,完全消除单点是可以的,但对于大多数项目来说并不划算,意义不大。

2 高并发

水平扩展方案是支持高并发(也叫可扩展性)的主要手段,上面已经介绍过。

下面主要介绍除了高并发水平扩展之外的其他方案以及需要注意的地方。

2.1 纵向扩展和性能优化

为了提高承载能力,一般有两种选择:

垂直扩展在某些场景下也很有用。 一般来说,对于我们上面提到的单点,如果很难消除或者消除成本较高,可以通过垂直扩展的方式把这个逻辑放在高端机器上,以增加单点逻辑的负载。

另外,我们经常对战斗服务器的性能进行优化,比如使用C++编写高成本的模块,但对于大厅服务器,我们一般不以此作为提高承载能力的主要手段。 我们不会深入讨论这个问题。 一方面,总是有上限,很难发生质变。 另一方面,不同游戏的优化方案差别很大,都是代码级的优化。

服务器端优化的目标与垂直扩展类似,都是让一台机器承载更多的玩家/逻辑。

2.2 消除系统单点和逻辑单点

上面介绍的消除单点主要是系统的单点,即使用多个进程而不是一个进程来提供服务。

消除系统单点的前提是消除逻辑单点。

例如:当我们发布一个武器时,我们需要为该武器生成一个全服务器唯一的ID来识别它。 该ID可以使用自动递增ID,这会创建逻辑单点。

对于这种情况,如果游戏中武器生成的频率很低,那么这种方案也是可以的,但是如果武器生成的频率很高,因为游戏中所有的逻辑都需要到一个地方去申请这个ID,可能会造成瓶颈。 。 这种情况下,我们一般可以使用uuid来代替自增ID。 (这种场景在DB的自增列中也很常见,所以一般建议少用自增)

2.3 数据库托管

当玩家在线量达到一定程度时,往往会给后端数据库带来很大的压力。

一般来说,数据库本身就具备水平扩展能力,配合分库分表等解决方案,更容易增加承载能力。 不过,在设计数据库结构时,还需要考虑索引和索引等问题,否则数据库性能会受到严重影响。 另外,还必须考虑数据库的并发读写能力。 比如mongo中的存储引擎有级别锁,而存储引擎有doc级锁。 两者的并发能力有很大不同。

游戏逻辑一般比较复杂,读写数据量较大。 如果每次玩家信息发生变化都对数据库进行读写,会对数据库造成较大的压力。 因此,游戏的玩家服务一般都是有状态的服务。 当玩家上线时,数据从数据库读取到内存中。 当播放器在线时,数据直接读取并写入内存。 当玩家离线或间歇时,数据会记录到数据库中。 该方案大大减少了数据库的读写操作,对数据库的压力也大大降低。

对于一些数据读写操作频率不高的服务,可以考虑将服务设为无状态,每次读写时都操作数据库。

2.4 多集群和跨集群

当游戏服务器达到一定规模时,往往需要集群部署,集群解决的场景有:

多集群中需要解决的一个问题是跨集群通信。 在集群中,进程之间一般是全连接的。 但如果集群之间的进程全连接,就会造成拓扑混乱,连接数爆炸。 因此,集群间通信一般采用消息总线。 集群通过消息总线进行通信。

2.5 临时高并发

在游戏业务场景中,玩家在线状态与时间、活动等密切相关,不同时间在线玩家数量可能相差数倍、数十倍。

对于预期的高流量,可以提前进行扩容。 参考《的服务器优化》中的“动态扩容和缩容”。

对于突发的瞬间高并发,可以通过排队系统将流量挡在系统外,动态扩容后再慢慢进入游戏。

2.6 战斗场景高并发

游戏还有一个特殊的高并发场景,大规模MMO玩家聚集在某个场景,比如国战。

对于这种情况没有完美的解决方案。 我们只能尽量增加承载能力。 常见的改进解决方案包括:

我之前写过一篇文章《》是关于我在之前的游戏中所做的战斗服务器性能优化的。

3 高可用性

高可用性追求系统在运行过程中尽量减少系统服务不可用的情况。

评价指标是一个周期内的服务可用时间(SLA,Level)。 计算公式为:服务可用性=(总服务周期分钟数-服务不可用分钟数)/总服务周期分钟数×100%。

一般从两个维度来评价: 1、系统的完全可用性:所有服务对所有用户都可用。 2、系统整体可用性:部分服务或部分用户不可用,但整个系统可用。

高可用性的目标是力求系统完全可用,保证整体可用性。

大集群中的异常

由于客观存在机器故障、网络卡顿或断线等低概率异常,服务器也需要考虑这些问题,特别是在大集群场景下,低概率事件累积成高概率事件。 因此,在大型集群服务器场景中,高可用性是我们必须考虑的问题。

高可用实际上就是隔离处理各种异常情况,防止小概率异常事件影响游戏整体服务。

常见的例外情况包括:

3.1 基于水平扩展实现高可用

上面我们提到水平扩展可以增加并发负载能力,提高可用性,但侧重点不同。 对于高并发,水平扩展意味着我们可以通过添加机器/进程来增加容量。 对于高可用性来说,意味着当一台机器/进程遇到异常或者崩溃时,不会影响集群的整体可用性。

上面水平扩展中介绍过,对于支持水平扩展的服务,有状态服务的异常只会影响本进程提供的服务,其他进程会正常运行; 对于无状态服务,影响会更小,只会影响正在执行的进程。

当然,这需要我们编写一些处理逻辑,包括:

上述逻辑如何实现其实相当复杂,这里不再赘述。

服务隔离和灰度发布

在开发过程中,我们应该尽可能地将大的功能拆分成小的服务,每个服务只负责一小部分功能。 它还提供了更好的模式。 不同的可以放在一个进程中,也可以放在不同的进程中。

上一篇文章介绍了服务隔离和灰度发布,也是为了隔离高风险的服务,这样即使出现问题,也不会影响系统整体的可用性。

3.2 主从复制

对于有状态服务,支持水平扩展的进程可以保证一个进程的异常不会影响其他进程提供的服务。 但该进程的崩溃会导致该进程提供的服务不可用,并导致内存数据丢失等问题。

为了解决这个问题,常见的解决方案是主从复制。 主从复制在数据库中非常常见,是保证数据库高可用性的常用解决方案。

主从复制就是在主节点()后面挂一个或者多个从节点(slave),主节点将状态/数据实时复制到从节点。 正常情况下,由主节点提供服务。 当主节点出现问题时,从节点成为主节点并继续提供服务。 由于主节点虚拟地将数据复制到从节点,因此可以近似保证数据不会丢失。

因此,如果想要进一步提高有状态服务或者单点服务的可用性,可以使用主从复制方案。

很少有游戏服务器使用这种解决方案来编写业务逻辑。 部分集群管理节点(非业务逻辑)采用此方案。

另外,由于常见的db(mysql/mongo/redis)都自带主从复制,无状态服务实际上是让db替我们管理状态,从而获得了防止db主从复制数据丢失的能力。

3.3 云服务异常处理

除了ECS机器之外,我们还大量使用了阿里云的各种SAAS服务,比如redis/Mysql/Mongo等DB,以及类似ELK的日志服务。

这些服务大部分都支持主从切换等高可用方案,但是我们需要考虑它们进行主从切换时对我们系统的影响。

在Mysql和Redis中,当发生主从切换或者宕机时,网络连接将会断开。 因此,我们必须在逻辑上处理网络中断和重连。 在断网重连阶段,不可避免的会出现一些db请求失败的情况,我们也需要对这种异常进行处理。

在数据落地场景中,需要判断每个db请求是否成功。 如果没有,则重试并保证请求的幂等性,防止请求被多次执行。

4 高并发、高可用目标

为了实现高并发和高可用性,有很大的开发成本,而且大多数项目的人力资源都不是无限的。 因此,大家在做相关工作时,也必须过度设计,综合考虑业务需求、承载期望、开发成本。

其实我说的开发成本不仅仅指的是程序开发量越大,也意味着系统越复杂,就越容易出现问题。 如果没有足够的人力来测试、维护和迭代,最好采用更简单的方式来实现。 相反,出现问题的可能性较小。

当然,如果你的项目团队是王者荣耀或者和平精英,对高并发、高可用要求非常高,而且人力几乎无限,请忽略这一段。

百万人同时在线

在游戏行业中,一般将百万并发在线连接数作为游戏服务器架构的并发目标。 百万量级也是大多数游戏的上限(除了万王之王和炸鸡)。

因此,在游戏架构设计、规划和压力测试的前期,我们可以以百万同时在线连接数为基准来估算不同系统所需的负载能力。 达到这个负载能力就足够了。

例如,在我们的游戏中,虽然存在很多单点和性能瓶颈,但根据我们的估算,即使这些单点存在,我们通过增加更多的机器也可以支持最多100万个同时在线连接。 那么这些单点和瓶颈都在我们的预期之内,我们不会进一步优化。

如果有一天我们的游戏流行起来,需要支持千万人同时在线,理论上我们可以继续消除单点,优化性能瓶颈,但成本会大幅增加。

高可用!=完全可用

我们追求服务器集群的高可用,并不需要对所有异常进行容灾。 这是不可能的,也是不现实的。

按照思路,如果一个节点遇到异常,未能及时响应,那么所有访问它的请求都会被阻塞。 如果节点挂了,会直接上报trace。 相当于把集群当成一个整体,没有容错机制。 如果一个核心节点发生故障,整个集群应该不可用。

正如我上面所说,总体来说我认同的想法可以有效减轻业务发展过程中的精神负担。 在此基础上,对于一些常见的异常现象,应尽可能减少影响,避免雪崩效应。

超越技术的高并发、高可用

高并发、高可用还与非技术方面密切相关,如运维能力、硬件条件、人员素质、管理水平等。

为了解决高并发,需要部署大规模集群,这会对运维能力提出更高的要求。 当用户数量较少且集群较小时,可以接受手动运维。 但随着集群规模和复杂度的增加,手动运维会变得越来越困难,需要工具化和自动化。

游戏系统出现的很多问题,其实都是由运维工作、流程、规范等问题造成的。 比如,战双上线后,运营误发福利。

因此,要实现高并发和高可用,运维工具、运维流程和规范都必须做好。 运维必须工具化、自动化,而不是人工运维。 此外,监控、报警以及人员快速响应也是大型系统稳定运行的必要条件。