本专栏的项目代码在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。并将消息路由到正确的实体。它的实现定义了如何解析消息以确定目标分片和实体。
由于我们业务中的 PlayerActor 和 WorldActor 都是带 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)
}
结尾
通过以上操作,PlayerActor 和 WorldActor 的分片工作就完成了。在 ChannelActor 中收到消息之后,就可以通过 ShardRegion 的 Proxy 模式将消息路由到正确的分片 Actor 上了。