akka Cluster基本实现原理已经分析过,其实它就是在remote基础上添加了gossip协议,同步各个节点信息,使集群内各节点能够识别。在Cluster中可能会有一个特殊的节点,叫做单例节点。也就是具有某个角色的节点在集群中只能有一个,如果这个节点宕机了,需要把这个角色的工作转移到其他节点。

  使用单例模式有几个场景。

  • 负责集群的一致性决策,跨集群协调工作。比如集群事务。
  • 对外部系统的单实例接入。
  • 一个master,多个worker
  • 集中化的命名服务或路由策略。

  官网说单例对象在设计时,永远不要成为第一个选择,它可能存在单点性能瓶颈、单点故障等多个问题。这点我是赞同的,这年头都是分布式为王,当你考虑单点的时候,有两种情况:设计错误、业务特殊。单例虽然有那么多缺点,但还是有点用处的,源码分析还是要做的,我们要知其然还要知其所以然。

  akka的单例模式由akka.cluster.singleton.ClusterSingletonManager.实现,它在集群或角色相同的一组节点管理一个单例actor实例。ClusterSingletonManager 是集群或角色相同节点中最先被启动的actor 。ClusterSingletonManager 会在所有节点中运行时间最长的节点上启动,如果这个节点宕机,则重新选择运行时间最长的节点。如果你要问运行时间最长的信息从哪里获取?我不知道,哈哈,这肯定是在通过gossip协议广播的Member消息中呗。

  那么问题来了,既然ClusterSingletonManager 的位置会发生漂移,那我怎么跟它通信呢?akka还是很贴心的,它帮你实现了一个akka.cluster.singleton.ClusterSingletonProxy,这个actor会把消息路由给当前的ClusterSingletonManager 单例对象。简单来说就是ClusterSingletonProxy会给运行时间最长的节点发送一个akka.actor.Identify消息,查询单例actor的actorRef。当然了,在查找的期间消息是被缓冲的,超过缓冲大小直接丢弃。读者需要注意的是,akka只是尽量保证同一时刻最多有一个ClusterSingletonManager 单例,但并不保证同一时刻有且仅有一个。

    system.actorOf(
ClusterSingletonManager.props(
singletonProps = Props(classOf[Consumer], queue, testActor),
terminationMessage = End,
settings = ClusterSingletonManagerSettings(system).withRole("worker")),
name = "consumer")

  同样的,我们还是从官网demo入手。上面是ClusterSingletonManager的创建过程,很明显这是创建了一个actor,这个actor内部还有一个Props,也就是会创建对应的子actor:Consumer。ClusterSingletonManager会保证在集群中的Consumer实例只有一个。

    val proxy = system.actorOf(
ClusterSingletonProxy.props(
singletonManagerPath = "/user/consumer",
settings = ClusterSingletonProxySettings(system).withRole("worker")),
name = "consumerProxy")

  上面创建了ClusterSingletonProxy,这个proxy路由消息给集群中path为/user/consumer的单例对象,当然了ClusterSingletonProxy是可以有多个的。

  下面切入正题,来看看ClusterSingletonManager和ClusterSingletonProxy的源码。首先这两个包是在cluster-tools这个包里面。

