AKKA 实现并发、分布式和容错

来源:互联网 发布:s3世界总决赛faker数据 编辑:程序博客网 时间:2024/06/04 19:13


  Actor系统 

  把一个复杂的问题不断分解成更小规模的子问题通常是一种可靠的解决问题的技术。这个方法对于计算机科学特别有效(和 单一职责原则一致),因为这样容易产生整洁的、模块化的代码,产生的冗余很少甚至没有,而且维护起来相对容易。 

  在基于Actor的设计里,使用这种技术有助于把Actor的逻辑组织变成一个层级结构,也就是所谓的 Actor系统。Actor系统提供了一个基础框架,通过这个系统Actor之间可以进行交互。 

  

 

  在Akka里面,和Actor通信的唯一方式就是通过ActorRef。ActorRef代表Actor的一个引用,可以阻止其他对象直接访问或操作这个Actor的内部信息和状态。消息可以通过一个ActorRef以下面的语法协议中的一种发送到一个Actor: 

  -!(“告知”) —— 发送消息并立即返回 

  -?(“请求”) —— 发送消息并返回一个Future对象,代表一个可能的应答 

  每个Actor都有一个收件箱,用来接收发送过来的消息。收件箱有多种实现方式可以选择,缺省的实现是先进先出(FIFO)队列。 

  在处理多条消息时,一个Actor包含多个实例变量来保持状态。Akka确保Actor的每个实例都运行在自己的轻量级线程里,并保证每次只处理一条消息。这样一来,开发者不必担心同步或竞态条件,而每个Actor的状态都可以被可靠地保持。 

  Akka的Actor API中提供了每个Actor执行任务所需要的有用信息: 

  sender:当前处理消息的发送者的一个ActorRef引用 

  context:Actor运行上下文相关的信息和方法(例如,包括实例化一个新Actor的方法ActorOf) 

  supervisionStrategy:定义用来从错误中恢复的策略 

  self:Actor本身的ActorRef引用 

  Akka确保Actor的每个实例都运行在自己的轻量级线程里,并保证每次只处理一条消息。这样一来,开发者不必担心同步或竞态条件,而每个Actor的状态都可以被可靠地保持。 

  为了把这些教程组织起来,让我们来考虑一个简单的例子:统计一个文本文件中单词的数量。 

  为了达到演示Akka示例的目的,我们把这个问题分解为两个子任务;即(1)统计每行单词数量的“孩子”任务和(2)汇总这些单行单词数量、得到文件里单词总数的“父亲”任务。 

  父Actor会从文件中装载每一行,然后委托一个子Actor来计算某一行的单词数量。当子Actor完成之后,它会把结果用消息发回给父Actor。父Actor会收到(每一行的)单词数量的消息并维持一个整个文件单词总数的计数器,这个计数器会在完成后返回给调用者。 

  (注意以下提供的Akka教程的例子只是为了教学目的,所以没有顾及所有的边界条件、性能优化等。同时,完整可编译版本的代码示例可以在这个GIST中找到) 

  让我们首先看一个子类StringCounterActor的示例实现:

 

    这个Actor有一个非常简单的任务:接收ProcessStringMsg消息(包含一行文本),计算这行文本中单词的数量,并把结果通过一个StringProcessedMsg消息返回给发送者。请注意我们已经实现了我们的类,使用!(“告知”)方法发出StringProcessedMsg消息(发出消息并立即返回)。 

  好了,现在我们来关注父WordCounterActor类:

 

 

  这里面有很多细节,我们来逐一考察(注意讨论中所引用的行号基于以上代码示例)。 

  首先,请注意要处理的文件名被传给了WordCounterActor的构造方法(第3行)。这意味着这个Actor只会用来处理一个单独的文件。这样通过避免重置状态变量(running,totalLines,linesProcessed和result)也简化了开发者的编码工作,因为这个实例只使用一次(也就是说处理一个单独的文件),然后就丢弃了。 

  接下来,我们看到WordCounterActor处理了两种类型的消息: 

  StartProcessFileMsg(第12行) 

  从最初启动WordCounterActor的外部Actor接收到的消息 

  收到这个消息之后,WordCounterActor首先检查它收到的是不是一个重复的请求 

  如果这个请求是重复的,那么WordCounterActor生成一个警告,然后就不做别的事了(第16行) 

  如果这不是一个重复的请求: 

  WordCounterActor在FileSender实例变量(注意这是一个Option[ActorRef]而不是一个Option[Actor])中保存发送者的一个引用。当处理最终的StringProcessedMsg(从一个StringCounterActor子类中接收,如下文所述)时,为了以后的访问和响应,这个ActorRef是必需的。 

  然后WordCounterActor读取文件,当文件中每行都装载之后,就会创建一个StringCounterActor,需要处理的包含行文本的消息就会传递给它(第21-24行)。 

  StringProcessedMsg(第27行) 

  当处理完成分配给它的行之后,从StringCounterActor处接收到的消息 

  收到此消息之后,WordCounterActor会把文件的行计数器增加,如果所有的行都处理完毕(也就是说,当totalLines和linesProcessed相等),它会把最终结果发给原来的FileSender(第28-31行)。 

  再次需要注意的是,在Akka里,Actor之间通信的唯一机制就是消息传递。消息是Actor之间唯一共享的东西,而且因为多个Actor可能会并发访问同样的消息,所以为了避免竞态条件和不可预期的行为,消息的不可变性非常重要。 

  因为Case Class默认是不可变的并且可以和模式匹配无缝集成,所以用Case Class的形式来传递消息是很常见的。(Scala中的Case Class就是正常的类,唯一不同的是通过模式匹配提供了可以递归分解的机制)。 

  让我们通过运行整个应用的示例代码来结束这个例子。

 

 

  请注意这里的?方法是怎样发送一条消息的。用这种方法,调用者可以使用返回的 Future对象,当完成之后可以打印出最后结果并最终通过停掉Actor系统退出程序。 

  Akka的容错和监管者策略 

  在Actor系统里,每个Actor都是其子孙的监管者。如果Actor处理消息时失败,它就会暂停自己及其子孙并发送一个消息给它的监管者,通常是以异常的形式。 

  在Akka里面,监管者策略是定义你的系统容错行为的主要并且直接的机制。 

  在Akka里面,一个监管者对于从子孙传递上来的异常的响应和处理方式称作监管者策略。 监管者策略是定义你的系统容错行为的主要并且直接的机制。 

  当一条消息指示有一个错误到达了一个监管者,它会采取如下行动之一: 

  恢复孩子(及其子孙),保持内部状态。 当孩子的状态没有被错误破坏,还可以继续正常工作的时候,可以使用这种策略。 

  重启孩子(及其子孙),清除内部状态。 这种策略应用的场景和第一种正好相反。如果孩子的状态已经被错误破坏,在它可以被用到Future之前有必须要重置其内部状态。 

  永久地停掉孩子(及其子孙)。 这种策略可以用在下面的场景中:错误条件不能被修正,但是并不影响后面执行的操作,这些操作可以在失败的孩子不存在的情况下完成。 

  停掉自己并向上传播错误。 适用场景:当监管者不知道如何处理错误,就把错误传递给自己的监管者。 

  而且,一个Actor可以决定是否把行动应用在失败的子孙上抑或是应用到它的兄弟上。有两种预定义的策略: 

  OneForOneStrategy:只把指定行动应用到失败的孩子上 

  AllForOneStrategy:把指定行动应用到所有子孙上 

  下面是一个使用OneForOneStrategy的简单例子:

 

 

  如果没有指定策略,那么就使用如下默认的策略: 

  如果在初始化Actor时出错,或者Actor被结束(Killed),那么Actor就会停止(Stopped) 

  如果有任何类型的异常出现,Actor就会重启 

  Akka提供的默认策略的实现如下:

 

 

  Akka也考虑到对 定制化监管者策略的实现,但正如Akka文档也提出了警告,这么做要小心,因为错误的实现会产生诸如Actor系统被阻塞的问题(也就是说,其中的多个Actor被永久挂起了)。 

  本地透明性 

  Akka架构支持 本地透明性,使得Actor完全不知道他们接受的消息是从哪里发出来的。消息的发送者可能驻留在同一个JVM,也有可能是存在于其他的JVM(或者运行在同一个节点,或者运行在不同的节点)。Akka处理这些情况对于Actor(也即对于开发者)来说是完全透明的。唯一需要说明的是跨越节点的消息必须要被序列化。 

  Akka架构支持本地透明性,使得Actor完全不知道他们接受的消息是从哪里发出来的。 

  Actor系统设计的初衷,就是不需要任何专门的代码就可以运行在分布式环境中。Akka只需要一个配置文件(Application.Conf),用以说明发送消息到哪些节点。下面是配置文件的一个例子:

 

 

  最后的一些提示 

  我们已经了解了Akka框架帮助完成并发和高性能的方法。然而,正如这篇教程指出的,为了充分发挥Akka的能力,在设计和实现系统时,有些要点值得考虑: 

  我们应尽最大可能为每个Actor都分配最小的任务(如上面讨论的,遵守单一职责原则) 

  Actor应该异步处理事件(也就是处理消息),不应该阻塞,否则就会发生上下文切换,影响性能。具体来说,最好是在一个Future对象里执行阻塞操作(例如IO),这样就不会阻塞Actor,如:

 

 
  ·要确认你的消息都是不可变的,因为互相传递消息的Actor都在它们自己的线程里并发运行。可变的消息很有可能导致不可预期的行为。 

  ·由于在节点之间发送的消息必须是可序列化的,所以必须要记住消息体越大,序列化、发送和反序列化所花费的时间就越多,这也会降低性能。 

  结论 

  Akka用Scala语言写成,简化并为开发高并发、分布式和容错式应用提供了便利,对开发者隐藏了很大程度的复杂性。把Akka用好肯定需要了解比这个教程更多的内容,但是希望这里的介绍和示例能够引起你的注意并继续了解Akka。 

0 0
原创粉丝点击