akka actor模型_使用Akka的Actor模型和领域驱动设计构建React系统

akka actor模型

重要要点

  • 面向角色的编程是面向对象编程的替代方法
  • 使用参与者可以轻松开发高度并发的系统
  • Actor系统不限于单个节点上的单个进程,而是作为分布式集群运行
  • 演员和演员模型就是“React”的全部内容
  • 演员和领域驱动设计是完美的选择

随着移动和数据驱动应用程序的爆炸性增长,用户要求实时访问任何地方的所有内容。 系统弹性和响应能力不再是“拥有”。 它们是必不可少的业务要求。 企业越来越需要从静态,易碎的体系结构中换取支持灵活,弹性的系统。

因此,响应式开发正在Swift普及。 为了支持响应式开发, 参与者模型以及领域驱动的设计可以满足现代的弹性要求。

演员模型,一些历史

演员模型是在Smalltalk出现后于70年代初构思的,不久后面向对象的编程(OOP)本身就出现了。

2003年前后,随着处理器速度开始达到顶峰,计算的本质发生了根本性的变化。 在接下来的十五年中,时钟速度的提高将是增量的,而不是过去的指数级。

但是用户需求持续增长,计算世界不得不找到某种方式做出React,孵化出多核处理器。 处理收益成为“团队努力”,它通过多个内核之间的通信而不是通过传统的时钟速度加速来提高效率。

这是线程进入的地方,这个概念比看起来要复杂得多。 考虑一个简单计数器的示例,该计数器代表一种稀缺资源,例如库存中的项目数或可用于事件销售的票证。 在此示例中,可能有许多同时请求以获得一件或多件物品或票证。

考虑这种常用的实现,其中一个线程处理每个购买请求。 使用这种方法,有可能许多同时运行的线程将各自尝试调整计数器。 为了满足语义要求,我们的模型必须确保一次仅一个线程递减计数器。 这样做的原因是减量操作涉及两个步骤:

  1. 检查计数器当前是否大于或等于所需的计数器减量
  2. 减少计数器

这是为什么两个步骤必须作为单个操作完成的示例。 每个请求代表购买一个或多个要出售的物品或请求购买一张或多张门票。 假设有两个线程正在同时尝试调整当前值为5的计数器。线程一想要将计数器减4。线程两个想要将计数器减3。它们都检查计数器的当前值并验证它大于减量。 然后他们都继续前进并递减计数器。 结果是5-4-3 = -2。 结果是项目被过度分配,在这种情况下,这违反了指定的业务规则。

防止这种类型的过度分配的简单方法是在单个原子操作中执行检查和递减步骤。 将两个步骤锁定到一个操作中,就消除了在售罄时购买商品的可能性,例如,两个线程试图同时购买最后一件商品。 如果没有锁定,则有可能多个线程同时首先检查计数器是否大于或等于所需的购买量,然后它们都错误地减少了计数,从而导致负值。

这种一次使用一个线程的方法的潜在问题是,在竞争激烈的时期,可能会出现较长的线程队列,每个线程都在等待轮到自己递减计数器。 一个现实的例子就是一群等待购买事件门票的人。

这种方法的一大缺点是可能存在许多被阻塞的线程,每个线程都在单个文件中等待轮流执行序列化操作。

如果应用程序设计人员不谨慎,则固有的复杂性将带来将多核处理器,多线程应用程序转变为本质上是单线程应用程序或至少一个在工作线程之间具有较高竞争水平的真正风险。

针对多线程环境的更好解决方案

actor模型很好地解决了这一难题,为真正的多线程应用程序奠定了基础。 actor模型被设计为消息驱动和非阻塞的,吞吐量是自然方程式的一部分。 它为开发人员提供了一种针对多个内核进行编程的简便方法,而没有并发中常见的认知负担。 让我们看看它是如何工作的。

演员由发送者和接收者组成; 设计用于异步的简单消息驱动对象。

让我们修改上述的票务柜台方案,用参与者替换基于线程的实现。 演员当然必须在线程上运行。 但是,参与者仅在有事情要做时才使用线程。 在我们的反向方案中,请求者被表示为客户参与者。 现在,票数由演员来维护,并保持柜台的当前状态。 客户和票证参与者在空闲或无事可做时都没有线程,也就是说,没有消息要处理。

