6.2.4 远程部署

远程配置可以通过编程或者配置来完成。 我们从比较偏爱的方式开始:配置。 当然,这个方法比较被偏爱是因为对集群设置的改变可以不用重新编译应用就可以实现。 标准的 BoxOfficeCreator trait 创建 boxOffice 作为它混入的 Actor 的一个孩子,即 RestInterface:

val boxOffice = context.actorOf(Props[BoxOffice], "boxOffice")

到这个 actor 的本地路径是 /restInterface/boxOffice, 省去了用户监控 actor。 当我们使用配置的远程部署,我们所有要做的就是告诉前端 Actor 系统,当使用路径 /restInterface/boxOffice 来创建 actor 时,它不应该创建本地的,而应该是远程的。 这是通过使用列表 6.9 中的配置片段来实现的:

Listing 6.9 配置 RemoteActorRefProvider

actor {
    provider = "akka.remote.RemoteActorRefProvider"
    deployment {
        /restInterface/boxOffice {
            remote = "akka.tcp://[email protected]:2552"
        }
    }
}
  • 使用这个路径的 actor 会被远程部署
  • actor 应该被部署到的地址。ip 地址或者主机名应该与远程的后台 actor 系统正在监听的网卡接口完全一致。

为了完整性,我们也来展示一下以编程的方式来完成远程部署。 多数情况下,最好是通过配置系统来配置 actors 的远程部署,但是在某些情况下,也许你通过 CNAMES (其本身是可配置的)来引用不同的节点,你可能通过代码来进行配置。 当使用 akka-cluster 模块时,完全动态的远程部署更有意义,因为它是专为支持动态成员构建的。 列表 6.10 展示了一个用程序进行远程部署的例子。

Listing 6.10 用程序进行远程部署配置

val uri = "akka.tcp://[email protected]:2552"
val backendAddress = AddressFromURIString(uri)
val props = Props[BoxOffice].withDeploy(
    Deploy(scope = RemoteScope(backendAddress))
)
context.actorOf(props, "boxOffice")
  • 从 uri 创建到后台的地址
  • 使用远程部署范围来创建 Props

上述代码创建,同时远程部署 boxOffice 到后台。 Props 配置对象指定了远程部署的范围。

远程部署不要求 Akka 以某种方式将 BoxOffice actor 的实际 class 文件自动部署到远程 actor 系统,注意到这一点很重要;BoxOffice 的代码需要已经在远程 actor 上了,且远程 actor 系统需要正在运行。 如果远程后台 actor 系统崩溃并重启, ActorRef 不会自动指向新的远程 actor 实例。 既然 actor 要远程部署,它不能已经由后台 actor 系统启动,如我们在 BackendMain 所做的那样。 因为这一点,需要做一些改变。 下面是 Main classes 的定义:

// the main class to start the backend node.
object BackendRemoteDeployMain extends App {
    val config = ConfigFactory.load("backend")
    val system = ActorSystem("backend", config)
}

object FrontendRemoteDeployMain extends App {
    val config = ConfigFactory.load("frontend-remote-deploy")
    val host = config.getString("http.host")
    val port = config.getInt("http.port")
    val system = ActorSystem("frontend", config)
    val restInterface = system.actorOf(Props[RestInterface],
        "restInterface")
    Http(system).manager ! Bind(listener = restInterface,
        interface = host,
        port =port)
}
  • 不再创建 boxOffice actor
  • 不再混入指定的 trait,使用默认的 BoxOfficeCreator

当你用两个终端像以前一样运行主类,使用 httpie 创建活动,你会在前台 actor 系统的控制台上看到类似如下的信息:

// very long message, formatted in a couple of lines to fit.
INFO  [RestInterface]: Received new event Event(RHCP,10), sending to
Actor[akka.tcp://[email protected]:2552/remote/akka.tcp/
    [email protected]:2551/user/restInterface/boxOffice#-1230704641]

这表明前台 actor 系统在实际发送消息给远程部署的 boxOffice。 actor 路径与你期待的不同。 它记录了 actor 是从哪里部署的。 为后台的 actor 系统监听的远程守护进程使用这个信息来与前台 actor 系统通信。

到目前为止,我们所做的都可以工作,但是用这个方法有一个问题。 如果当前台视图部署远程 actor 时,后台 actor 系统没有启动,部署显然会失败,但是不明显的是,ActorRef 仍然创建了。 即使后台之后启动了,创建的 ActorRef 也不能工作。 这是正确的行为,因为它不是同一个 actor 实例。 (与之前我们看到的失败的例子不同,其中只有 actor 本身被重启,这样引用仍然只想创建的 Actor)

如果在远程后台崩溃,或者远程 boxOffice actor 崩溃时,我们想做点什么,就需要更多地改变。 我们需要像之前那样监视 boxOffice ActorRef,在崩溃发生时采取动作。 因为 RestInterface 有一个变量指向 boxOffice,我们需要再次使用 RemoteLookup 放入一个 actor,像之前所做的一样。 这个介于中间的 actor 称为 RemoteBoxOfficeForwarder。

配置需要稍微改变,因为 boxOffice 现在的路径是 restInterface/forwarder/boxOffice,这是由于介于中间的 RemoteBoxOfficeForwarder。 替代原来部署去的 /restInterface/boxOffice 路径,它现在应该是 /restInterface/forwarder/boxOffice。

Listing 6.11 展示了 ConfiguredRemoteBoxOfficeDeployment trait 和监视元车工部署的 actor 的 RemoteBoxOfficeForwarder

Listing 6.11 远程 Actors 监视机制

trait ConfiguredRemoteBoxOfficeDeployment
    extends BoxOfficeCreator { this:Actor =>
    override def createBoxOffice = {
        context.actorOf(Props[RemoteBoxOfficeForwarder],
        "forwarder")
    }
}

class RemoteBoxOfficeForwarder extends Actor with ActorLogging {
    context.setReceiveTimeout(3 seconds)
    deployAndWatch()

    def deployAndWatch(): Unit = {
        val actor = context.actorOf(Props[BoxOffice], "boxOffice")
        context.watch(actor)
        log.info("switching to maybe active state")
        context.become(maybeActive(actor))
        context.setReceiveTimeout(Duration.Undefined)
    }

    def receive = deploying

    def deploying:Receive = {
        case ReceiveTimeout =>
            deployAndWatch()
        case msg:Any =>
            log.error(s"Ignoring message $msg, not ready yet.")
    }

    def maybeActive(actor:ActorRef): Receive = {
        case Terminated(actorRef) =>
            log.info("Actor $actorRef terminated.")
            log.info("switching to deploying state")
            context.become(deploying)
            context.setReceiveTimeout(3 seconds)
            deployAndWatch()
        case msg:Any => actor forward msg
    }
}
  • 创建一个 forwarder 来监视和部署远程的 BoxOffice
  • 远程部署和监视 BoxOffice
  • 监视远程 BoxOffice 是否终止
  • 一旦 actor 部署了,切换到 ‘maybe action’,如果 actor 部署了,不查询我们就不能确定
  • 部署的 boxoffice 终止了,所以必然需要重新部署

上面的 RemoteBoxOfficeForwarder 看起来非常类似前一节中的 RemoteLookup,因为它也是一个状态机,它在两种状态之一:‘正在部署’和‘可能活动’。 不查询 actor selection,我们不能确定远程 actor 是否部署。 增加使用到 RemoteBoxOfficeForwarder 的 actorSelection 来查询的练习留给读者,‘可能活动’状态现在是够用了。

前台的主类需要调整,需要将 ConfiguredRemoteBoxOfficeDeployment 混入 RestInterface。 FrontendRemoteDeployWatchMain 类展示了这个 trait 是如何混入的:

class RestInterfaceWatch extends RestInterface
    with ConfiguredRemoteBoxOfficeDeployment

val restInterface = system.actorOf(Props[RestInterfaceWatch],
    "restInterface")

在两个 sbt 控制台终端上运行 FrontendRemoteDeployWatchMain 和 BackendRemoteDeployMain 来展示远程部署 actor 是如何被监视的,当后台进程被杀死后,或者前台比后台先启动,又是如何被重新部署以及重启的。

如果你读了上一段,仍然不明白,就再读一遍。 当其在之上运行的节点重新出现并继续工作时,应用自动重新部署 actor。 这是很酷的事,我们还是只接触了表面。

这总结了本节的远程部署。 我们看了远程查询和远程部署,以及以弹性方式做时要求的东西。 即使只有两台服务器的情形,从一开始就考虑弹性也有很大的好处。 在查询和部署的例子中,节点都可以按任意顺序启动。 远程部署的例子可以简单地通过修改部署配置来完成,但是我们可能做出一个幼稚的方案,其中没考虑节点或 actor 崩溃,还需要特定的启动顺序。

在 6.2.5 节,我们来看看多个 jvm 的 sbt 插件,以及可以测试 goticks 应用的前台和后台节点的 akka-multi-node-testkti。