/**
* Manages singleton actor instance among all cluster nodes or a group
* of nodes tagged with a specific role. At most one singleton instance
* is running at any point in time.
*
* The ClusterSingletonManager is supposed to be started on all nodes,
* or all nodes with specified role, in the cluster with `actorOf`.
* The actual singleton is started on the oldest node by creating a child
* actor from the supplied `singletonProps`.
*
* The singleton actor is always running on the oldest member with specified role.
* The oldest member is determined by [[akka.cluster.Member#isOlderThan]].
* This can change when removing members. A graceful hand over can normally
* be performed when current oldest node is leaving the cluster. Be aware that
* there is a short time period when there is no active singleton during the
* hand-over process.
*/
@DoNotInherit
class ClusterSingletonManager(
singletonProps: Props,
terminationMessage: Any,
settings: ClusterSingletonManagerSettings)
extends Actor with FSM[ClusterSingletonManager.State, ClusterSingletonManager.Data]

  上面是ClusterSingletonManager的定义,官方注释很多,我只保留了最重要的三段。第一段介绍了什么是单例对象;第二段说ClusterSingletonManager需要在所有的节点启动,它管理的单例对象在集群中只会有一个;第三段说这个单例actor只会在运行时间最长的节点上,重启过程中单例不可用。其实第二点值得我们深究,既然ClusterSingletonManager需要在所有的节点启动,那怎么保持单例呢。其实吧,正如它的名字那样,它只是个管理者,真正的单例actor是通过singletonProps创建的。也就是说,ClusterSingletonManager在节点中互相协调,具体由谁创建整个单例actor。看到这里我都想自己实现一个单例actor了。

  还要注意的是这个manager继承了FSM接口,应该是为了协调单例的状态什么的。既然这是个actor,那就需要看看这个actor启动时,做了哪些初始化。

 override def preStart(): Unit = {
super.preStart()
require(!cluster.isTerminated, "Cluster node must not be terminated") // subscribe to cluster changes, re-subscribe when restart
cluster.subscribe(self, ClusterEvent.InitialStateAsEvents, classOf[MemberRemoved]) setTimer(CleanupTimer, Cleanup, 1.minute, repeat = true) // defer subscription to avoid some jitter when
// starting/joining several nodes at the same time
cluster.registerOnMemberUp(self ! StartOldestChangedBuffer)
}

  简单来说就是订阅了ClusterEvent.InitialStateAsEvents、MemberRemoved、MemberUp消息,启动了一个timer。这个timer每个1分钟发送一个Cleanup消息,timer的名字是CleanupTimer。收到MemberUp消息会给自己发送StartOldestChangedBuffer。初始化是不是很简单?想想都简单,这就是一个普通actor,只不过需要跟其他actor协调一下,那个是运行时间最久的actor,然后再加上单例actor漂移的功能就行了。

  startWith(Start, Uninitialized)

  主构造函数中还有上面一段代码需要注意,这是FSM的函数,之前分析过,这是用来设置初始状态的,也就是定义当前ctor的receive。

 when(Start) {
case Event(StartOldestChangedBuffer, _) ⇒
oldestChangedBuffer = context.actorOf(Props(classOf[OldestChangedBuffer], role).
withDispatcher(context.props.dispatcher))
getNextOldestChanged()
stay case Event(InitialOldestState(oldestOption, safeToBeOldest), _) ⇒
oldestChangedReceived = true
if (oldestOption == selfUniqueAddressOption && safeToBeOldest)
// oldest immediately
gotoOldest()
else if (oldestOption == selfUniqueAddressOption)
goto(BecomingOldest) using BecomingOldestData(None)
else
goto(Younger) using YoungerData(oldestOption)
}

  很显然Start状态会收到StartOldestChangedBuffer消息。

// started when when self member is Up
var oldestChangedBuffer: ActorRef = _

  首先创建了一个OldestChangedBuffer,这个actor会在当前节点变成up状态时启动,作用未知,然后就调用getNextOldestChanged。

/**
* Notifications of member events that track oldest member is tunneled
* via this actor (child of ClusterSingletonManager) to be able to deliver
* one change at a time. Avoiding simultaneous changes simplifies
* the process in ClusterSingletonManager. ClusterSingletonManager requests
* next event with `GetNext` when it is ready for it. Only one outstanding
* `GetNext` request is allowed. Incoming events are buffered and delivered
* upon `GetNext` request.
*/
class OldestChangedBuffer(role: Option[String]) extends Actor

  OldestChangedBuffer官方注释也很清晰,这个actor就是用来监听集群事件,用来判断运行时间最久的节点的。

 def getNextOldestChanged(): Unit =