为了启动购买操作,客户参与者将购买消息发送给单个票证参与者。 此类购买消息包含要购买的数量。 当票务演员收到购买消息时,它会验证购买金额不超过当前剩余数量。 如果购买请求有效,则计数递减,并且票证执行者向客户执行者发送一条消息,指示已接受订单。 如果购买金额超出了该数量,则反操作者向客户操作者发送一条消息,指示已拒绝该订单。 角色模型本身可确保处理被同步处理。

在下图中,我们显示了几个客户参与者,每个客户参与者向票证参与者发送购买消息。 这些购买消息最初在票证演员的邮箱中排队。

图3-客户参与者发送购买消息

票务人员处理每个消息。 这里的第一条消息是要求购买五张门票。

图4-Tickets Actor处理消息

票务人员检查购买金额是否不超过剩余票数。 在这种情况下,票数当前为15,因此购买请求得到批准,计数减少,并且从票证参与者向请求客户的参与者发送一条消息,指示票证已被购买。

图5-Ticket Actor处理消息队列

票证参与者处理其邮箱中的每个消息。 注意,这里不需要复杂的线程或锁定。 这是一个多线程过程,但是参与者系统管理线程的使用和分配。

在下图中,我们看到票证参与者如何处理超出剩余票证数量的请求。 这里显示的是在只有一张票的情况下购买两张票的请求。 票务演员拒绝此购买请求,并向请求的客户演员发送售罄的消息。

图6-票务演员拒绝购买请求

当然,经验丰富的线程级开发人员知道检查的两步操作,并且将票证计数器递减很容易实现为同步的操作序列。 例如在Java中,使用同步方法或同步语句。 但是,基于参与者的实现不仅提供了每个参与者内操作的自然同步,而且还消除了在同步部分等待线程轮换的潜在大量积压。 在票证示例中,每个客户参与者都在等待响应而没有线程。 结果是基于参与者的解决方案更易于实现,并且可能导致系统资源利用率的显着降低。

演员:客体王位的合法继承人

演员是对象的自然继承者的想法并不是什么新鲜事物,实际上并不是所有的革命。 Smalltalk的发明者Alan Kay定义了许多仍在使用的对象范例。 他强调了消息传递的重要性,甚至说对象的内部实现是次要的。

尽管Smalltalk最初并不是异步的,但它仍然基于消息,一个对象本质上将向另一个对象发送消息以完成任何事情。 因此,现代演员模型继承了艾伦·凯(Alan Kay)的面向对象设计的最早思想。

以下是Akka actor系统中actor的Java示例实现(我们将看到,我们为每个actor分配了一个唯一的“魔术”序列号,以演示状态)。

public class DemoActor extends AbstractActor {

  private final int magicNumber;

  public DemoActor(int magicNumber) {
    this.magicNumber = magicNumber;
  }

  @Override
  public Receive createReceive() {
    return receiveBuilder()
      .match(Integer.class, i -> {
        getSender().tell(i + magicNumber, getSelf());
      })
      .build();
  }

  public static Props props(int magicNumber) {

    // Akka Props is used for creating Actor instances
    return Props.create(DemoActor.class, () -> new DemoActor(magicNumber));
  }
}

请注意,参与者是作为扩展Akka抽象基类的类实现的。 actor的实现必须重写一个方法createReceive方法,该方法负责创建消息接收构建器,定义如何处理发送到此actor实现的传入消息对象。

还请注意,此参与者是有状态的。 演员状态可以很简单,如本例中的魔术数字,也可以更复杂。

要创建角色的实例,我们需要一个ActorSystem。 ActorSystem启动后,创建actor通常只需要一行代码。

ActorSystem system = ActorSystem.create("DemoSystem");
ActorRef demo = system.actorOf(DemoActor.props(42), "demo");

参与者创建操作的返回值是参与者参考。 该参与者参考用于将消息发送给参与者。

demo.tell(123, ActorRef.noSender());

上面显示了一些定义,创建正在运行的实例以及向参与者发送消息的基本步骤。 当然,除了这个简单的示例以外,还有很多其他功能,但是在大多数情况下,使用参与者的开发系统需要学习如何实现设计为应用程序和服务的系统,这些应用程序和服务被设计为参与者通过交互异步消息进行交互的系统。

更好的应用程序带来更好的网络

除了内核和线程之外,当今的环境还使开发人员可以利用超高速存储设备,大量内存以及大量高度可扩展的,广泛连接的设备。 这项技术都通过相当实惠的云托管解决方案和快速网络进行通信。

