摘要
也许您已经听说过Akka,一种用于建造可伸缩,弹性且高效应用程序的工具包,支持Java和Scala编程语言。Akka包括了16个开源或商业的模块,帮助开发者构建从单JVM的Actor到网络分区修复,跨多个JVM的分布式集群等功能。如此多的特性,架构师和开发者如何从高层的角度理解Akka?读完本系列白皮书,您将更好的理解Akka “从A到Z”,这趟旅程从简单的Actor开始,直到集群系统结束,您将会学到以下知识:
- Akka Actors如何运作,从创建系统到管理监督和路由
- Akka 使用Akka Streams 和 Alpakka实现Reactive Streams的方式
- 如何用Akka建造分布式集群系统,甚至集群中的集群
- 对于分布式数据,分布式持久化,发布-订阅 和 ES/CQRS,Akka工具包如何通过多种组件提供开箱即用的解决方案
- 可用的Akka商业版技术有哪些,它们是如何工作的
Akka的历史
Akka开始与1973(以IT世界的时间计算的话,相当于8000年前),在MIT的实验室,数学博士Carl Hewitt 合作发表了论文《A Universal Modular Actor Formalism for Artificial Intelligence》,在这篇论文中,介绍了一种数据模型,将actors作为并发计算的通用原子类型。
“提出Actor 模型是基于高度并行计算的前景,计算系统可能由成百上千的独立微处理器组成,每个处理器都拥有自己的本地内存和通讯处理器,可以通过高性能通讯网络在相互之间进行通讯”
--Carl Hewitt
在那个时候,跨多种云基础架构的低延迟并发计算在商业上并不可行,一直到企业级云计算的到来。
2009年,湾光公司的合伙人兼CTO Jonas Bonér,创建了Akka项目,希望在JVM上构造一种分布式,高并发,事件驱动的Actor模型实现,就像 Erlang语言实现的Actor模型一样。
现在到了2018年,离Akka十周年之剩一年之际,我们仍在继续我们的目标,将构建多核,异步,自愈,并且高伸缩性系统的能力带给Java和Scala的架构师和开发者,而无需关注所有隐藏在后的底层细节。
值得注意的是,Akka是一个JVM工具包,而不是一个框架(例如Play 或者 Lagom).虽然面临开源和商业组件的选择(Lightbend Enterprise Suite的一部分),本系列文章将关注在actors, streams,HTTP,集群,持久化和部署概念上。
这趟旅程将从独立的actors开始,介绍监督和自愈的行为模式,然后讲解通过Akka Streams, Akka HTTP 和 Alpakka实现Reactive Steams,再介绍事件Sroucing的分布式持久化和CQRS,微服务。最后到分布式集群以及如何管理,监控,编排集群。
了解了蓝图,让我们从Akka Actors开始吧。
第一部分 Akka Actors 及其工作方式
Actor 模型
让我们从Akka系统的原子或分子级别来看看actors,相对于大多数开发者来说,这是一种完全不同的编程方式。许多开发者学习过面向对象编程,甚至函数式编程,大多数情况下,这是必要的,同步方式编程设计到了很多线程和相关元素。
然而Actor 模型有很大不同,一个Actor本身是一个class,可以用Java或者Scala实现,actor具备方法,但是actor最大的不同在于actor的方法并不是在actor外部直接被别的类访问调用的。
不直接调用actor的方法,这需要改变原来的习惯。和actor交互的唯一方式是通过Akka提供的 actor system向actor发送消息。
在actors中,actor之间消息是异步发送的,而不是同步的,如图一所示,消息首先存入待处理消息收件箱,actor遍历收件箱,一个一个地处理消息。这就是actor的基本机制,Akka系统就是由许多个actor构成,actor之间互相通过发送消息进行通讯。
其工作原理如图2所示: actor A 发送消息给 actor B. 这是一个异步消息,就像两个人互相发送短信。A 发送给 B一条消息, A可以继续干别的事情,A并不会挂起等待B的回应,这和传统的面向对象的方法调用不一样,传统的方法调用必须等待方法返回。在这个例子中,A发了消息就继续往下执行了,或者执行别的代码,或者没有代码执行,啥也不干。
在图3中 B 收到A发送的消息,触发了状态的改变,例如B代表一个银行账户,消息代表取款动作的发生,因此状态改变即更新账户余额,如果需要,B可以给A发送一条回执消息:“已完成状态更新”
现实中,actors A和B可能运行在分布式环境,B可能在另一个机器上无法回应。从一开始,这就提出了关于该做什么的问题;B从A获得消息,完成任务,也许会发送一条回执消息。但这个流程也可能发生故障。
图4 展示了A能够直接向Akka的actor system报告一个请求: “请在未来某个时间点给我发送一个消息“,可以是几毫秒,几秒或者几分钟之后。这个使用的场景是A向B发送消息,得不到回应的情况下,一个超时的消息将会被发送。
在图5中,A获取到超时消息,A处理两种类型的消息:从B得到的回应消息或者超时消息。换句话说,A知道这两种情形下分别该如何处理,这和异常处理不同,大多数异常只是被抛出,而在Akka中,弹性(resilience)是架构中就考虑的特性,而不是事后的想法。
Actor监督 (自修复)
弹性是Akka设计的一部分,让我们来看看Actor的监督机制。Actor能够创建子Actor,形成Actor的层级关系,就像我们在图6中看到的一样。在这个例子中,actor A是监督者,创建了多个干活的actor,他们之间是一种"父-子"关系或者 “监督者-工作者”的关系。
监督者和工作者之间的关系是一种监督策略,当干活的actor处理消息遇到问题的时候,它自己并不处理这个异常,而是由监督者来处理,这种监督策略是定义好的,但也可以定制化。总之,对于工作actor出现的异常,监管actor始终有一个应对计划。
监督者A的响应包括:异常不严重的情况下恢复工作actor,继续执行;或者重启工作actor;或者这个问题实在太严重,需要停止工作actor;最极端的情况是,监督者也无法处理此问题,因此将问题向上报告给它的上级监督者。
因此,根据严重程度,问题可以在层次结构中逐级上升,这都处于开发者的控制之下。要记住的是,这是处理问题的明确方式。
对于actor系统的初学者,往往容易在actor中写入很多的逻辑。随着对actor模型越来越熟悉,会更容易地使用分而治之的策略。
分而治之的目的是为了降低风险,通过构建层次结构的Actor来代理有风险的操作,从而降低影响。
在图1-7中,我们有A1,B1和C3,C3将一些有风险的行为委托给actor D(D3),叶子节点用红色标记这些Actor做的是有风险的操作,例如通过网络和数据库通讯。有风险是因为网络可能中断,数据库可能宕机,或者其他的因素都可能导致错误。
通过将风险的行为推给下层工作的Actor,上层的Actor得到了某种程度的保护。通过Actor的层次结构,工作被委托给更加细粒度的专门的Actor,就像图中把风险行为推给树的叶子节点。
路由和并发
Akka专注于多线程,多核并发,使我们能轻松处理以前需要手工管理多线程的操作。多线程的事情很容易变复杂,所以Akka提供了开箱即用的并发特性,不会遇到Java和Scala中多线程编程的许多技术挑战。
在图1-8中,我们有Actor A和B,以及作为路由的actor R,R创建了一批工作的actor W,A发送消息给路由actor R,要求完成某项工作,R实际并不做这些工作,而是将工作代理给底下的actor W
在图-9中,当actor W在完成A的任务时,这时B发了一个消息给路由actor R,要求完成一些工作。R会转发消息给底下其他的某一个W,这样我们就实现了多线程并发而不用写任何代码。从A和B的视角看,R在同一时刻做了很多工作,但是R在幕后实际上是通过代理工作到许多actor W来实现的。
在图1-10中,我们再来看看这个例子中的Akka监督策略。假设A发送了一个消息给路由R,路由R再转发给实际工作的W,但是W在运行过程中出错了。
在常用的“try-catch”方法编程中,捕获的异常通常是发送给A。然而在actor系统中,是监督者收到这个异常。因此在这个场景中是监督者R收到了异常,而不是A
这就是超时机制起作用的地方了,R发送一个响应给A说:“这里有一些问题,我无法完成你要求的任务”。无论如何,A没有看到异常,作为调用者无需处理问题。而且作为服务的消费者,最好也不是由它来处理,而是由服务的提供者来处理。
Actor模型作为Akka的核心,提供了许多强大的内置特性来管理并发和弹性。它展现了一种不同的思维方式,是一种高效且安全的方法构建分布式系统的方法。
总结:
› Actors 是消息驱动的,有状态的构建模块,模块间异步传递消息。
› 与传统的同步和阻塞不同,Actors 是轻量的,不阻塞线程
› Actor能创建Actor,使用监督层次结构来支持从错误自愈(弹性)。