if (oldestChangedReceived) {
oldestChangedReceived = false
oldestChangedBuffer ! GetNext
}

  getNextOldestChanged也比较简单就是给oldestChangedBuffer发送了GetNext消息。

def receive = {
case state: CurrentClusterState ⇒ handleInitial(state)
case MemberUp(m) ⇒ add(m)
case MemberRemoved(m, _) ⇒ remove(m)
case MemberExited(m) if m.uniqueAddress != cluster.selfUniqueAddress ⇒
remove(m)
case SelfExiting ⇒
remove(cluster.readView.self)
sender() ! Done // reply to ask
case GetNext if changes.isEmpty ⇒
context.become(deliverNext, discardOld = false)
case GetNext ⇒
sendFirstChange()
}

  oldestChangedBuffer收到GetNext消息,会判断changes是不是为空,如果为空,则简单的进行beconme,否则调用sendFirstChange。那changes什么时候赋值呢?

def trackChange(block: () ⇒ Unit): Unit = {
val before = membersByAge.headOption
block()
val after = membersByAge.headOption
if (before != after)
changes :+= OldestChanged(after.map(_.uniqueAddress))
} def handleInitial(state: CurrentClusterState): Unit = {
membersByAge = immutable.SortedSet.empty(ageOrdering) union state.members.filter(m ⇒
(m.status == MemberStatus.Up || m.status == MemberStatus.Leaving) && matchingRole(m))
val safeToBeOldest = !state.members.exists { m ⇒ (m.status == MemberStatus.Down || m.status == MemberStatus.Exiting) }
val initial = InitialOldestState(membersByAge.headOption.map(_.uniqueAddress), safeToBeOldest)
changes :+= initial
}

  简要分析之后,可以发现OldestChangedBuffer在收到集群的相关消息时会调用上面两个函数更改changes的值,其实就是保存运行时间最长节点的信息。但不论哪种情况,其实最终都是会调用sendFirstChange方法。

def sendFirstChange(): Unit = {
// don't send cluster change events if this node is shutting its self down, just wait for SelfExiting
if (!cluster.isTerminated) {
val event = changes.head
changes = changes.tail
context.parent ! event
}
}

  这个函数的功能不作过多介绍,其实就是获取集群节点信息,获取运行时间最长的节点信息,发送给context.parent也就是ClusterSingletonManager,只不过会区分当前节点是不是第一个启动的节点,但无论是不是第一个启动,区别都不大。

  此时ClusterSingletonManager还在Start状态,会收到InitialOldestState消息。假设当前节点就是最老的,那么会调用gotoOldest

def gotoOldest(): State = {
val singleton = context watch context.actorOf(singletonProps, singletonName)
logInfo("Singleton manager starting singleton actor [{}]", singleton.path)
goto(Oldest) using OldestData(singleton)
}

  可以看到,如果当前节点是最老的,那么会创建singletonProps,然后转入Oldest状态,且FSM数据就是这个singletonProps的ActorRef。那么后面启动的节点,同样会执行上面的流程,只不过它判断当前节点是最老节点时会失败,不会执行gotoOldest方法,也就不会创建singletonProps,而是转到Younger状态。

