Akka -构建与JVM上的 高并发、分布式和可快速恢复的消息驱动应用的工具集
我们相信编写出正确的具有容错性和可扩展性的并发程序太困难了。这多数是因为我们使用了错误的工具和错误的抽象级别。Akka就是为了改变这种状况而生的。通过使用Actor模型我们提升了抽象级别,为构建正确的可扩展并发应用提供了一个更好的平台(工具)。
那什么是 actor模型?
(Actor model)是一种并发运算上的模型。“参与者”是一种程序上的抽象概念,被视为并发运算的基本单元:当一个参与者接收到一则消息,它可以做出一些决策、创建更多的参与者、发送更多的消息、决定要如何回答接下来的消息。参与者模式早在1973年于Carl Hewitt、Peter Bishop及Richard Steiger的论文中就已经提出
* 参与者模型推崇的哲学是“一切皆是参与者”,这与面向对象编程的“一切皆是对象”类似,但是面向对象编程通常是顺序执行的,而参与者模型是并行执行的。
* 发送者与已经发送的消息解耦,是参与者模型的根本优势。这允许进行异步通信,同时满足消息传递的控制结构
* 消息接收者是通过地址区分的,有时也被称作“邮件地址”。因此参与者只能和它拥有地址的参与者通信。它可以通过接受到的信息获取地址,或者获取它创建的参与者的地址。
* 参与者模型的特征是,参与者内部或之间进行并行计算,参与者可以动态创建,参与者地址包含在消息中,交互只有通过直接的异步消息通信,不限制消息到达的顺序。
ok.. 我们总结一些,(换种比较接地气的说法),在基于actor 模型构建的系统中,
- 我们 用actor 代替 对象 的叫法,可以直接把 TA 们当作一个人
- 对象之间的调用,变为了 消息的传递, 消息传递的过程是异步的。
- 每个actor 天生就是并行的,TA 一直在忙碌的查收着自己的邮箱,“批阅着各种奏章”
- actor 之间是 通过地址 (邮件)来互相 找到对方的
- actor 也是 人类 社会体统一样 有着层次的关系
基于这个模型 实现了独特的混合模型
actors
- 对并发/并行程序的简单的、高级别的抽象
- 异步、非阻塞、高性能的事件驱动编程模型
- 非常轻量的事件驱动处理(1G内存可容纳约250万个actors)
容错性(Fault Tolerance)
- 使用“let-it-crash”语义和监管者树形结构来实现容错
- 监管者树形结构可以跨多个JVM来提供真正的高容错系统
- 非常适合编写永不停机、自愈合的高容错系统。
路径透明(Location Transparency)
Akka的所有元素都为分布式环境而设计: 所有actor都仅通过发送消息进行互操作,所有操作都是异步的
持久化(Persistence)
- 被actor 接收的消息,可以随意的 持久化 和 重放(重新读取)当actor 启动之后 候或者 重新启动
- 所有actor都仅通过发送消息进行互操作,所有操作都是异步的
- 这样就允许 actor 重置他们的状态,当 jvm crash 的时候 或者 这个actor 迁移到其他 节点(node) 上。
akka 中的actor
一个Actor是一个容器,它包含了 状态,行为,一个邮箱,子Actor和一个监管策略。所有这些包含在一个Actor Reference里。
Actor Reference(引用)
- actor 需要与外界隔离开才能从actor模型中获益。所以actor是以actor引用的形式展现给外界的,actor引用可以被自由的无限制地传来传去
- 内部对象和外部对象的这种划分使得所有想要的操作能够透明:
- 重启actor而不需要更新别处的引用
- 将实际actor对象放置到远程主机上
- 不同应用中 ,相互发送消息
- 屏蔽内部状态
State(状态)
- Actor对象通常包含一些变量来反映actor所处的可能状态。这可能是一个明确的状态机(e.g. 使用 FSM 模块),或是一个计数器,一组监听器,待处理的请求,等等。这些数据使得actor有价值,并且必须将这些数据保护起来不被其它的actor所破坏。{color}好消息是在概念上每个Akka actor都有它自己的轻量线程,这个线程是完全与系统其它部分隔离的。这意味着你不需要使用锁来进行资源同步,可以完全不必担心并发性地来编写你的actor代码。
- 在幕后,Akka会在一组线程上运行一组Actor,通常是很多actor共享一个线程,对某一个actor的调用可能会在不同的线程上进行处理。Akka保证这个实现细节不影响处理actor状态的单线程性。
- 由于内部状态对于actor的操作是至关重要的,所以状态不一致是致命的。当actor失败并由其监管者重新启动,状态会进行重新创建,就象第一次创建这个actor一样。这是为了实现系统的“自愈合”。
Behavior(行为)
- 每次当一个消息被处理时,消息会与actor的当前的行为进行匹配。
- 行为是一个函数,它定义了处理当前消息所要采取的动作,例如如果客户已经授权过了,那么就对请求进行处理,否则拒绝请求。
- 这个行为可能随着时间而改变,例如由于不同的客户在不同的时间获得授权,或是由于actor进入了“非服务”模式,之后又变回来。{color:#000000}Actor对象通常包含一些变量来反映actor所处的可能状态。这可能是一个明确的状态机(e.g. 使用 FSM 模块),或是一个计数器,一组监听器,待处理的请求,等等。这些数据使得actor有价值,并且必须将这些数据保护起来不被其它的actor所破坏。好消息是在概念上每个Akka actor都有它自己的轻量线程,这个线程是完全与系统其它部分隔离的。这意味着你不需要使用锁来进行资源同步,可以完全不必担心并发性地来编写你的actor代码。{color}
行为匹配,不是消息匹配。 行为是指函数, 处理 特定消息的函数。
换种说法
我们定义了3个receive 方法。 命名 receive-a,receive-b,receive-c
根据不同情况下,切不同的行为。 支持热切换。(这一概念,可以参考 FSM)在幕后,Akka会在一组线程上运行一组Actor,通常是很多actor共享一个线程,对某一个actor的调用可能会在不同的线程上进行处理。Akka保证这个实现细节不影响处理actor状态的单线程性。
由于内部状态对于actor的操作是至关重要的,所以状态不一致是致命的。当actor失败并由其监管者重新启动,状态会进行重新创建,就象第一次创建这个actor一样。这是为了实现系统的“自愈合”。
Mailbox(邮箱)
- Actor的用途是处理消息,这些消息是从其它的actor(或者从actor系统外部)发送过来的。连接发送者与接收者的纽带是actor的邮箱:每个actor有且仅有一个邮箱,所有的发来的消息都在邮箱里排队。排队按照发送操作的时间顺序来进行,这意味着从不同的actor发来的消息在运行时没有一个固定的顺序,这是由于actor分布在不同的线程中。从另一个角度讲,从同一个actor发送多个消息到相同的actor,则消息会按发送的顺序排队。
- 可以有不同的邮箱实现供选择,缺省的是FIFO:actor处理消息的顺序与消息入队列的顺序一致。这通常是一个好的选择,但是应用可能需要对某些消息进行优先处理。在这种情况下,可以使用优先邮箱来根据消息优先级将消息放在某个指定的位置,甚至可能是队列头,而不是队列末尾。如果使用这样的队列,消息的处理顺序是由队列的算法决定的,而不是FIFO。
- Akka与其它actor模型实现的一个重要差别在于当前的行为必须处理下一个从队列中取出的消息,Akka不会去扫描邮箱来找到下一个匹配的消息。无法处理某个消息通常是作为失败情况进行处理,除非actor覆盖了这个行为。
Children(子actor)
- 每个actor都是一个潜在的监管者:如果它创建了子actor来委托处理子任务,它会自动地监管它们。子actor列表维护在actor的上下文中,actor可以访问它。
- 对子actor列表的更改是通过创建(context.actorOf(...))或者停止(context.stop(child))子actor来实现,并且这些更改会立刻生效。实际的创建和停止操作在幕后以异步的方式完成,这样它们就不会“阻塞”其监管者。
Supervisor Strategy(监管策略)
- Actor的最后一部分是它用来处理其子actor错误状况的机制。错误处理是由Akka透明地进行处理的
- 由于策略是actor系统组织结构的基础,所以一旦actor被创建了它就不能被修改。
- 考虑对每个actor只有唯一的策略,这意味着如果一个actor的子actor们应用了不同的策略,这些子actor应该按照相同的策略来进行分组,生成中间的监管者,又一次倾向于根据任务到子任务的划分来组织actor系统的结构。
actor system (站在系统的角度去看)
从某种意义上来说,actor是面向对象的最严格的形式,但是最后把它们看成一些人:在使用来对解决方案建模时,把actor想象成一群人,把子任务分配给他们,将他们的功能整理成一个有组织的结构,考虑如何将失败逐级上传(好在我们并不需要真正跟人打交道,这样我们就不需要关心他们的情绪状态和道德问题)。这样的结果就可以在脑中形成进行软件实现的框架。
树形结构
- 象一个经济学组织一样,actor自然会形成树形结构。程序中负责某一个功能的actor可能需要把它的任务分拆成更小的、更易管理的部分。为此它启动子Actor并监管它们。即 每个actor有且仅有一个监管者,就是创建它的那个actor.
- Actor 系统的精髓在于任务被分拆开来并进行委托,直到任务小到可以被完整地进行处理。这样做不仅使任务本身被清晰地划分出结构,而且最终的actor也能按照它们“应该处理的消息类型”,“如何完成正常流程的处理”以及“失败流程应如何处理”来进行解析。如果一个actor对某种状况无法进行处理,它会发送相应的失败消息给它的监管者请求帮助。这样的递归结构使得失败能够在正确的层次进行处理。
- 可以将这与分层的设计方法进行比较。分层的设计方法最终很容易形成防护性编程,以防止任何失败被泄露出来。把问题交由正确的人处理会是比将所有的事情“藏在深处”更好的解决方案
监管
监管描述的是actor之间的关系:监管者将任务委托给下属并对下属的失败状况进行响应。当一个下属出现了失败(i.e. 抛出一个异常),它自己会将自己和自己所有的下属挂起然后向自己的监管者发送一个提示失败的消息。取决于所监管的工作的性质和失败的性质,监管者可以有4种基本选择
- 让下属继续执行,保持下启下属,清除下属的内部状态属当前的内部状态
- 重启下属,清除下属的内部状态
- 永久地终止下属
- 将失败沿监管树向上传递
重要的是始终要把一个actor视为整个监管树形体系中的一部分,这解释了第4种选择存在的意义(因为一个监管者同时也是其上方监管者的下属),并且隐含在前3种选择中:
- 让actor继续执行同时也会继续执行它的下属,
- 重启一个actor也必须重启它的下属,
- 相似地终止一个actor会终止它所有的下属
让actor继续执行同时也会继续执行它的下属,重启一个actor也必须重启它的下属,相似地终止一个actor会终止它所有的
- actor被挂起,调用旧实例的 supervisionStrategy.handleSupervisorFailing 方法 (缺省实现为挂起所有的子actor)
- 调用旧实例的 preRestart hook (缺省实现为向所有的子actor发送终止请求并调用 postStop)
- 等待所有子actor终止直到 preRestart 最终结束
- 调用旧实例的 supervisionStrategy.handleSupervisorRestarted 方法 (缺省实现为向所有剩下的子actor发送重启请求)
- 再次调用之前提供的actor工厂创建新的actor实例
- 对新实例调用 postRestart恢复运行新的actor
提actor路径 前,再过一次 actor 引用
前面提到了actor引用的由来,和作用。这里更进一步介绍实际的内容。Actor引用是 ActorRef 的子类,它的最重要功能是支持向它所代表的actor发送消息。每个actor通过self来访问它的标准(本地)引用,在发送给其它actor的消息中也缺省包含这个引用。反过来,在消息处理过程中,actor可以通过sender来访问到当前消息的发送者的引用。
根据actor系统的配置,支持几种不同的actor引用
- 纯本地引用使用在配置为不使用网络功能的actor系统中。这些actor引用不能在保持其功能的条件下从网络连接上向外传输。
- 支持远程调用的本地引用使用在支持同一个jvm中actor引用之间的网络功能的actor系统中。为了在发送到其它网络节点后被识别,这些引用包含了协议和远程地址信息
- 本地actor引用有一个子类是用在路由(routers, i.e. mixin 了 Router trait的actor). 它的逻辑结构与之前的本地引用是一样的,但是向它们发送的消息会被直接重定向到它的子actor。
- 远程actor引用代表可以通过远程通讯访问的actor,i.e. 从别的jvm向他们发送消息时,Akka会透明地对消息进行序列化
ps: 使用过程中 没有任何差别。。 因为他们都是ActorRef 的子类,而我们使用的 正是这个顶级抽象的 类。
actor路径
- 由于actor是以一种严格的树形结构样式来创建的,沿着子actor到父actor的监管链一直到actor系统的根存在一条唯一的actor名字序列。这个序列可以类比成文件系统中的文件路径,所以我们称它为“路径”。就象在一些真正的文件系统中一样,也存在所谓的“符号链接”,i.e. 一个actor可能通过不同的路径访问到,除了原始路径外,其它的路径都包含到actor实际的监管祖先链的转换方法。这些特性将在下面的内容中介绍。
- 一个actor路径包含一个标识该actor系统的锚点,之后是各路径元素连接起来,从根到指定的actor;路径元素是路径经过的actor的名字,以"/"分隔。
- 每一条actor路径都有一个地址组件,描述如何访问到actor的协议和位置,之后是从根到actor所经过的树节点上actor的名字。例如:
"akka://my-sys/user/service-a/worker1" // purely local "akka.tcp://[email protected]:5678/user/service-b" // remote
- 逻辑Actor路径:顺着actor的父监管链一直到根的唯一路径被称为逻辑actor路径。这个路径与actor的创建祖先完全吻合,所以当actor系统的远程调用配置(和配置中路径的地址部分)设置好后它就是完全确定的了。
- 物理路径:逻辑Actor路径描述一个actor系统内部的功能位置,而基于配置的远程部署意味着一个actor可能在另外一台网络主机上被创建,i.e.在另一个actor系统中。在这种情况下,从根穿过actor路径肯定要访问网络,这是一个昂贵的操作。因此,每一个actor同时还有一条物理路径,从实际的actor对象所在的actor系统的根开始的。跟其它actor通信时使用物理路径作为发送方引用能够让接收方直接回复到这个actor上,将路由延迟降到最小。
- *一个重要的方面是物理路径决不会跨多个actor系统或跨虚拟机。这意味着一个actor的逻辑路径(监管树)和物理路径(actor部署)可能会分叉,如果它的祖先被远程监管了
"/user" 是所有由用户创建的顶级actor的监管者,用 ActorSystem.actorOf 创建的actor在其下一个层次 "/system" 是所有由系统创建的顶级actor(如日志监听器或由配置指定在actor系统启动时自动部署的actor)的监管者 "/deadLetters" 是死信actor,所有发往已经终止或不存在的actor的消息会被送到这里 "/temp" 是所有系统创建的短时actor(i.e.那些用在ActorRef.ask的实现中的actor)的监管者. "/remote" 是一个人造的路径,用来存放所有其监管者是远程actor引用的actor
来个图看看
从上图 能得出几个结论
- 应用级别的根路径 是 “/user” 。所有创建的actor 都是在他之下
- 一个 actor 有父actor,有子actor。 路径上就能看出他在这个树形结构中的位置。
- 找到一个actor, 可以直接从路径中找,也可以通过 actorref 的父子级去寻找。。
上面最后一点用代码更能体现出来。
context.actorSelection("../broder") ! msg //通过相对路径查找 context.actorSelection("/user/serviceA") ! msg //通过绝对路径 context.actorSelection("../*") ! msg //支持unix shel 路径 通配符