目录
Akka简介
Akka应用场景
Akka架构体系
Actor模型
体系结构
Actor组件
邮箱(Mailbox)
路由(Routing)
状态持久化( Persistence )
网络(远程和分布式集群)
HTTP 模块
相关开源项目
基本使用
环境搭建
Maven依赖
创建一个Actor
工厂模式
发送与接收消息
tell方法
ask方法
消息转发
查找Actor
消息不可变
Actor行为切换
Actor生命周期
创建并启动
恢复运行
重启
停止
停掉一个Actor
监督与容错处理
熔断(circuit breaker)
Close 状态
Open 状态
Half-Open 状态
配置
Akka 是一款高性能、高容错性的分布式&并行应用框架,遵循 Apache 2 开源许可,底层通过 JVM 上另外一个流行的语言 Scala 实现,提供Java&Scala API 。它基于经典的 Actor 并发模型(即所有的消息都是基于 Actor 组件进行传递),拥有如下特点:
目前 Akka 已经在多家互联网&软件公司广泛使用,比如 eBay、Amazon、VMWare、PayPal、阿里、惠普、豌豆荚等,涉及行业包括游戏、金融投资、医疗保健、数据分析等。
使用场景包括:
总之,对高并发和密集计算的系统,Akka 都是适用的!
Akka 采用 Scala 开发,运行于 JVM 之上,提供了 Scala 和 Java 两种 API,目前属于 Lightbend 公司(原名 Typesafe)。它实现了经典的 Actor 模型,同时也提供了丰富的组件,比如邮箱(MailBox)、路由(Routing)、持久化( Persistence )、网络(包括远程、集群)等,在底层对分布式&并行模式进行了高度且统一的抽象,使工程师用很少的代码就可以实现一个完整的分布式应用。
Actor 模型最早在 1973 年由 Carl Hewitt 提出,它高度抽象了分布式并行程序的运行模式,从底层屏蔽了线程和锁机制的管理,为开发者提供了简单可依赖的开发方式。
Actor 模型认为,并行计算的最小单元就是一个 Actor 实例,而每个实例拥有自己的状态和行为,在一个大型系统中,可能存在成千上万个 Actor 实例,它们之间通过消息的方式进行通信,每个 Actor 都能发送消息给其他 Actor,也能从其他 Actor 接收消息。当我们在执行某个计算任务时,会给对应的 Actor 实例发送一个相关的消息,该 Actor 在接收消息后开始执行计算任务,由于整个消息通信的过程是异步的,所以不用等到 Actor 执行完整个过程就能执行下一步(发送消息后会马上返回),这种异步通信的方式大大提高了程序的响应性。
Actor 是 Akka 最核心的概念,也是最基本的执行单元,所以对 Actor 管理和监控的有效性是极为重要的。在 Akka 中,每个 Actor 都有自己的监管对象,即该 Actor 的创建者,它们通常会负责子 Actor 的失败处理,另外,某些 Actor 也需要对生命周期进行监控(比如该 Actor 的终止),以便及时响应并做正确处理,这些监督和监控者本身也都是一个 Actor。在 Akka 中,整个 Actor 体系被抽象成一个 ActorSystem ,它是一个层级的结构,拥有公共行为的配置和管理。
在 ActorSystem 基础上,Akka 也提供了一些配套的组件,比如持久化,HTTP 服务,网络服务等,它们都是构建高可用分布式应用不可或缺的部分,基本架构体系和周边产品如下图所示。
在 Akka 中,Actor 是一个高度抽象的对象引用,它包含以下几个要素:
每个 Actor 都有自己的邮箱,所有其他 Actor 发送过来的消息都会进入该邮箱。Akka 自带多种邮箱类型,也提供自定义邮箱的接口。
消息除了通过普通的 Actor 发送之外,也可以通过路由发送。当通过路由发送消息时,我们可以根据需求来选择不同的路由策略,比如轮询、广播等。路由也可以是一个 Actor。
任何程序都有失败的可能,即便是 JVM 如此强大稳定的平台也一样。当程序出错、JVM 崩溃时,任何关键状态的丢失,对我们后续的业务来讲都可能是致命的打击,所以状态数据的持久化变得非常重要。Akka 提供了 Actor 状态的持久化方案,以便我们在必要时恢复数据。
网络功能是实现远程 Actor 和分布式集群的基础,这其中包含 I/O、网络通信(TCP/UDP)、序列化配置、分布式通信协议(Gossip)、节点(node)管理、集群分片等内容。
Akka 提供了简单易用的 HTTP 模块,支持完整的 HTTP 服务端与客户端开发,可以帮助我们快速构建性能极强的 Rest Web 服务。
Akka 具有高性能、可扩展、设计友好等诸多优点,非常适合用来作为分布式应用的基础框架,而且由于对 HTTP 有非常好的支持,也让它在 Web 服务领域占有一席之地。目前业界已经有多个基于 Akka 实现的开源项目,项目类型涵盖了 Web 开发、微服务、分布式文件或计算服务等。下面是 Akka 中两个具有代表性的开源项目:
JDK版本最好是在 1.6 以上,不过后续会介绍Akka的一些新特性,所以最好采用1.8的版本。
使用最新版akka依赖。
com.typesafe.akka
akka-actor_2.12
2.5.23
注意Akka中不同组件分为了不同的模块,我们这里暂时只是引用了核心模块,通过它可以使用akka的最基本功能。除开核心模块还包括akka-persistence 、akka-remote 、akka-cluster 等模块,分别用于实现持久化、远程、集群等功能。
public class ActorDemo extends AbstractActor {
private LoggingAdapter log = Logging.getLogger(this.getContext().getSystem(), this);
@Override
public Receive createReceive() {
return receiveBuilder()
.match(
String.class,
s -> log.info(s))
.matchAny(o -> {
log.info("any" + o.toString());
})
.build();
}
public static void main(String[] args) {
ActorSystem system = ActorSystem.create("sys");
ActorRef actorRef = system.actorOf(Props.create(ActorDemo.class), "actorDemo");
actorRef.tell("hello world", ActorRef.noSender()); // ActorRef.noSender()实际上就是叫做deadLetters的actor
}
}
ActorSystem 和 ActorContext 通过接收一个 Props 实例来创建 Actor,而 Props 实例本身有两种方式可以创建:
对于比较简单的场景使用第一种就足够了,而如果需要按统一的规则创建Actor,我们可以在Actor内定义一个创建Props的方法,比如:
public static Props createProps() {
return Props.create(new Creator() {
@Override
public Actor create() throws Exception {
return new HelloWorldActor();
}
});
}
然后可以使用一下代码来创建Actor:
ActorRef actorRef = system.actorOf(ActorDemo.createProps(), "actorDemo");
可以看出,如果我们使用工厂模式,在创建Actor时调用工厂方法,我们不用关心Actor创建的具体过程了,有Actor自己封装即可。
Actor中的createReceive方法是用来接收并处理消息的。
createReceive方法需要放回Receive对象,而Receive对象就是真正用来处理消息的逻辑。
Receive对象可以定义规则使接收到的消息进入不同的处理逻辑:
type):消息类型为指定type的进入此分支
当然,一个消息只会进入到一个分支,如果一个消息匹配多个条件优先进入定义靠前的条件。
可以使用tell和ask两种方式发送消息,它们都以异步的方式发送消息,不同的是,前者发完后立即返回,而后者期待得到一个返回结果,假如在设置的时间(Timeout)内没有得到返回结果,发送方会收到一个超时异常。
第一个参数即"消息",它可以是任何可序列化的数据或对象,第二个参数表示发送者,noSender()表示无发送者(实际上是一个叫做 deadLetters 的 Actor,后面介绍)。假如想在Actor内部得到发送者,可以调用getSender ()方法。
在调用 tell 方法后,Actor 就会异步的去处理该消息,并不会阻塞后续代码的运行。
这是一种“请求-响应”模式,ask方法会将返回结果包装在scala.concurrent.Future中。
对于一个Actor如何将响应返回给消息发送者呢,其实就是想响应作为消息发送给请求消息的发送者:
getSender().tell( "hello " +msg , getSelf());
使用样例:
public class AskDemo extends AbstractActor {
@Override
public Receive createReceive() {
return receiveBuilder().matchAny(o -> {
System.out.println("发送者是" + getSender());
Thread.sleep(1000);
getSender().tell("hello " + o, getSelf());
}).build();
}
public static void main(String[] args) {
ActorSystem system = ActorSystem.create("sys");
ActorRef askActor = system.actorOf(Props.create(AskDemo.class), "askActorDemo");
Timeout timeout = new Timeout(Duration.create(2, TimeUnit.SECONDS));
Future
Actor可以将接收到的消息转发给其他Actor,语法为:
static class ForwardActor extends AbstractActor {
private ActorRef target = getContext().actorOf(Props.create(TargetActor.class), "targetActor");
@Override
public Receive createReceive() {
return receiveBuilder().matchAny((message) -> {
target.forward(message, getContext());
}).build();
}
}
TargetActor接收到转发过来的消息后,发送者任然是消息原始的发送者,而不是转发者。
对于已存在的 Actor,我们可以根据路径来进行查找,API 如下:
ActorSelection as = [ActorSystem/ActorContext].actorSelection([path]);
在调用它时,必须指定一个绝对或者相对路径(包括远程路径),比如:
查找结果为ActorSelection,表示一个或多个Actor,所以向ActorSelection发送消息时,对应的一个或多个Actor都将接收到消息。
Akka中都是消息驱动的,所以消息是非常重要。理论上,Actor 可以传递或接收任何类型的消息,但是为了避免出现竞态条件(对共享资源的并发写)和不可预知的数据篡改,最好把消息设计成不可变的对象。在该对象中,我们通常会把所有变量设计成只读,在该对象的整个生命周期内,这些数据将不可修改。所以在 Java 中,我们借助final关键字来实现。
对于集合框架,有必要通过 Collections.unmodifiable 系列的 API 来保证不变性。Collections.unmodifiable 内部使用 UnmodifiableList 、UnmodifiableMap 等 API 对现有的集合进行包装,它们通过重写其中的 set、add、put 等方法来达到限制数据修改的目的。
在业务处理过程中,往往会面临一些关键数据的变化,而这些变化也会进一步影响着程序的执行逻辑,很多时候,我们把这些关键数据称之为「状态」。状态的处理需要依赖于具体的业务,而业务本身也可能充满不确定性,一种比较好的做法是,我们把多个状态的处理过程(即行为)封装起来,形成一个个独立的组件,在使用时就可以很方便地组装、重用或切换。在 Actor 中,行为可以通过Receive来定义,一旦抽取完我们所需要的Receive,就可以使用 become/unbecome 方法来切换他们,其中 become 表示切换为某个行为,unbecome 表示修改回上一个行为。
Actor 在运行时中会经历不同的阶段,比如创建、运行、重启和销毁等,这一系列的过程或状态,我们称之为生命周期。在理想情况下,Actor 应该像一头老黄牛一样任劳任怨地不停工作,但实际情况往往是,当某个不算常态的错误发生时(比如网络偶尔超时),我们想让它重启一下,然后重复之前的动作;又或者,当一个程序异常被抛出时,我们不得不停止该 Actor,让它不能继续走下去,这些行为有可能是手动触发的,也可能是 Actor 的监督-容错机制导致的。无论怎样,当这些行为发生时,我们都希望能及时感知并做有效处理。
Actor 生命周期主要包括启动(Start)、恢复(Resume)、重启(Restart)、停止(Stop)这四个阶段,每个阶段都会伴随着自身状态信息的变化。
当通过 actorOf 创建并启动 Actor 时,该 Actor 除了默认拥有已指定的 path 外,也会被分配一个 UID(可以通过 getSelf().path().uid()得到该值),作为它的唯一标识。在 Actor 启动后,会默认调用 preStart 方法,在该方法里,我们可以做些资源初始化的操作。
当 Actor 出现某种异常后,通过容错机制,可以让该 Actor 恢复并继续运行,此时 Actor 会延用之前的实例,状态也会被保留下来。
当 Actor 出现某种异常后,通过容错机制,可以让该 Actor 执行重启。重启的具体过程如下:
Actor 重启后,path 和 UID 不变,这意味着假如要继续使用该 Actor,不需要重新获取 ActorRef 对象,使用之前那个就可以了。同时也得注意到:Actor 重启后不会保留自身状态。
当 Actor 停止时,会调用 postStop 方法,同时会发送一条 Terminated信息给自己的监控者,告知自己已处于停止状态。
停止 Actor 大致有三种方式,它们分别是:
Actor 在停止时都会遵循一套比较可靠的流程:
可以使用getContext().watch();方法监控某个节点的停止信息,当该节点停止后,监控者会接收到一个Terminated消息。
Actor 系统采用"父监督"的模式进行管理,即父 Actor 会监督子 Actor 的异常情况,然后根据默认或者预设的处理逻辑来确定到底是该恢复 Actor、停止 Actor、重启 Actor 还是把错误上溯到父级。
Akka 提供了两种监督策略,分别是 One-For-One Strategy 和 All-For-One Strategy ,前者表示当一个子 Actor 出现异常时,只对该 Actor 做处理,后者则表示对所有子 Actor 都做处理,大部分时候应该选择 One-For-One Strategy (这也是默认的策略),除非子 Actor 之间的业务有很强的关联或者互相依赖。当程序中没有显式指定策略时,会启动一个默认策略:
在 Actor 执行任务时可能会抛出不同类型的异常,很多时候,我们应该知道,哪些异常需要让 Actor 停止或重启,哪些异常可以「视而不见」。为了更加细化这种判断,我们需要自定义监督逻辑。
自定义一个监督逻辑非常简单,只需要在父 Actor 中创建一个SupervisorStrategy 对象,并通过 supervisorStrategy ()方法返回出来.
当一个Actor出现异常时,如果用户还继续向这个Actor发送消息,很有可能这个消息还是无法被处理,还额外的增加了服务器的压力,Akka中提供了熔断机制(circuit breaker)。
Circuit Breaker 状态图(图片来自 Akka 官网)
circuit breaker有三个状态,分别是:Closed、Open、Half-Open。
Akka 程序在启动时会加载一些默认的配置项,所以我们不需要显式地提供配置文件,假如要个性化配置某些行为,则需要新建配置文件。在默认情况下,程序会加载 classpath 下的 application.conf 、application.json 、application.properties 文件。
在创建ActorSystem 时,可以显式加载自定义的配置文件,比如app.conf ,代码如下:
ActorSystem system = ActorSystem.create( "myapp",ConfigFactory.load( "app.conf" ));
Akka 提供的配置项涵盖了几乎所有的核心功能,比如日志、远程、路由、消息序列化、分布式集群等.