在现代游戏开发中,尤其微服务盛行,一个请求需要经历多个服务处理,不同服务之间也需要高频交互,服务间路由方式越来越多,扩展性和可靠性要求越来越高,需要选择合适的负载均衡方式,来达成系统高可用、可扩展性。下面将介绍几种常见的路由方式,当然,还有更多的负载均衡方式,如Weighted-Round-Robin、Power of Two Choices。
因为介绍负载均衡的文章很多,这里对相关概念都不再详述,主要结合具体的游戏场景来作说明。
一致性哈希(Consistent Hashing)
概述
一致性哈希是一种高效的分布式路由方式,特别适合动态变化的环境。在一致性哈希中,整个哈希空间被视为一个环,节点和数据项都通过哈希函数映射到这个环上。当节点增加或减少时,只有少量的数据需要重新分配,通常是顺时针方向上的数据项。这种特性使得一致性哈希在大规模在线游戏中非常受欢迎,能够有效地减少数据迁移,提高系统的可扩展性。
游戏应用场景
在微服务模式下,ConsistenHash可以说是游戏中应用最多的负载均衡方式,因为它对业务完全透明、扩缩容时流量切换最小,可以极大地简化业务逻辑,提高服务的稳定性,只要可以,请优先使用ConsistentHash,它将大大减少你后续为扩缩容、容灾所做的工作。关于ConsistentHash,你可以参阅更多的文章,这里就不再详述了。
像我之前项目,用的是Hash的方式(参考下文的Shard方式),在机器扩容器时,会产出大量的告警,有时还会发生玩家数据的回退(同一个玩家的请求路由到了两台机器上),改用ConsistenHash后,这种概率就大大降低了,但只要流量发生切换,就会有双写的可能,这里需要其它方式来解决(比如版本号机制),不是负载均衡能解决的。
在使用ConsistentHash中,要注意几个问题:
- 均匀性
在使用ConsistenHash后,你会发现流量并不是完全均衡地打到各个节点的,这是正常的,负载最高的节点和最低的节点,负载差距可能会达到30%。

- 性能
使用ConsistenHash时,要注意hash的Add/Del操作的性能,从下表可以看出,在1000个节点且配置100个虚拟节点的情况下,1000个节点添加完,需要6.4s。这个耗时是很致命的,比如一次故障影响了100个节点,这里完成这100个节点的hash变更会耗时0.64s,直接把服务卡住了。

最少负载(Least Load)
概述
最少负载路由策略将请求分发给当前负载最小的服务器。这种方法通过监控每个节点的负载情况,确保请求被均匀分配,从而避免某些节点过载而其他节点空闲的情况。对于需要实时响应的在线游戏,最少负载策略能够有效提高系统的响应速度和用户体验。
游戏应用场景
游戏中有很多服务的业务逻辑很重,比如游戏服务(gamesvr)、单局服务(gameplay/ds),每分配一个玩家或一个单局都会占用大量的服务资源,如果用ConsistentHash,就可能出现部分节点过载的情况,因为这些服务的承载大多只在百或千量级,所以它们的负载需要精心的维护。
经典的LeastLoad业务模型如下,每个节点都需要上报自己的负载信息,可以是承载的单局数、CPU、内存等,上报可以通过定时上报,也可以通过zk/etcd/db等,然后路由层维护所有节点的负载信息,从而进行节点的分配。


主从(Master-Slave)
概述
主从路由方式通常用于数据存储和管理。在这种模式下,主节点负责处理所有的写请求,而从节点则负责读取请求。主从模式能够有效地分担读取压力,提高系统的读性能。在游戏中,主从模式常用于管理玩家数据、游戏状态等信息,确保数据的一致性和高可用性。
游戏应用场景
游戏中主从的应用场景不多,应该尽量避免去使用主从方式,因为整体方案会比ConsistenHash要复杂。但可能出现某些服务必需是单节点,因为它需要维护全局的状态才能做决绝,那么,针对这种情况,主从是适合的路由方式。
随机(Random)
概述
随机路由是一种简单而有效的负载均衡策略。它将请求随机分配给可用的服务器。这种方法实现简单,适合于负载相对均匀的场景。然而,在负载不均的情况下,随机路由可能导致某些节点过载,而其他节点则处于闲置状态。因此,随机路由通常适用于负载较轻或对延迟要求不高的游戏场景。
游戏应用场景
Random应用场景不多,因为所有可以使用Random的地方,也一定可以使用ConsistenHash,这样,项目就可以少维护一种负载均衡方式了。
分片(Shard)
概述
分片是一种将数据分割成多个部分并分配到不同节点的路由方式。在游戏中,分片可以根据玩家的地理位置、游戏角色或其他属性将玩家请求分配到特定的服务器。这种方法能够有效地减少延迟,提高玩家的游戏体验。分片策略通常与一致性哈希结合使用,以实现更好的负载均衡和数据管理。
经典的Shard路由方式如下:

游戏应用场景
为什么有了ConsistenHash后,还会需要Shard这种方式的?(相对于ConsistentHash,Shard方式在节点挂掉时,即不能保证服务可用性,也需要额外配置每个节点的分配的key)但对于一些强缓存的服务,Shard的方式反而更加简单。
如非必须,不要使用Shard的路由方式,因为它会带来维护的复杂性,需要考虑key的分配,需要考虑扩缩容时数据的迁移。
游戏中的搜索是一个典型的适合使用Shard的服务。比如某游戏的搜索服务,提供玩家众多的选项来筛选自己心怡加入的队伍一起作战,这时就需要一个搜索服务来缓存所有的队伍,并按各种条件建索引(当然,你也可以用ES组件),它的业务模型是这样的(假设数据需要N个节点来承载):

每个索引服务存储了部分的队伍数据,所有索引服务的并集是所有的队伍数据,玩家每次搜索,就是去每个索引服务上搜索符合条件的队伍,在接入层进行结果汇总,并返回给玩家。
采用Shard的方式,每个节点很容易判断数据是不是自己负责的,各个节点启动时可以直接从DB加载需要的数据,完成冷启动,并且当某个节点挂掉后,对于搜索服务来说,它只是有损的(搜索结果变少了),玩家甚至感受不了服务出现了异常,而当这个服务拉起后,走冷启动就可以恢复数据。基于这个前提,你可以通过简单的重启服务完成扩缩容,故障处理也不需要太实时,走正常故障恢复就可以。

轮询(Round Robin)
概述
轮询是一种简单的负载均衡策略,它将请求依次分配给每个可用的服务器。轮询方法实现简单,能够确保请求的均匀分配。然而,在节点负载不均的情况下,轮询可能无法有效地平衡负载。因此,轮询通常适用于负载相对均匀的场景。
游戏应用场景
轮询在游戏中应用场景不多,在Web类应用比较多,游戏业务大部分都是强状态的,一些对外的服务适合使用这种负载均衡方式,比如gatesvr(提供http相关的服务), dirsvr(提供客户端的接入)等。所有对外的服务,都可以采用这种方式。
点对点(P2P)
概述
点对点(P2P)路由方式允许玩家之间直接通信,而不需要通过中心服务器。这种方法能够减少延迟,提高数据传输速度,适合于需要实时交互的在线游戏。P2P 网络的一个典型应用是多人在线游戏中的玩家对战,玩家可以直接连接到其他玩家的客户端进行游戏。
游戏应用场景
P2P是游戏中典型的路由方式,比如A玩家邀请B玩家,或者公会向所有成员推送消息等,P2P需要额外的记录地址信息,用于消息发送的目的地址,可以记录在一张online表中,也可以记录在公会成员的信息中。
广播(Broadcast)
概述
广播是一种将请求发送到所有可用节点的路由方式。在游戏中,广播可以用于发送全局消息、更新游戏状态或进行事件通知。虽然广播能够确保所有节点都接收到信息,但在节点数量较多时,可能会导致网络拥塞和性能下降。因此,广播通常适用于需要全局同步的场景。
游戏应用场景
Broadcast也是游戏中典型的路由方式,也P2P相反,它是发送给所有的目标,在游戏中的应用场景也很多,比如广播公会的所有成员、广播所有的dirsvr当前gamesvr的负载(假设dirsvr是用来管理gamesvr负载的服务)等。
这里的广播也通常意义的广播还稍有区别,像很多游戏都是分区的(不同的帐号体系不同的大区),分小区的、分平台的,所以广播也相应有不同的级别。
- 全区广播:发送给所有的指定服务类型节点
- 大区广播:发送给本大区所有的指定服务类型节点
- 小区广播:发送给本小区所有的指定服务类型节点
- 有限广播:发送给指定的节点列表
像上文例子中广播公会的所有成员,一般游戏的实现可能并不是广播,因为每个成员所在的节点不同,实际实现会变成发送N次P2P消息;当然,更好的是支持有限广播,通过指定要发送的列表,由路由层进行消息的分发,而不是由业务层进行。
在进行广播时,需要注意的是广播风暴的问题,这种一般出现在多级路由时,如果路由层一味地将消息广播给所有周边节点,那就会如图所示进入死循环,形成广播风暴,解决它也很简单,广播时踢除掉来源的链路就可以了。

但是如果我们稍微复杂一点,就会发现踢除来源链路,解决不了下面的问题,节点X还是会广播两遍,这里就需要进一步对消息进行标识,踢除掉已经广播过的消息。除此之外,消息中最好带上TTL,每经过一跳就+1,当达到X跳时,这条消息自动丢弃,这样可以杜绝广播风暴的产生。
