在最近,我们不断努力提升我们的技术栈和架构,并提高可扩展性。我们最近的重点是将我们的瑞米游戏服务转变为状态无关架构,这是现代化基础设施和提高性能的关键一步。
向无状态服务的转变是由关键目标驱动的:增强可扩展性,增强可靠性和容错能力,并为未来的升级提供更大的灵活性。通过简化架构,我们旨在更高效地处理更多的并发请求,减少系统故障的风险,并在技术迅速变化的环境中保持灵活性。
在这篇博客中,我们将探讨我们在开发无状态拉米游戏服务过程中的经历,讨论我们遇到的挑战、实施的解决方案以及学到的经验。作为我们最重大的技术欠账之一,我们非常希望与科技社区分享我们的见解。
在如意牌游戏中采用无状态架构至关重要直到2023年之前,我们的Rummy游戏后端服务一直受到其有状态设计的限制。持久化的Akka actor管理游戏桌,每个都绑定到特定的节点上。这种架构将游戏功能、数据和通信限制在单个节点上,阻碍了可扩展性。转向无状态架构对于增强可扩展性和适应性至关重要,使操作能够自由分布在多个节点上。以下将突出我们以前设置的关键限制。
服务节点与游戏房间之间的强耦合我们的有状态的 Rummy 服务使用持久化的 Akka 活动家 来管理游戏桌。这些活动家与特定的游戏桌绑定,在游戏进行过程中保持活跃。由于这些活动家被限制在特定的 Rummy 节点上,每张桌子只能由其分配的节点来管理。因此,某个 Rummy 桌的所有功能、数据和通信都局限于单个节点上。以下图示说明了这一概念。
集群系统中的部署扩容和节点故障带来的挑战在有状态的设计中,持久性 Actor 和 内存中的调度器 是特定于节点的。节点故障或部署过程可能会使状态恢复和继续操作变得复杂且耗时。我们利用 Akka 集群来处理这些问题。
在部署时,我们必须等待当前的游戏结束,然后才能继续在另一个节点上进行。每个节点通常需要20到25分钟。如图所示,这过程如下。
服务间的紧密耦合创建的表节点映射使 Rummy 和连接服务节点紧密相连,这些节点与客户端应用程序之间保持着实时的 websocket 连接。
连接服务管理表格到Rummy节点的映射,并在表格打开或关闭时更新映射。新Rummy节点需要通知所有的连接服务节点以便接收游戏请求。
为了管理这一点,连接服务节点为每个rummy节点维护了专门的TCP通道,形成了一个TCP网状。这确保了高效且专属的通信,但也带来了紧密的依赖。
玩无主瑞雅克的目标和规则我们将Rummy游戏服务转向无状态架构的转型由多个战略目标驱动,旨在提高可扩展性、可靠性和灵活性。以下是指导我们转型的关键目标。
解耦和独立的游戏模块我们的主要目标是让每个拉米游戏节点都能独立运行,不依赖于特定的节点或服务。每个节点应该能够处理任何请求并为任何拉米游戏桌执行任何功能。从而避免任何单一节点成为瓶颈或故障点,增强系统的整体稳定性。
与节点和服务无关的通信要实现真正的无状态,Rummy(鲁米)节点之间的通信必须设计为既具有节点无关性又具有服务无关性。这意味着任何节点都能够与任何其他节点或服务无缝交互,无需了解它们的具体身份或状态。这种交互的灵活性允许动态扩展,并更轻松地管理系统。
分布式数据存储及访问在无状态架构下,数据存储和访问需要在所有节点上均匀分布,以确保一致性和准确性。游戏状态及相关数据必须能够随时从任何节点访问。这种分布能够实现高效的负载均衡,并防止数据孤岛,从而提升了系统的性能和可靠性。
可伸缩性和弹性转向无状态架构的核心目标是提高可扩展性并降低运营成本。通过解耦游戏节点并将数据分布,我们可以根据需求轻松地添加或移除节点,确保资源被高效利用。这种动态伸缩性不仅确保系统在流量增加时性能不会下降,而且在非高峰时段通过减少规模来降低基础设施成本,从而在降低成本的同时保持无缝的用户体验。
增强的故障容忍性和可靠性无状态服务本质上增强了容错性和可靠性。由于每个请求独立处理,一个节点的失败不会影响整个系统。这种设计降低了系统级故障的风险,确保即使某个节点失败,整个系统也不会受到影响,并确保游戏服务即使在单个节点失败的情况下也能保持可用和响应。
简单部署维护无状态的架构简化了部署和维护。无需管理各节点间的持久状态,部署更新和执行维护变得更简单快捷。这样不仅减少了停机时间,还能更快地推出新功能和修复,确保服务总是最新且安全。
功能完整性在无状态架构中,保持功能一致性和准确性非常重要。Akka框架提供的功能,比如消息处理和Actor间通信,仍然需要保持可用。这可以通过例如顺序消息处理和消息缓存等替代设计来达成,确保可靠运行和快速响应。确保所有节点以一致的方式处理请求,有助于保持游戏体验的完整性和流畅性。
我们踏上无状态的兰米游戏之旅:一步一步的转变之旅 从有状态的 Akka 持久性 Actor 迁移到无状态有限状态机 (FSM)我们的主要目标是让每个Rummy节点都能处理来自任何游戏桌的请求。我们这样做是通过开发一个自定义的分布式有限状态机(FSM,简称FSM)来实现的,以此取代之前由Akka持久化Actor管理的功能。
- 我们设计了FSM层来管理Rummy节点之间的游戏状态,使用Redis进行数据存储,并用Lua脚本进行状态管理。这种分散式架构确保了任何节点都可以根据需要访问和更新游戏状态。
- FSM支持诸如消息队列和暂存等核心功能,类似于Akka的功能,但在分布式架构中实现。这种做法使游戏节点间能高效且可靠地通信,确保系统稳定运行和响应迅速。
- 我们实现了适用于无服务和节点无关的 Redis Streams 分布式通信框架。
- 我们已将所有基于 TCP 的通信迁移到 Redis Streams。
- 我们取消了为 Rummy 服务专门设置的通信通道。
- 我们移除了Akka集群,使得每个Rummy节点都能独立运行。
- 这确保了Rummy节点之间没有联系。
- 每个Rummy节点现在可以独立处理其功能。
- 在节点故障时不再需要迁移游戏桌。故障节点只会停止处理传入的消息,而其他节点可以接管所有运行中的功能。
- 我们用自定义的Jesque框架替换了特定于节点的Akka调度器。
- Jesque调度器是分布式的,其数据存储在Redis中并通过Lua脚本管理。
- 一个内部框架负责调度器的轮询,服务层则负责管理预定任务的消费和处理。
- 我们使用 AWS 自动伸缩组(ASG)来创建新的 Rummy 节点。
- 只需点击几下 AWS 控制台即可轻松调整容量,实现轻松扩展。
- 我们可以在高流量或低流量时段根据需要调整节点数量。
- 自动化管道会将新构建部署到生产环境。
让我们来探讨FSM架构的内部运作。该架构由两个主要组件组成:FSM发送器和FSM接收器。它们共同确保每个游戏房间的消息在服务器之间按顺序处理。每个游戏房间都会被分配一个唯一的FSM-ID,用于在不同的键中存储消息和执行状态。
在深入了解FSM组件(发送器和接收器)之前,了解可能的执行状态及其转变非常重要。
有限状态机-执行阶段在FSM里,执行状态表示FSM的状态。存在四种可能的状态,通过其FSM-ID来识别。
空: 当队列为空时,FSM的状态为空,表示没有消息正在处理。
就绪: 当队列中有至少一条消息但没有消息正在被处理时,FSM 的状态是就绪。
处理中 当一条消息正在处理时,FSM的状态为处理中。
完成: 当FSM被摧毁时,状态就变为完成。
FSM-发送者FSM 发送器是 FSM 消息处理系统的关键组件。其主要职责是将消息分发到系统中进行分布式处理,并不保留任何请求之间的信息。这种无状态的设计使得 FSM 能够高效运行且独立扩展。
消息发送和状态处理过程始于发送方发送一条消息,这条消息由一个唯一的FSM-ID(有限状态机标识符)标识。发送方与状态机互动,根据其当前状态来决定如何处理该消息。
发送者如下处理执行状态:
空: 将消息加入到FSM-ID的队列中,将状态改为就绪,并通知接收方处理该消息。通过发布FSM-ID。
READY :将消息加入到FSM-ID的队列中,而不通知接收者,因为在状态从空变到就绪时已经通知过了。
处理中:直接将消息加入FSM-ID的队列中,不通知接收者,因为FSM正在忙于另一条消息。
完成状态,由于FSM已经销毁,无需采取任何措施。
FSM接收器在FSM-Sender之后,FSM-Receiver在处理传入消息方面发挥关键作用。就像发送方一样,接收方也不保留任何请求之间的状态信息,从而能够高效地扩展规模。
消息接收及状态管理接收器根据当前的FSM-ID状态处理消息:(注:FSM-ID为状态机标识符的缩写,首次出现时加以解释以确保理解。)
- 空闲: 变为空闲状态。
- 就绪: 状态变为进行中,处理消息后:
- 如果队列为空,则变为空闲状态。
- 如果队列中有消息,则变为就绪状态,并通知接收方。
- 进行中: 不做任何操作。
- 完成: 无操作(状态机将被销毁)。
为了确保系统的健壮性,未被确认的FSM-ID会在经过设定的时间后自动重新处理这些未被确认的FSM-ID,避免因节点故障而丢失消息。
部署和迁移虽然无状态的拉米提供了部署和扩展的明显好处,但这些好处只有在完成全部流量迁移后才真正显现。我们最大的挑战是在有效地完成这一过渡。
无状态兰米实现- 部署了无状态的Rummy到一个新的独立节点组(包含在自动扩展中)
- 维护现有的有状态节点,计划在迁移后逐步淘汰这些节点
- 使用了独立的ElastiCache集群(Jesque、Redis流、FSM)为新功能
- 使用第三方代理实现了集中日志记录
- 开发了三阶段的交通切换:旧版轮盘、部分新版轮盘、全部新版轮盘
- 在连接服务中加入了交通切换层
- 使用ZooKeeper进行增量配置
- 初始仅将配置的模板发送到新版轮盘
- 从低风险的模板开始,以尽量减少潜在问题
- 从部分流量切换开始,配置一个模板进行测试
- 监控性能并进行基本检查
- 依次迁移模板,以便于快速解决可能出现的问题
Sourabh Gond,是一名热衷于解决复杂问题的SDE-2。他的博客文章展示了他对我们业务的有影响力贡献。擅长Java、Spring Boot和Redis,Sourabh喜欢应对各种困难挑战。在他的业余时间,他喜欢玩视频游戏。
共同学习,写下你的评论
评论加载中...
作者其他优质文章