when(Younger) {
case Event(OldestChanged(oldestOption), YoungerData(previousOldestOption)) ⇒
oldestChangedReceived = true
if (oldestOption == selfUniqueAddressOption) {
logInfo("Younger observed OldestChanged: [{} -> myself]", previousOldestOption.map(_.address))
previousOldestOption match {
case None ⇒ gotoOldest()
case Some(prev) if removed.contains(prev) ⇒ gotoOldest()
case Some(prev) ⇒
peer(prev.address) ! HandOverToMe
goto(BecomingOldest) using BecomingOldestData(previousOldestOption)
}
} else {
logInfo("Younger observed OldestChanged: [{} -> {}]", previousOldestOption.map(_.address), oldestOption.map(_.address))
getNextOldestChanged()
stay using YoungerData(oldestOption)
} case Event(MemberRemoved(m, _), _) if m.uniqueAddress == cluster.selfUniqueAddress ⇒
logInfo("Self removed, stopping ClusterSingletonManager")
stop() case Event(MemberRemoved(m, _), _) ⇒
scheduleDelayedMemberRemoved(m)
stay case Event(DelayedMemberRemoved(m), YoungerData(Some(previousOldest))) if m.uniqueAddress == previousOldest ⇒
logInfo("Previous oldest removed [{}]", m.address)
addRemoved(m.uniqueAddress)
// transition when OldestChanged
stay using YoungerData(None) case Event(HandOverToMe, _) ⇒
// this node was probably quickly restarted with same hostname:port,
// confirm that the old singleton instance has been stopped
sender() ! HandOverDone
stay
}

  如果最老节点发生变化,且当前节点不是最老节点,而当前节点变成了最老节点,会发生什么呢?会命中Younger状态的第一个case店里面的第一个if语句。也就是会给原来的最老节点发送HandOverToMe消息 ,自己变成BecomingOldest状态。

def gotoHandingOver(singleton: ActorRef, singletonTerminated: Boolean, handOverTo: Option[ActorRef]): State = {
if (singletonTerminated) {
handOverDone(handOverTo)
} else {
handOverTo foreach { _ ! HandOverInProgress }
singleton ! terminationMessage
goto(HandingOver) using HandingOverData(singleton, handOverTo)
}
}

  此时原来的最老节点,会调用上面的函数,简单来说既是给原来的singleton这个单例actor发送terminationMessage消息,当然,单例actor收到这个消息后可自行处理,但最终都需要stop。这样,才能给最新的最老节点发送HandOverDone消息。而此时最新的最老节点处于BecomingOldest状态,收到消息后命中以下case。

 case Event(HandOverDone, BecomingOldestData(Some(previousOldest))) ⇒
if (sender().path.address == previousOldest.address)
gotoOldest()
else {
logInfo(
"Ignoring HandOverDone in BecomingOldest from [{}]. Expected previous oldest [{}]",
sender().path.address, previousOldest.address)
stay
}

  也比较简单,就是调用gotoOldest,这个方法的功能之前说过了。

  但我发现了一个比较重要的问题,那就是如果当前节点处于Oldest状态,而创建的单例对象actor以外终止了,会发生什么呢?从Oldest的处理来看,它就是修改了当前状态数据的singletonTerminated为true,没有其他操作。难道不应该是重启么?很奇怪啊,不管了,先继续分析吧。

/**
* The `ClusterSingletonProxy` works together with the [[akka.cluster.singleton.ClusterSingletonManager]] to provide a
* distributed proxy to the singleton actor.
*
* The proxy can be started on every node where the singleton needs to be reached and used as if it were the singleton
* itself. It will then act as a router to the currently running singleton instance. If the singleton is not currently
* available, e.g., during hand off or startup, the proxy will buffer the messages sent to the singleton and then deliver
* them when the singleton is finally available. The size of the buffer is configurable and it can be disabled by using
* a buffer size of 0. When the buffer is full old messages will be dropped when new messages are sent via the proxy.
*
* The proxy works by keeping track of the oldest cluster member. When a new oldest member is identified, e.g. because
* the older one left the cluster, or at startup, the proxy will try to identify the singleton on the oldest member by
* periodically sending an [[akka.actor.Identify]] message until the singleton responds with its
* [[akka.actor.ActorIdentity]].
*
* Note that this is a best effort implementation: messages can always be lost due to the distributed nature of the
* actors involved.
*/
final class ClusterSingletonProxy(singletonManagerPath: String, settings: ClusterSingletonProxySettings) extends Actor with ActorLogging

  官方注释也很详细,ClusterSingletonProxy也会跟踪当前集群的最老节点,然后通过singletonManagerPath进行actorSelection获取单例actor的ActorRef。ClusterSingletonProxy跟OldestChangedBuffer关于集群消息的部分,功能框架有点类似,都是在集群节点变化时调用handleInitial、add、remove三个函数,不同的是这三个函数在ClusterSingletonProxy中还调用了trackChange。