但是随着系统变得更加分散,必然会增加延迟。 分布式系统可能会由于停机或网络分区而中断,这可能是由于一台或多台服务器无法使用,产生延迟而引起的。 对象模型不适合处理此问题。 因为每个请求和每个响应都是异步的,所以参与者模型可以帮助开发人员解决此问题。

使用actor模型,减少延迟是免费的。 因此,不能期望立即得到结果,并且系统仅在发送或接收消息时对消息做出React。 当检测到延迟降低时,系统会自动做出React并进行调整,而不是关闭。

节点的分布式集群,每个节点都运行参与者的子集,是参与者通过异步消息相互交互的自然环境。 参与者的基本功能增加了一个事实,即消息发送者和接收者不受限于单个JVM进程边界。 Akka的最佳功能之一是您可以构建可以在集群中运行的系统。 Akka群集是在独立JVM中运行的一组节点。 通过编程,向本地JVM中的actor发送消息与向另一个JVM中运行的actor发送消息一样容易。 如下图所示,分布在多个群集节点上的参与者可以将消息发送到其他群集节点上的其他参与者。

在集群环境中运行会为actor系统的体系结构带来全新的动态。 在单个服务器上,单个进程中以及单个JVM中运行是一回事。 运行跨整个网络分布的JVM集群的系统完全是另一回事。

在单个actor中,actor运行在actor系统中的JVM中,JVM正在运行或未运行。 另一方面,在集群中运行时,集群的拓扑在任何时间点都可能发生变化。 群集节点JVM可能会立即出现或消失。

只要至少一个节点处于运行状态,集群本身就处于运行状态。 一个节点上的参与者可能正在与其他节点上的参与者愉快地交换消息,然后,在没有警告的情况下,一个节点离开了该节点上驻留的参与者。 其余的参与者应该如何应对这些变化?

群集节点的丢失会影响到消息发送方和消息接收方的消息交换。

对于消息接收者,始终有可能永远不会收到预期的消息。 接收方需要考虑到这一点。 必须有一个计划B。可能不会收到预期的消息是异步消息传递的事实。 的确,在大多数情况下,处理丢失的传入消息不需要群集感知。

另一方面,消息发件人通常必须具有一定程度的集群意识。 路由器参与者可以处理将消息发送到可能分布在整个集群中的其他参与者的后勤工作。 路由器参与者会收到消息,但不会自行处理消息。 它将消息转发给工人演员。 这些工人演员通常被称为路线。 路由器参与者负责基于路由算法将消息路由到其他路由参与者。 实际使用的路由算法会根据每个路由器的特定要求而有所不同。 路由算法的示例包括轮询,随机,最小邮箱等。

考虑下图所示的示例场景(回想一下,向客户端发送消息的客户端不知道该客户端将如何处理消息。)从客户端的角度来看,接收方是一个黑匣子。 接收者演员可以将要完成的工作委托给其他工人演员。 在这种情况下,接收方参与者可以是路由器。 它将传入的消息路由到负责工作的代理路由参与者。

在此示例场景中,路由器参与者可能是群集感知的,并且可能是将消息路由到分布在群集中各个节点之间的参与者。 那么了解集群意味着什么?

群集感知参与者使用有关当前群集状态组成的信息来决定如何将传入消息路由到跨群集分布的其他参与者。 群集感知参与者最常见的用例之一是路由器。 群集感知路由器根据群集当前状态决定如何将消息路由到路由参与者。 例如,一个知道在整个群集中分布的路由参与者的位置的路由器会将消息路由到基于分布式工作算法的路由。

角色模型如何支持React系统

React式宣言中所定义,“React式是响应式的,React式是弹性的,React式是弹性的,React式是消息驱动的。” 消息驱动的组件本质上是允许支持Reactive的其他三个特征的组件。

增强系统响应能力

响应式响应能力强,因为系统可以动态适应不断变化的用户需求。 React式系统能够以请求/响应方式响应用户需求的情况并不少见。 通过参与者模型支持React式,开发人员可以实现很高的吞吐量。

增强系统弹性

消息传递架构还通过消息传递和其他功能来支持弹性。 当客户端参与者发送消息到服务器参与者接收者时,客户端不必处理可能在该服务器对象或参与者内引起的异常处理。

