Akka实战:快速构建高可用分布式应用
上QQ阅读APP看书,第一时间看更新

第2章 走进Actor

开发一个功能完备的并行&分布式程序并不容易,我们需要考虑其中可能出现的任何问题。对于有经验的工程师来说,抽象出一个简单的并行模型也不算太复杂,我们使用JDK内置的并发包就可以轻松实现,但是这种做法大多在单机中运行良好,在分布式环境中可能并不适用,比如单机上的任务大多靠直接调用API来实现,而分布式环境下需要通过网络通信(通知)实现;单机程序中要得到一个返回结果,直接通过返回值得到,而分布式环境下需要通过消息回传来实现,诸如此类,在单机上的某些概念并不能无缝的推及到分布式环境中。

而Akka通过Actor模型很好地解决了这一问题,它认为,每个执行单元都应该是一个Actor,他们都有着自己的使命,并且分布在网络中的任何一个节点,在执行某个任务时,我们无须关心每个Actor的具体位置,无论在单机还是分布式环境下,它们都有着统一的操作方式。

2.1 Actor组件

2.1.1 Akka中的Actor

在Akka中,消息在不同Actor之间进行传递和处理,以此来驱动任务的执行,这和普通OOP的调用方式有明显的区别,在执行某个功能时,不会直接通过对象调用某个业务方法来实现。Actor提供良好易用的“问答”式API来进行消息的通信,并且理论上不限制任何类型的消息,它们可以是一个字符串,也可以是一个对象,但是为了保证消息的状态不受意外修改,最好使用不变类型的对象。

除此之外,Actor也拥有线程安全和轻量级的特点。

线程安全:Actor运行于线程池之上,但是几乎不用考虑它的安全性问题,因为单个Actor总是线程安全的,并且本身在处理接收到的消息时是串行的。

轻量级:在一个大型的应用中,可能同时运行着成千上万个Actor,这样会不会耗费太多资源?这点我们大可放心,在Akka中,每个Actor只占用300字节左右,即使单机内存不够用了,我们也可以很方便地切换成分布式模式。

2.1.2 ActorSystem与监管

在一个应用中,所有的Actor共同构成了Actor系统,即ActorSystem。它是一个层级结构,该结构已经限定了我们对某个Actor该施行怎样的管理策略,一般来讲,我们所用的Actor都可能有一个显式的父级Actor(除了顶级Actor外),它会处理所有自己子孙失败的情况,比较常见的场景是:当子Actor在处理消息时出现了异常情况,父Actor可以通过匹配预先设定的某个动作来处理子Actor,处理方式有:恢复子级、重启子级、停止子级、扩大化失败,这就是所谓的“父监督”模式。

图2-1 ActorSystem层级和监管结构(摘自官网)

那么父子关系是怎样定的呢?很简单,Actor被谁创建,谁就是父级。实际上,在Actor-System的创建过程中,会默认启动三个比较顶级的Actor,而我们应用程序里面创建的Actor只是其中的一个分支而已,如图2-1所示。

从图2-1中我们看到,Root guardian是整个ActorSystem的根,它下面有两个分支,左边的/user分支是我们最常见到的,所有通过ActorSystem.actorOf()方法创建的Actor都属于该分支下,这是我们能手动创建的最高级别Actor,其他通过ActorContext.actorOf()方法创建的Actor都是其子级(ActorContext即Actor的上下文对象,后面会多次使用到),而右边的/system分支下的Actor都是系统层面创建的,主要与系统整体行为有关,在开发阶段并不需要对其过多关注。

在使用ActorSystem时,我们也需要知道,它是一个非常重量级的对象,一般来讲,每个应用程序只需要一个ActorSystem对象。

2.1.3 生命周期监控

每个Actor都会经历“生老病死”的阶段,在任何时候,我们都需要谨慎的对待它每个阶段可能出现的状况或者需要执行的任务。Actor提供了生命周期管理的API,比如preStart、preRestart、postRestart、postStop等,可以让我们知道,当前这个Actor到底处于什么状况,是启动?重启?还是已经停止了。

当一个Actor停止时,有时候我们希望释放掉不需要的资源,但有时候,我们得为此做出一些更复杂的或者更全局的响应,此时引入一个新的Actor作为“死亡监控者(Death Watch)”是有必要的,当某个Actor被停止后,监控者(其实是另外一个Actor)可以收到一个Terminated消息,通过匹配该消息,监控者来决定到底该怎样做善后工作,这就是DeathWatch的做法。和上面讲的父监督比较类似,DeathWath其实也描述了两个Actor之间的关系,但是这种关系可以通过unwatch的方式解除,而父子关系从出生那一刻就决定了,嗯,这真的非常拟人化!

2.1.4 引用与路径

Actor不同于一般的Java对象,它既可能存在于本地,也可能存在于远程,但是对使用者来讲并没有明显的区分,仅仅只需要操作它的引用对象就行了,在Akka中,使用ActorRef来表示一个Actor的引用。ActorRef对原生的Actor实例做了良好的封装(也可以理解为一种代理),外界不能随意修改其内部状态,并且只能通过统一的方式去操作一个Actor。

ActorRef和Actor实例的关系如图2-2所示。

图2-2 ActorRef和Actor实例的引用关系

每个Actor在被创建后都拥有自己的路径,该路径遵循ActorSystem的层级结构,看起来是这个样子:

        akka://mysys/user/parentActor/childActor
        akka.tcp://mysys@127.0.0.1:2554/user/parentActor/childActor

其中mysys是ActorSystem的名字,/user是用户创建Actor的默认根节点,parentActor和childActor分别是父子Actor,如果是远程Actor,则需要给出IP和端口。通过路径,我们可以很方便地查找到一个Actor。