Akka Cluster之集群分片
一、介绍
当您需要在集群中的多个节点之间分配Actor,并希望能够使用其逻辑标识符与它们进行交互时,集群分片是非常有用的。你无需关心Actor在集群中的物理位置,因为这可能也会随着时间的推移而发生变化。
例如,它可以是代表域驱动设计术语中聚合根的参与者。在这里,我们称这些Actore为“实体”。这些Actor通常具有持久状态,但此功能不仅限于具有持久化状态的Actor。
当你有一个很消耗资源的Actor,例如占内存或者CPU,把它放在一台机器上可能吃不消,这时候集群分片就能够提供很好的帮助,将这些Actor分散在集群中的多个结点上。
二、依赖
<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-cluster-sharding_2.12</artifactId>
<version>2.5.18</version>
</dependency>
三、例子
假设我们有如下一个需要使用集群分片模式的实体Actor:
case object Increment
case object Decrement
final case class Get(counterId: Long)
final case class EntityEnvelope(id: Long, payload: Any)
case object Stop
final case class CounterChanged(delta: Int)
class Counter extends PersistentActor {
import ShardRegion.Passivate
context.setReceiveTimeout(120.seconds)
// self.path.name is the entity identifier (utf-8 URL-encoded)
override def persistenceId: String = "Counter-" + self.path.name
var count = 0
def updateState(event: CounterChanged): Unit =
count += event.delta
override def receiveRecover: Receive = {
case evt: CounterChanged ⇒ updateState(evt)
}
override def receiveCommand: Receive = {
case Increment ⇒ persist(CounterChanged(+1))(updateState)
case Decrement ⇒ persist(CounterChanged(-1))(updateState)
case Get(_) ⇒ sender() ! count
case ReceiveTimeout ⇒ context.parent ! Passivate(stopMessage = Stop)
case Stop ⇒ context.stop(self)
}
}
上面这个Actor使用PersistActor中的事件来源模式存储其内部状态,当然其不一定需要是持久化的Actor。使用持久化的好处是,如果节点之间的实体发生故障或迁移,它能够恢复其状态。
我们要使用集群的分片模式,通常就需要在群集中每个节点上的系统启动时,使用ClusterSharding.start
方法注册支持的实体类型,这样我们就能在所有将承载分片的节点上运行这个方法来部署分片。其中,ClusterSharding.start如下:
val counterRegion: ActorRef = ClusterSharding(system).start(
typeName = "Counter",
entityProps = Props[Counter],
settings = ClusterShardingSettings(system),
extractEntityId = extractEntityId,
extractShardId = extractShardId)
其中,start方法返回了ShardRegion,是个ActorRef类型。ShardRegion是一个特殊的Actor,负责管理可能多个分片(shard)内称为Entity的Actor实例。这些分片可能是分布在不同的集群节点上的,外界通过ShardRegion与其辖下Entities沟通。从start函数参数entityProps我们看到:每个分片中只容许一个种类的Actor;具体的Entity实例是由另一个内部Actor即shard构建的,shard可以在一个分片中构建多个Entity实例。多shard多entity的特性可以从extractShardId,extractEntityId这两个方法中得到一些信息。我们说过Actor自编码即entity-id是Cluster-Sharding的核心元素。在entity-id这个自编码中还包含了shard-id,所以用户可以通过entity-id的编码规则来设计整个分片系统包括每个ShardRegion下shard和entity的数量。当ShardRegion得到一个entity-id后,首先从中抽取shard-id,如果shard-id在集群中不存在的话就按集群各节点负载情况在其中一个节点上构建新的shard;然后再用entity-id在shard-id分片中查找entity,如果不存在就构建一个新的entity实例。整个shard和entity的构建过程都是通过用户提供的函数extractShardId和extractEntityId实现的,Cluster-Sharding就是通过这两个函数按用户的要求来构建和使用shard和entity的。下面我们看下这种自编码的例子:
val extractEntityId: ShardRegion.ExtractEntityId = {
case EntityEnvelope(id, payload) ⇒ (id.toString, payload)
case msg @ Get(id) ⇒ (id.toString, msg)
}
val numberOfShards = 100
val extractShardId: ShardRegion.ExtractShardId = {
case EntityEnvelope(id, _) ⇒ (id % numberOfShards).toString
case Get(id) ⇒ (id % numberOfShards).toString
case ShardRegion.StartEntity(id) ⇒
// StartEntity is used by remembering entities feature
(id.toLong % numberOfShards).toString
}
在大多数情况下工作正常的简单分片算法是获取hashCode
实体标识符的模数为分数的绝对值。下面我们可以通过如下程序测试:
val counterRegion: ActorRef = ClusterSharding(system).shardRegion("Counter")
counterRegion ! Get(123)
expectMsg(0)
counterRegion ! EntityEnvelope(123, Increment)
counterRegion ! Get(123)
expectMsg(1)