考虑一个典型的面向对象的体系结构,在该体系结构中,客户端发送消息或在接收方上调用方法,从而迫使客户端处理任何类型的崩溃或引发的异常。 作为响应,客户通常会将异常抛出或扔给更高级别的组件,并希望有人对此进行处理。 但是客户端不适合修复服务器崩溃。

在参与者模型中,尤其是在Akka中,建立了用于监督的层次结构。 当服务器崩溃或在传入消息上引发异常时,不是由客户端来处理崩溃,而是由服务器角色或对象的父级来处理崩溃。

父母可以更好地了解其子actor发生崩溃的可能性,因此可以对此做出React并重新启动该actor。 因此,客户端仅需处理以下信息:客户端已收到对其请求的响应,或者尚未收到响应。 并且基于计时器或计划的事件,如果在可接受的时间段内未收到响应,它可以要求同一行为者再次处理同一请求。 因此,actor系统(正确构建时!)非常有弹性。

这是一个展示演员监督行动的例子。 在图7中,演员R是创建了四个工作人员演员的主管。 演员A和B是演员,向演员R发送消息,要求演员R执行某些操作。 演员R将工作委派给其可用的工人演员之一。

图7-Actor由R委派给Worker Actor的消息

在此示例中,工作人员参与者遇到问题,如图8所示,并引发异常。 异常由主管处理; 参与者R.监督者参与者遵循明确的监督策略来处理工人错误。 监督者可以选择在遇到简单问题的情况下简单地恢复演员,或者可以根据严重性和恢复策略来重新启动或停止工作人员。

图8-工人演员抛出异常

当异常由主管处理时,参与者A期望收到响应消息。 请注意,参与者A仅在等待一条消息,而不在等待一条消息。

异步消息的这种交换引入了一些有趣的动态。 演员A希望演员R对它的消息做出预期的React。 但是,不能保证将处理参与者A的消息或返回响应消息。 可能发生任何数量的问题,这些问题都会破坏此异步请求和响应周期。 例如,考虑参与者A和参与者R在不同节点上运行,并且参与者A和参与者R之间的消息通过网络连接发送的情况。 网络可能已关闭,或者参与者R正在运行的节点可能发生故障。 或者,例如,如果由于网络故障或服务器关闭而导致数据库操作失败,则要执行的任务可能会失败。

鉴于没有任何保证,处理这一问题的通用方法是实施参与者A,以期获得两种可能的结果。 一个结果是,当它向参与者R发送消息时,它最终会收到响应消息。 另一个可能的结果是,演员A也可能期望收到一条备用消息,该消息指示尚未收到预期的响应。 这种方法涉及演员A发送两个消息:一个是给演员R的消息,另一个是将来某个特定时间要发送给自己的消息。

图9-Actor A收到超时消息

这里使用的基本策略是有一个计划A和一个计划B。计划A是一切都按预期进行。 参与者A向参与者R发送消息,执行了预期的任务,并将响应消息返回给参与者A。计划B处理了参与者R无法处理请求消息的情况。

扩大系统弹性

React系统也是有弹性的。 它们可以根据当前需求增长和收缩。 真正的React式设计没有争用点或中央瓶颈,因此您可以分片或复制组件并在其中分配输入。 它们通过提供相关的实时性能指标来启用预测性和React性缩放算法。

Actor模型通过动态响应用户活动的高峰和低谷,在需要时智能地提高性能并在低使用率期间保留功率来支持此功能。 它们的消息驱动性质固有地导致更大程度的弹性。

弹性需要两个关键要素。 一种机制是随着负载的增加和减少而扩大和缩小系统的处理能力的机制。 第二种机制允许系统随着系统容量的变化做出适当的React。

有许多方法可以处理扩展和收缩处理能力。 通常,容量更改是手动或自动处理的。 手动流程的一个示例是增加处理能力,以准备客户流量的季节性高峰。 经典示例是黑色星期五和网络星期一或光棍节。 自动缩放是许多云提供商(例如Amazon AWS)提供的有用功能。