def trackChange(block: () ⇒ Unit): Unit = {
val before = membersByAge.headOption
block()
val after = membersByAge.headOption
// if the head has changed, I need to find the new singleton
if (before != after) identifySingleton()
}

  trackChange发现节点有变化时,会调用identifySingleton

def identifySingleton() {
import context.dispatcher
log.debug("Creating singleton identification timer...")
identifyCounter += 1
identifyId = createIdentifyId(identifyCounter)
singleton = None
cancelTimer()
identifyTimer = Some(context.system.scheduler.schedule(0 milliseconds, singletonIdentificationInterval, self, ClusterSingletonProxy.TryToIdentifySingleton))
}

  其实就是启动了一个定时器,每隔singletonIdentificationInterval秒给自己发送ClusterSingletonProxy.TryToIdentifySingleton消息,为啥需要间隔时间重复发送了,这主要是为了防止单例还在漂移,无法回应Identify消息。

case ClusterSingletonProxy.TryToIdentifySingleton ⇒
identifyTimer match {
case Some(_) ⇒
membersByAge.headOption foreach { oldest ⇒
val singletonAddress = RootActorPath(oldest.address) / singletonPath
log.debug("Trying to identify singleton at [{}]", singletonAddress)
context.actorSelection(singletonAddress) ! Identify(identifyId)
}
case _ ⇒
// ignore, if the timer is not present it means we have successfully identified
}

  很显然收到这个消息,会通过actorSelection发送Identify消息。

case ActorIdentity(identifyId, Some(s)) ⇒
// if the new singleton is defined, deliver all buffered messages
log.info("Singleton identified at [{}]", s.path)
singleton = Some(s)
context.watch(s)
cancelTimer()
sendBuffered()

  收到ActorIdentity消息后,会取消timer,然后把缓存的消息发送出去。

case msg: Any ⇒
singleton match {
case Some(s) ⇒
if (log.isDebugEnabled)
log.debug(
"Forwarding message of type [{}] to current singleton instance at [{}]: {}",
Logging.simpleName(msg.getClass), s.path)
s forward msg
case None ⇒
buffer(msg)
}

  如果singleton已经有值,收到消息后会命中上面的case分支,其实就是简单的forward消息。

  好了,到此为止,单例模式基本就解释清楚了,其实源码也不是非常复杂,如果不考虑到通用、健壮性,自己实现一个获取更高效。而且在ClusterSingletonManager中,发现单例actor已经stop了,居然没有任何补偿措施,这真是恶心啊,说好的高可用呢,说好的故障恢复呢。另外ClusterSingletonManager需要自己单独的用actorOf进行创建,为啥不用一个扩展来实现呢,简单、方便、易管理。

Cluster Singleton

