Akka分布式游戏后端开发13 Cluster Sharding

216 阅读4分钟

本专栏的项目代码在Github上,如果感兴趣的话,麻烦点点Star,谢谢啦。

Akka Cluster Sharding 是 Akka 集群模块中用于分布式系统的一项重要功能。它通过将实体(例如 actor)分布在集群中的多个节点上,实现了分布式状态的管理,同时简化了开发。以下是 Akka Cluster Sharding 提供的主要功能:

1. 实体的动态分片与分布

  • Akka Cluster Sharding 自动将实体分片并分布到集群中的多个节点上。
  • 每个分片包含一组实体,分片之间是独立的,可以动态调整分布,优化资源使用和负载均衡。
  • 当集群节点变化(新增或移除)时,Akka 会重新分配分片,确保系统正常运行。

2. 实体的自动定位

  • 开发者不需要手动跟踪某个实体在哪个节点上,Akka Cluster Sharding 会自动定位目标实体。
  • 通过唯一的 实体 ID分片 ID,可以直接与目标实体进行交互。

3. 实体的懒加载

  • 实体不会在集群启动时立即全部加载,而是按照需求动态启动。
  • 当某个实体首次接收到消息时,Akka Cluster Sharding 会自动启动相应的 actor。

4. 实体的容错与高可用性

  • 如果集群中的某个节点失效,Akka Cluster Sharding 会自动将该节点上的分片及其实体重新分配到其他可用节点上。
  • 容错机制确保系统的高可用性,即使部分节点故障,系统也能正常运行。

5. 状态持久化支持

  • Akka Cluster Sharding 可以与 Akka Persistence 集成,支持持久化实体的状态。
  • 实体的状态可以在节点重启或转移后恢复,确保状态一致性。

6. 消息路由与负载均衡

  • Akka Cluster Sharding 提供了消息路由的功能:

    • 开发者只需发送消息到 ShardRegion,它会自动根据分片策略将消息路由到正确的实体。
  • 支持动态负载均衡,通过调整分片分布来避免某些节点过载。

7. 动态扩展与缩减

  • 支持动态扩展:可以在运行时添加新节点,集群会自动重新分配分片。
  • 支持动态缩减:移除节点时,分片和实体会自动迁移到其他节点上。

8. 自定义分片策略

  • 开发者可以定义自定义的分片逻辑,控制实体如何划分到不同的分片中。
  • 默认的分片策略是基于哈希分片,但也可以实现复杂的分片规则,比如基于特定的业务逻辑或负载。

9. 实体生命周期管理

  • 支持管理实体的生命周期,例如通过超时设置(passivate),在实体长时间未使用时自动停止,释放资源。
  • 停止的实体可以在收到新消息时重新激活(懒加载)。

10. 消息一致性与顺序保证

  • 对同一实体的消息 Akka Cluster Sharding 保证顺序性,即消息会按照发送顺序到达目标实体。
  • 配合 Akka 的可靠传递机制,可以实现消息的一致性和可靠性。

在游戏集群中,大部分的 Actor 实体为玩家实体以及区服实体,因此对这两种类型的 Actor 做分片就好了。

定义消息路由逻辑

当我们向 ShardRegion 发送一条消息时, ShardRegion 需要如何路由这条消息,Akka 为我们提供了 MessageExtractor 接口,给定一条消息,我们需要返回这条消息所在的分片 ID实体 ID。并将消息路由到正确的实体。它的实现定义了如何解析消息以确定目标分片和实体。

由于我们业务中的 PlayerActorWorldActor 都是带 id 的,所以我们直接用 id 做分片逻辑就好了。最简单的逻辑就是取余来确定分片的 id,当然,除数具体是多少还是要根据项目的情况具体做调整。

class LongShardMessageExtractor(private val numberOfShards: Int) : ShardRegion.MessageExtractor {
    override fun entityId(message: Any): String {
        return when (message) {
            is ShardMessage<*> -> message.id.toString()
            else -> error("Unknown message type: $message")
        }
    }

    override fun entityMessage(message: Any): Any {
        return message
    }

    override fun shardId(message: Any): String {
        return when (message) {
            is ShardMessage<*> -> (abs(message.id as Long) % numberOfShards).toString()
            else -> error("Unknown message type: $message")
        }
    }
}

val PlayerMessageExtractor = LongShardMessageExtractor(PLAYER_SHARD_NUM)

val WorldMessageExtractor = LongShardMessageExtractor(WORLD_SHARD_NUM)

启动分片

private fun startPlayerSharding() {
    playerSharding = system.startSharding(
        ShardEntityType.PlayerActor.name,
        Role.Player,
        PlayerActor.props(this),
        HandoffPlayer,
        PlayerMessageExtractor,
        ShardCoordinator.LeastShardAllocationStrategy(1, 3),
    )
}

在其它节点启动 Proxy 就可以向该类型的 Actor 发送消息:

private fun startPlayerSharding() {
    playerSharding =
        system.startShardingProxy(ShardEntityType.PlayerActor.name, Role.Player, PlayerMessageExtractor)
}

结尾

通过以上操作,PlayerActorWorldActor 的分片工作就完成了。在 ChannelActor 中收到消息之后,就可以通过 ShardRegion 的 Proxy 模式将消息路由到正确的分片 Actor 上了。