actor模型和actor模型实现Akka不提供任何机制来触发处理能力调整,但是它是构建系统的理想平台,当集群拓扑发生变化时,这些系统可以做出适当的React。 如前所述,在参与者级别,可以实现专门为在节点离开或加入集群时做出React而设计的集群感知参与者。 在很多情况下,当您设计并实现角色系统以使其具有更大的弹性时,这是一个令人高兴的情况,同时也为弹性奠定了基础。 当您的系统可以处理由于故障而离开群集的分布式节点,并且新节点加入群集以替换故障节点时,由于故障或由于对可用处理能力的调整(例如,对活动水平变化的React。

讯息很重要

正如我们所说,参与者模型专注于直接异步消息传递。 为此,发送者必须知道接收者的地址,以便向接收者发送消息。

演员是无锁的,他们没有任何共享。 如果三个发送者要同时向接收者发送一条消息,则接收者会将这些消息放入其邮箱中,并一次处理一个。 因此,接收器无需内部锁定即可保护其状态,防止其同时运行多个线程。 并且接收者不会与任何其他参与者共享其内部状态。

演员模型也为准备接收演员准备处理他们的下一条消息提供了机会。 例如,假设有两个程序流程序列。 当第一个序列中的发送者向接收者发送消息时,该接收者对该消息做出React,然后转换为另一种消息侦听器。 现在,当在所述第二序列中的消息被发送到相同的演员,它响应使用一组不同的逻辑在它的接收块(演员互换消息接收时,它决定改变状态的逻辑。有一个的这个例子中在Akka文档)。

事半功倍

参与者模型帮助解决的另一个重要问题是事半功倍。 各种规模的系统都可以从Amazon和Netflix使用的大型网络(甚至更小的体系结构)中受益。 使用actor模型,开发人员可以充分利用每台服务器中的大部分资源,从而具有缩减集群的巨大潜力。

基于角色的服务可以有几个角色? 五十? 一百? 也许! 基于Actor的系统是如此灵活和有弹性,它们可以潜在地支持数以百万计的众多Actor。

在典型的N层体系结构或可能称为“ 端口和适配器 ”或六边形体系结构中,存在许多不必要的复杂性,甚至可能是偶然的复杂性。 actor模型的最大优点之一是,它可以消除很多这种复杂性,并说出边界上的一组“控制器”适配器。 控制器可以通过将消息发送到域模型来进行委派,并且该域模型会发出事件。 这样,参与者模型极大地降低了网络复杂性,使设计人员可以在有限的资源和预算下实现更多目标。

通过域驱动设计加速业务

域驱动设计(DDD)的实质是在有限的上下文中对无处不在的 语言进行建模。 让我解释一下:考虑将某种服务建模为有限的上下文。 边界上下文是一个语义边界,其中的所有内容(包括域模型)都有特定的定义,包括队友使用的一种语言,以帮助开发人员理解边界上下文中每个概念的含义。

这就是上下文映射起作用的地方。 上下文映射可以模拟每个有界上下文如何相互对应,团队关系是什么以及模型如何交互和集成。 上下文映射中的杠杆作用也非常重要,因为有界上下文往往比整体上下文中的许多习惯要小得多。

对于任何企业及其开发团队而言,响应Swift的新业务方向都是一个巨大的挑战。 DDD可以在处理这些不断发展的业务方向时进行必要的知识处理。 参与者和消息可能会帮助开发人员响应这些需求而快速实现域模型,并具有对域模型的清晰了解。

演员与DDD:完美匹配

用艾伦·凯(Alan Kay)的话说:“演员模型保留了更多我认为是对象概念的良好特征的东西。” 同样来自Kay:“最大的主意是消息传递。” 在创建一种普遍存在的语言时,开发人员可以将角色集中在作为对象或组件,域模型的元素以及它们之间发送的消息的角色上。

一种React式服务是无服务; React性服务进入系统。 因此,开发人员最终想要实现的目标是构建完整的系统,而不仅仅是单个服务。 通过将域事件发送到其他有界上下文或微服务,开发人员可以更轻松地完成此任务。

为什么演员更适合业务

演员是DDD的理想选择,因为他们会说出核心业务领域的普遍语言。 它们旨在优雅地处理业务失败,无论网络上发生什么情况,都可以保持系统的弹性和响应能力。 它们可以帮助开发人员以响应方式扩展系统,以满足并发需求,可以灵活地增长和扩展以处理峰值负载,并在流量减少时缩小负载,从而最大程度地减少了基础架构占用的空间和硬件需求。 此模型更适合于当今高度分散的多线程环境,并且可以产生远远超出服务器机房的业务收益。

翻译自: https://www.infoq.com/articles/Reactive-Systems-Akka-Actors-DomainDrivenDesign/?topicPageSponsorship=c1246725-b0a7-43a6-9ef9-68102c8d48e1

akka actor模型

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值