• Akka源码分析-Cluster-Singleton


      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 

  • 相关阅读:
    Oracle TRCA 工具 说明
    Oracle TTS ORA39322: Cannot use transportable tablespace with different timezone version 说明
    Oracle 传输表空间(Transportable Tablespaces) 示例(二) 跨操作系统迁移表空间(endianness格式不同)
    易于在各手机平台移植的设计(转)
    Symbian 学习资源
    项目经理修炼手册
    ZIP 算法详解 (转!)
    google Android OS 学习资源、资料和笔记汇总(要看的)
    小白兔的求职遭遇
    数据结构和算法学习笔记(1)
  • 原文地址:https://www.cnblogs.com/gabry/p/9488118.html
Copyright © 2020-2023  润新知