Akka源码分析-Cluster-Singleton的更多相关文章

  1. Akka源码分析-Cluster-Distributed Publish Subscribe in Cluster

    在ClusterClient源码分析中,我们知道,他是依托于“Distributed Publish Subscribe in Cluster”来实现消息的转发的,那本文就来分析一下Pub/Sub是如 ...

  2. Akka源码分析-Cluster-Sharding

    个人觉得akka提供的cluster工具中,sharding是最吸引人的.当我们需要把actor分布在不同的节点上时,Cluster sharding非常有用.我们可以使用actor的逻辑标识符与ac ...

  3. Akka源码分析-Cluster-Metrics

    一个应用软件维护的后期一定是要做监控,akka也不例外,它提供了集群模式下的度量扩展插件. 其实如果读者读过前面的系列文章的话,应该是能够自己写一个这样的监控工具的.简单来说就是创建一个actor,它 ...

  4. Akka源码分析-Persistence

    在学习akka过程中,我们了解了它的监督机制,会发现actor非常可靠,可以自动的恢复.但akka框架只会简单的创建新的actor,然后调用对应的生命周期函数,如果actor有状态需要回复,我们需要h ...

  5. Akka源码分析-Cluster-ActorSystem

    前面几篇博客,我们依次介绍了local和remote的一些内容,其实再分析cluster就会简单很多,后面关于cluster的源码分析,能够省略的地方,就不再贴源码而是一句话带过了,如果有不理解的地方 ...

  6. storm操作zookeeper源码分析-cluster.clj

    storm操作zookeeper的主要函数都定义在命名空间backtype.storm.cluster中(即cluster.clj文件中).backtype.storm.cluster定义了两个重要p ...

  7. Akka源码分析-Akka Typed

    对不起,akka typed 我是不准备进行源码分析的,首先这个库的API还没有release,所以会may change,也就意味着其概念和设计包括API都会修改,基本就没有再深入分析源码的意义了. ...

  8. Akka源码分析-Akka-Streams-概念入门

    今天我们来讲解akka-streams,这应该算akka框架下实现的一个很高级的工具.之前在学习akka streams的时候,我是觉得云里雾里的,感觉非常复杂,而且又难学,不过随着对akka源码的深 ...

  9. Akka源码分析-local-DeathWatch

    生命周期监控,也就是死亡监控,是akka编程中常用的机制.比如我们有了某个actor的ActorRef之后,希望在该actor死亡之后收到响应的消息,此时我们就可以使用watch函数达到这一目的. c ...

随机推荐

  1. 九度oj 题目1055:数组逆置

    题目1055:数组逆置 时间限制:1 秒 内存限制:32 兆 特殊判题:否 提交:8225 解决:3932 题目描述: 输入一个字符串,长度小于等于200,然后将数组逆置输出. 输入: 测试数据有多组 ...

  2. POJ 3469 网络流最小割

    将两个CPU分别视作源点和汇点 对于那些不在同一个CPU中的模块会产生的代价作为一条双向的容量弧 这里每个模块可以在任意一个CPU中运行,相当于寻找一个割,分割后,在S集合中的模块安装在第一个CPU中 ...

  3. hdu1978

          {          scanf(          {         scanf(         ;i<n;i++) ;j<m;j++)         scanf(   ...

  4. 莫比乌斯反演套路一--令t=pd--BZOJ2820: YY的GCD

    t<=10000组询问:有多少x,y,满足$x\epsilon [1,n],y\epsilon [1,m],(x,y)为质数$.n,m<=1e7. 首先式子列出来,f(i)--1<= ...

  5. Nginx配置文件nginx.conf 详解

    #定义Nginx运行的用户和用户组 user www www;   #nginx进程数,建议设置为等于CPU总核心数. worker_processes 8;   #全局错误日志定义类型,[ debu ...

  6. Ubuntu 12.04 之 LAMP

    搭建LAMP环境 (1)更新软件列表: sudo apt-get update 结果报错: W: 无法下载 bzip2:/var/lib/apt/lists/partial/cn.archive.ub ...

  7. P1334 瑞瑞的木板 洛谷

    https://www.luogu.org/problem/show?pid=1334 题目描述 瑞瑞想要亲自修复在他的一个小牧场周围的围栏.他测量栅栏并发现他需要N(1≤N≤20,000)根木板,每 ...

  8. MariaDB 数据库的备份

    1> 备份单个数据库 mysqldump -uroot -plichao123 --database students1 > stundents.sql; 2>查看备份文件 3> ...

  9. 一个有趣的问题:ls -l显示的内容中total究竟是什么?

    当我们在使用ls -l的命令时,我们会看到例如以下类似的信息. 非常多人可能对于第一行的total 12的数值并非非常在意,可是你是否想过,它到底是什么意思? man中的说明,我们能够看出total的 ...

  10. 8148和8127中的ezsdk和dvrsdk

    http://www.dajudeng.com/d2012081009b8a9d5de87101f69f31952c.html