响应式云 Actor 实现:一种可进化的事件网络

阅读数:2754 2015 年 5 月 26 日

话题:语言 & 开发架构

Carl Hewitt 在 1973 年对Actor 模型进行了如下定义:

Actor 模式是一个把“Actor”作为“并发数字计算的通用原语”的数学理论。

Actor 已经成为分布式计算近来发展趋势中的主要关注点。最近,围绕着 Actor 在基于云计算的并发处理以及大数据世界中的地位出现了许多争论。虽然 Actor 模型以往的作用主要是对单一机器上的单一软件进程中的并行计算进行建模与实现,但如今它也能够很好地对云环境中繁重的并行处理进行建模。

在本文中,我们会首先定义三种类型的 Actor,并探索它们各自的角色与职责。接下来,我们将考察一种范式,它将业务流程分解为一种“可进化的事件网络”,在这一网络中的每个“重要的”业务步骤将生成一个持久性的事件,这些事件可由一个或多个 Actor 进行进一步独立处理。我们还将参考一些真实世界中的 Actor 模型的示例。在文章的最后,我们将一起思考这使用这一模式的含义,并考察一个使用BeeHive库按照这种方式实现的示例。

Actor 模型

1973 年,Carl Hewitt 与 Peter Bishop 和 Richard Steiger 共同发布了一篇文章,其中提出了一种形式,将一种单一类型的对象,即 Actor 作为用于实现人工智能算法的系统构建块。

在神经网络中也存在着一种类似的模式,这个模式中的神经元作为神经网络中的构建块,每个神经元具有一个偏置(bias),一个或多个输入(以及对应的权重)和一个或多个输出(以及对应的权重)。

不同之处在于,Actor 与神经元节点不同,它不仅仅是一种计算单元。Actor 本质上是存在于一个大型网络中的自治单元,通过接收消息的形式触发它们。

Actor

按照Hewitt所说,当某个 Actor 对消息进行响应时,它能够:

  1. 将有限数量的消息发送给其它 Actor
  2. 创建有限数量的其它 Actor
  3. 决定接收下一条消息时的行为

以上这些行为的各种组合都可以在对以各种顺序传入的消息进行响应时并发进行,因此,它对于消息的顺序没有限制,Actor 的实现必须能够处理带外(out of band)传入的消息。

Processor Actor

随后出现了一种对 Actor 模型的描述,它将第一个约束条件重新定义为“将有限数量的消息发送给其它 Actor 的地址”。寻址能力是这个模型中必不可少的一部分,它对 Actor 进行了解耦,将各个 Actor 相互之间的全部认知限制为一个令牌(即地址)。类似的寻址实现包括 Web Service 的终结点、发布 / 订阅队列的终结点以及 email 地址。使用第一种约束对消息进行响应的 Actor 可以被统一称为Process Actor

Factory Actor

第二条约束让 Actor 能够创建其它 Actor,出于方便,我们将其称之为Factory Actor。Factory Actor 是消息驱动系统中的重要元素,在这种系统中将通过消息队列获取 Actor,并根据消息的类型创建消息处理器。Factory Actor 控制着它们所创建的 Actor 的生命周期,并且了解它们所创建的 Actor 的方方面面,与 Process Actor 只了解地址的方式形成鲜明对比。对 Factory Actor 与用于处理消息的 Actor 进行分离是一种很实用的做法,它让我们能够贯彻单一职责原则。

Stateful Actor

第三条约束中的 Actor 称为Stateful Actor(有状态的 Actor)。符合第三条约束的 Actor 能够保留记忆,因此对于后续的消息能够进行不同的响应。这种 Actor 可能会产生各种各样的副作用。首先,一旦我们说起“后续的消息”,我们就对消息的顺序产生了一种假设,而我们之前说过,对于消息的顺序是没有约束的:因此带外消息的接收可能会导致并发问题。其次,CAP 原理的所有方面都适应于这种记忆性,因此无法同时具备一致性、高可用性和分区容忍性。简而言之,最好能够避免使用Stateful Actor

与最初的提议一样,某个 Actor 能够基于这三种约束的任何组合进行活动,但我们现在为 Actor 的创建施加了一种单一职责性,因此能够得到一种更好的、更为解耦的设计。在本文中,我们将高度依赖这种分离性,并且在提出的解决方案中会利用到它所带来的益处,随后展示一个按照这种方式实现的示例。

Actor 依赖

通过限制 Actor 之间的相互作用,Actor 模型降低了每个Actor对其周边环境信息的认知。一个Process Actor只需要知道整个链中之后的 Actor 的地址,而我们将进一步消除这种认知,让 Actor 生成事件,以描述(通过事件名称)并记录(事件状态)所发生的事,Actor 所形成的网络能够根据事件的类型进行选择。

Factory Actor只对它们所创建的、并将获得的消息进行传递的 Actor 有所认识。它将负责控制所创建的 Actor 的生命周期,但在本质上,它对负责 Processor Actor 的处理细节并不知情。

通过消除系统中元素的依赖与耦合,实现系统的可进化能力也变得可能了。可以对每个 Actor 的行为进行独立修改,事实上,每个 Actor 可以在独立于其它 Actor 的条件下进行部署。依赖的减少正是基于Actor的系统的最大优势之一。它会带来设计的简化,减少了依赖与耦合的元素将构成整个应用程序。

除了以上所说的内容之外,所有的 Actor 都依赖于一个只读的配置参数集,它提供了这些 Actor 运行环境的相关知识。这些配置参数可以包括各种环境(例如终结点的地址)与行为的默认值(例如重试次数、缓冲区大小或追踪信息的覆盖程度)。

响应式 Actor

Actor 的实现可以是命令式或者是响应式的。在命令式Actor 中,Actor 所发送的消息是一种 RPC 消息,它特定于某种 Actor 类型。这种 Actor 类型的接口、类型和终结点对发送者来说都是已知的。消息的发送通常是使用持久性的消息总线。这种方式与企业服务总线的实现可能是非常相近的,它们都将可部署的东西简化到了最简单的形式。

(单击图片以放大)

而在响应式Actor 的情形中,发送者只是简单地发布了一个消息,发出某个业务流程已完成(OrderAccepted、FraudDetected、ItemOutOfStock 或 ItemBackInStock 等等)的信号,其它 Actor 可以有选择地订阅这些事件,并执行自己的操作。在这种场景中,Actor 能够独立地进化,只需对一个或一定数量的 Actor 进行修改就能够实现业务流程的改动。这种方式能够带来更高程度的解耦,非常适合于在云环境中的水平伸缩能力中开发分析系统与业务系统。

目前已经出现了响应式 Actor 的具体实现,Fred George设计的响应式 MicroServices 就是一个重要的示例,而Amazon Kinesis也可以被视为一种粗粒度的响应式 Actor 框架。

根据 Actor 对系统进行建模

自从上世纪 70 年代起,Actor 模型系统就已经得到了应用。这些系统通过 Actor 实现了高并行能力的系统。Erlang 的语言本身就包含了对 Actor 模型的支持,由 Erlang 开发的应用很早之前就在电信产业中获得了成功。

如今,随着相对廉价而具备高可用性的计算能力与存储能力的出现,只需按下一个按钮,我们就能将系统部署到几百个、乃至几千个节点上。而真正的挑战在于让系统的设计能够充分地利用这些强大的资源。对于云的实施存在着一个常见的错误,就是所谓的“Lift and Shift”方式,即在云端以相同的方式部署来自于本地分层架构的系统,然后设置一个 RDBMS 的后台数据库就行了。这种方式在大多数情况下失去了云实施的意义。要想打造能够水平扩展、线性扩展的系统,需要在数据结构、计算与依赖的建模过程中使用全新的范式。

我们的目标模型是能够水平扩展与线性扩展、健壮并且高可用,并且易于进化的。让我们看看这种系统中的各种方面。

消息处理

基于消息的系统在这个行业中已经存在了很长时间,它也是使用企业服务总线进行 SOA 实施的支柱之一。另一方面,HTTP 定义了通信方式,即对于请求返回一个响应消息。SOAP 服务对于 SOAP 请求会返回一个 SOAP 消息。即使在较低的层面上,在 RPC 范式中也可以将某个方法调用以请求与响应的方式进行建模。那么,消息处理这种方式又有什么不同之处呢?

对于 Processor Actor 来说,它们只需了解所读取消息中的主题(topic)名称,以及它们所发送的消息的主题名称而已,其它像寻址、传递、故障转移等等问题可以交给底层的消息代理进行处理

为了创建一种健壮的基于 Actor 的系统,设计者需要创建一种基于主题的、持久的、高可用的、容错的并且能够水平伸缩的消息处理系统。在云架构中这方面的例子包括 Apache Kafka 和 Windows Azure Service Bus。EventStoreRabbitMQ是一种替代型的本地(或云端)选择。

消息、事件与事实

一条消息就是送往某个 Actor 的一段信息,通常来说消息中包含一个消息头和一个消息体,这一定义也适用于 Actor 消息。另一方面,一个事件通常被描述为“发生在过去的某件事”。

事件与消息存在很大程度上的相似之处。实际上,《企业集成模式》这本书将事件定义为消息的一个子集,即事件消息。不过,这本书的稍后部分提到,文档消息(通常是指消息这个术语本身的含义)与事件消息之间的区别在于它们的时机与内容。换句话说,它相信对于事件来说,保证传递(Guaranteed Delivery)并不十分重要。事件机制主要可用于能够容忍准确性偏差的商业智能(即分析业务指标)系统。

而在另一边,事件可作为系统中唯一的事实来源,基于事件溯源的系统就是这种方式的主要支持者。其它所有类型的数据都是基于这些事件所产生的,可以选择将它们保存在持久性的读模型中,或保存在可抛弃的、基于内存的软存储系统中。

我们将事件定义为具有时间戳的、不可变的、唯一的并且绝对真实的一段信息。这种定义与Nathan Marz对于事实(Fact)的定义有一些相似之处。我们意识到,对于某个业务来说,并非所有的事件都具有同等的重要性,因此它们的存储需求与可用性需求也是不同的。打个比方,在一个零售电子商务场景中(例如 amazon.com),一个包含了订单的细节(产品种类以及数量)的 OrderSubmitted 事件对于整个业务来说是最为重要的,虽然生成该订单的多个变化阶段的信息(产品的选择与取消、数量的变更等等)对于分析来说也很实用,但它对整个业务的重要程度就有所不及了。此外,并非所有的数据都是值得捕获的:将用户在访问网站时每一毫秒的鼠标精确位置都保存下来显然没有什么意义。因此业务本身需要判断要捕获哪些数据。另一方面,保存高可用性的数据是非常昂贵的,因此我们需要区别 SLA 需求中的关键数据与其它类型的数据。这里我们确定了两种类型的事件:业务事件日志事件

业务事件业务工作流的构建块,它需要在事务级别实现一致性和持久性,而日志事件主要用于衡量商业指标以及进行分析,因此不需要同等级别的可用性。业务事件是核心业务的基础(比如在线销售对于零售电子商务的意义),而日志事件中的数据能够为业务决策、处理客户关系或技术排错提供支持。业务事件的丢失会被视为一件严重的意外,而丢失日志事件对于业务来说虽然也很重要,但对于业务功能不会造成灾难性的影响。

将事件作为一种实现控制反转的方法

依赖注入(DI)是实现约定式软件开发的一种重要手段,也是实现控制反转原则的一种模式。松耦合的架构中的事件也能够实现与使用 DI 相同的目标。通过在事件的生产者中去除对消费者的认知,我们也能够得到相同程度的松耦合性。

这种方式与命令(Command)形成了鲜明对比,后者只能够发送给一个单一的处理器。

这种方式与依赖注入中使用的好莱坞原则“不要打电话给我们,我们会打电话给你”在本质上是相同的。使用事件能够实现响应式编程,这在最近几年中逐渐流行起来。使用响应式编程(例如 Reactive Extension,即 Rx)能够简化复杂软件中的依赖网络,而使用响应式、基于事件的架构(例如在响应式宣言中所描述的那样)能够得到一个高度解耦的、可进化的架构。

对于传统的、基于 ESB的 SOA实现来说,将命令替代为事件是最重要的转变。

对事件进行建模

正如我们之前所说,事件是具有时间戳的、不可变的、唯一的并且绝对真实的一段信息。业务流程中的代表性事件包括:CustomerCreated、OrderDispatched、BlogTagAdded 等等。

对事件进行分类能够带来很多便利,事件的类型描述了它所包含的信息,每种事件类型在整个系统中通常都是独一无二的。

一个事件通常会指向系统中的一个或多个聚合(见下文)。举例来说,CustomerCreated 事件通常会包含 CustomerId,而 BlogTagAdded 则包含 PostId 与 tag 信息(如果 Tag 被建模为一个值对象的话),或 TagId(如果 Tag 是一个实体)。

无论每个事件的内容是什么,它本身都需要具备一个标识。两个事件是有可能包含相同的信息的,例如 StaffTakenOffsick 事件。使用 GUID 作为事件的标识是一种实用的方式。在某些情况下,也可能会使用事件类型以及实体的 ID 作为标识(比方说对于某个 CustomerId 来说,只可能出现一次 CustomerCreated 事件),但事件的标识定义应当总是独立于它的内容

事件对象从业务上需要捕获关于该事件的所有信息。在对事件溯源架构进行建模时,要捕获到事件相关的所有信息,并且能够通过对应某个实体的事件流进行重演,从而推导出该实体所经历的所有不同状态。对于大数据分析来说,事件溯源尤其重要。这也是我们对所有信息感兴趣的地方。ItemAddedToBasket 对于业务分析来说是一个重要的事件,而对事务系统来说,它只有在订单提交时才具有意义。另一方面,CustomerCreated 事件中客户的所有信息对于大数据系统来说是至关重要的,因此要将数据独立保存于事务系统之外。而某个信用检查服务会直接访问这一数据,但它只对客户的当前状态感兴趣。

基本上,对于事务系统来说,我们倾向于发布较小的事件,并依靠基本数据结构存储其余状态信息。在这种情况下,事件在整个系统中将成为状态转换的指示器,而状态本身将保存在某些我们将立即访问的数据结构存储机制中。这种方式需要所有的系统都能够访问这个数据结构存储机制,在云环境中,可以非常简便地实现这一假设。

上面所说的这个方式有一个问题,就是你将复写实体的状态,因此你的分析机制将丢失某些可能具有价值的数据,当然这些数据只是用于分析目的,不是事务性的。而你的分析系统也将依赖于它自己的数据存储机制,因此无需访问事务数据存储机制。这时有三种解决方案可以选择:

  • 你通过某种 EventEnricher Actor 订阅这些事件,将额外的状态信息填入事件,并发布这些修改后的事件,让分析系统进行调用。在这种情况下,你依然无法消除丢失某些状态转换信息的风险,因为数据填充过程很可能发生在两次连续的状态转换之后。不过在大多数情况下,它不太可能会对分析过程造成问题。
  • 让你的 Actor 发布两个事件:一个用于事务性用途,另一个用于大数据系统的分析。
  • 完全放弃数据存储机制,而拥抱事件溯源。由于这种方式增加了复杂度,以及之前已解释过的原因,对于主要目标是进行事务处理的系统来说,我们并不推荐这一方式。此外,这种方式也不适合于某些类型的数据结构,例如计数器,因为这会导致产生很高的争用。不过,如果要对这个主题进行彻底讨论,需要再开一篇文章才行。

队列

响应式云 Actor 所需的队列特性包括:

  • 高可用性以及容错性
  • 支持基于主题的订阅
  • 能够租赁某个消息,然后放弃或提交它
  • 能够实现一种延迟队列
  • 支持长轮询
  • 可选择支持批量操作

目前已经有 Kafka、Azure Service Bus、RabbitMQ 和 ActionMQ 等队列提供了以上所列举的功能,其中有些并未实现所有特性。

其它数据结构

传统的软件系统经常会将它们的数据建模为关系型数据库(RDBMS)一种柱状的行。实际上,将数据库表作为队列使用的做法并不罕见,如果你手中只有一把锤子,那么每样东西看上去都是钉子。对于纯粹的事件溯源系统来说,所有的数据都是事件,并且保存在某个事件数据库中,可以选择是否保留快照。在文档数据库中,数据被保存为文档的格式,并可选择使用索引。无论使用哪一种工具,为数据建模选择适当的数据结构都是至关重要的。每个数据结构都对一个特定的操作进行优化,将一段数据硬塞进一个不合适的模型通常会导致不得不采取妥协手段。

我们相信,要创建一个水平伸缩、高性能的事务型云系统,使用BigTable能够表现所有可能的数据结构(队列除外,它将通过某个服务总线实现)。值得注意的是,Redis也实现了所有可能的数据结构,但不幸的是它无法达到事务型系统所需的一致性级别。

保存在各种数据库中的数据都需要具有一个标识,方法是为每个实体创建一个 Id。因此,每个实体必须实现下面这个接口:

public interface IHaveIdentity
{
   string Id { get; }
}

为了满足并发处理的需求,实体可以通过实现以下接口以实现对并发的感知能力:

public interface IConcurrencyAware
{
   string ETag { get; }
   DateTimeOffset? LastModified { get; }
}

数据结构存储机制将检查某个实体是否实现了这个接口,如果是的话,他们就会在更新与删除时对并发冲突进行检验。

下面让我们来审查一下这些关键的结构。

键 -

键 - 值是一种最基础的结构,可以将一个任意的数值(通常是某个未经解释的字符数组)通过一个字符串键进行保存。可以将键进行组合以创建集合,也可以对键名加上前缀。通常来说无法对键进行迭代,这也是它的一大缺点。对于键 - 值存储机制的操作都是原子性的,并且有些能够处理并发性(但也有许多是做不到的),但它们应提供锁的超时机制以及自动清理机制。

键列表

键列表类似于键 - 值列表,但我们能够在一个键中保存一个键 - 值列表。对应该键的列表可以将尺寸扩展到很大,并且能够进行迭代,而在进行更新时也不需要加载所有数据。

集合

集合是指在一种逻辑集合类型中保存的多个键 - 值对。它与简单的键 - 值机制的不同之处在于,集合是能够进行迭代的,虽然这可能会耗费很长时间。可以用键来进行一定范围内的迭代操作,因此我们就可能对一个较小的数据集进行迭代,即应用某个范围。集合应当能够处理并发操作(使用 ETag 及 / 或 LastModified/Timestamp)。

计数器

顾名思义,计数器中保持着一个数字,可以原子式地增加或减少它的值。许多数据存储机制中都缺少了这种数据结构。在本文写作时,Windows Azure 上还不存在原子式的计数器。

对 Actor 建模

正如我们之前所说,我们需要设计 Processor Actor 和 Factory Actor,忘掉 Stateful Actor。那么我们将仔细地进行分别探讨。

Processor Actor

设计

Processor Actor 是完成业务流程的地方,因此它的设计至关重要。对它的设计必须能够适应所有场景,并且仍然能够提供云环境所要求的容错能力,因为在云环境中故障是相对常见的。

我们建议为 Processor Actor 使用以下接口,相信它能够涵盖所有的场景(C# 代码):

public interface IProcessActor: IDisposable
{
   Task> ProcessAsync(Event message);
}

上面的方法接受一个事件参数,并返回一个 promise,对应一个可枚举的列表,可包含 0 至多个事件。一个 Actor 通常应该返回 0 或 1 个事件。在某些情况下它可以返回多个消息。C# 中的 Task 就是一种 promise,并且能够通过使用 async/await 模式实现异步任务。云环境中的处理器通常都是 I/O 密集型的(对队列或存储机制的读写操作),对于使用 IO 完成端口(IOCP)实现异步操作的重要性怎样强调都不过分。

事件类型与队列

Processor Actor 将订阅一或多个事件类型,通常的做法是对某个主题创建一个专门的订阅。有时候,使用一个简单的队列就能够完成任务。但事件驱动架构的性质在于,为某个目的所创建的事件可能对其它目的也同样适用,因此创建一个主题以及一个默认的订阅通常来说是一种更好的选择。虽说如此,但有时在处理一个多步骤的流程时,我们需要明确作为这一流程的分解的事件。

一个 Actor 通常只会注册一种事件类型,但尽管如此,如果它所实现的步骤中包括万一下游步骤出错而实现的补偿行为,那么这个 Actor 就是实现补偿行为最佳的场所。在这种情况下,Actor 也会注册与补偿行为相关的事件。

处理

Actor 必须设计为实现一个单一的功能,并良好地实现它。一旦出现崩溃,那么消息将在超时之后返回队列,并为其它 Actor 所用。重试流程能够管理尝试对某个消息进行处理的次数,以及由操作失败可能产生的延迟。

分散 - 收集(Scatter-Gather)流程在某些时候可以进行分解,将并行流程转化为顺序流程,并为每个流程定义多种事件类型和 Actor。不过,由于流程中可能会产生延迟,因此这一方式未必总是可能的。分散 - 收集流程的最佳实现方式是将工作流状态实现为一个实体(并保存在某个集合存储机制中),其中包含了多个标记。Scatter Actor 在完成每个分散的流程后会返回多个事件(每个步骤一个事件),从而触发某个事件,并由某个 Actor 设置相应的标记。当所有的标记都被清除之后,就能够将流程向前推进了。在实现这个工作流状态的时候,对并发操作的处理能力是非常重要的。

将一个业务流程分解为多个单一步骤,产生的结果是:

  • 当流程产生变更时的敏捷性和灵活性。可以轻易地对步骤进行改动,并创建或移除并行分支。
  • 隔离的部署。每个 Actor 可以独立进行部署。
  • 对开发者来说更简单的编程模型以及对工作单元的测试。
  • Actor 的容错模型更简单。
  • 代码的可维护性与可跟踪性得到了改善。
  • 横切关注点,例如日志记录将通过一个单一的方法调用实现集中化。

以上这些都会使项目的整体成本降低。

以下是这种方式存在的一些缺点:

  • 整个操作的延迟增加了,因为它对队列的调用更多。响应式云 Actor 的目标是以延迟换取简便性和最终一致性。在多数情况下,一个消息经过整个流程的延迟能够控制在几秒钟之内,这完全在可容忍范围之内。
  • 代码将分散在多个地方实现,因此要在代码中跟踪整个流程变得更困难了,对于业务干系人来说也是如此。为了维护流程的可见性,必须始终保持工作流图的更新,这一任务需要由管理及架构团队承担。
  • 当实体通过整个系统时,有必要对它的细节进行日志记录,并使用工具显示它的状态。如果选择创建一个云系统,那么你就必须实现这一点,但实现它所需的精力不应被低估。

Factory Actor

Factory actor 负责创建 Processor Actor 并管理它们的生命周期、建立队列的轮询机制以获取订阅的消息、并将传入的消息传递给目标 Actor 以进行处理:如果消息处理成功,就向消息队列发布事件,并将传入的消息标记为已成功处理。一旦发生错误,即放弃处理传入的消息,或重置它的租赁状态。可以通过 Factory Actor 管理重试机制,或者在队列中进行配置。

Factory Actor 通常是底层 Actor 框架或库的一部分,而无需自己实现。Factory Actor 一般会使用某种依赖注入框架初始化 Processor Actor 以及它的依赖。

BeeHive 是一个微框架,它是我们对响应式 Actor 在云环境中的实现,接下来我们要开始对其进行审查。

BeeHive

BeeHive是响应式云 Actor 在 C# 上的开源实现。这一项目专注于定义接口与模式,以实现一个基于云环境的响应式 Actor 解决方案。目前已经实现了在 Azure 环境中的队列与基本数据结构,如果要实现适用于其它云平台和技术的接口,所需的精力非常小。

事件

事件包含一个用字符串表示的事件体(BeeHive 使用了 JSON 进行序列化,虽然可以实现为插件,但目前还没有这个必要)以及它的元数据:

public sealed class Event : ICloneable
{
   public DateTimeOffset Timestamp { get; set; }
   public string Id { get; private set; }
   public string Url { get; set; }
   public string ContentType { get; set; }
   public string Body { get; set; }
   public string EventType { get; set; }
   public object UnderlyingMessage { get; set; }
   public string QueueName { get; set; }
   public T GetBody()
   {
      ...
   }
   public object Clone()
   {
      ...
   }
}

Actor 的实现

为了创建一个响应式的 Actor,它必须实现 IProcessorActor 接口,其方法将接受一个事件,并返回一个 IEnumerable<Event> 类型的 promise(即 Task)。该 Actor 无需处理意外错误,因为所有未处理的异常都将由 Factory Actor 进行处理,它负责了创建 Actor 并管理它们的生命周期。

IProcessorActor 实现了 IDisposable 接口,因此 Actor 有机会进行清理工作。

由于某个 Actor 能够订阅不同类型的事件,因此它可以访问事件的 EventType 属性以查看该事件的类型。

Actor 配置

Actor 配置将告诉 Factory Actor 如何创建、传递以及管理一个 Processor Actor。由于 Actor 的配置是一件非常乏味而辛苦的任务,因此 BeeHive 内置提供了基于属性的配置方式。

[ActorDescription("OrderShipped-Email")]
public class OrderShippedEmailActor : IProcessorActor
{
   public async Task> ProcessAsync(Event evnt)
   {
      ...
   }
}

事件名称通常包含由连字号连接在一起的两个字符串,前一个字符串表示事件类型的名称(与队列中的名称相同),而后一个字符串表示订阅的名称。打个比方,如果我们为 PaymentActor 类型定义了“OrderAccepted-PaymentProcessing”,那么该 Actor 将订阅 PaymentProcessing 这个订阅中的 OrderAccepted 主题。如果使用简单队列(并非基于主题),那么只需使用事件类型名称。

通过实现 IActorConfiguration 接口,你也可以从文件或数据库中加载 Actor 的配置:

public interface IActorConfiguration
{
   IEnumerable GetDescriptors();
}
}

如果你使用基于属性(默认选择)的配置,那么你可以按照以下方式创建配置的实例:

var config = ActorDescriptors.FromAssemblyContaining()
      ToConfiguration();

Prismo eCommerce 示例

Prismo eCommerce 是使用 BeeHive 实现一个业务流程的示例实现。它实现的业务流程从一个订单被接受开始,随后对订单的所有状态进行追踪,直到订单被取消或配送为止。这一流程包含了付款授权、对欺诈进行检查、检查库存、如果库存不足时进行补货等等。

(单击图片以放大)

在 BeeHive 示例中包括了一个内存中的实现以及基于 Azure 平台的实现。

结论

响应式云 Actor 是创建云应用程序时使用的一种事件驱动范式,这种应用是由独立的 Actor 组成的,它们的功能是纯粹通过对事件的发布与使用的方式实现的。命令式的 Actor 会对其它 Actor 的已知终结点发送 RPC 消息,而响应式 Actor 则完全不了解会有哪些 Actor 使用由它发布的事件。一个响应式 Actor 对一个或多个事件类型感兴趣,并且能够完成一个单一的原子性活动,这简化了容错与补偿行为的设计。所有的事件都保存在水平伸缩的消息代理中,并且通常来说都是持久性的。

这一范式遵循了 Hewitt 对 Actor 的定义,并且通过将每个 Actor 的职责进行分离,定义了这三种 Actor:Processor Actor(负责处理事件,如同上文所述)、Factory Actor(管理 Process Actor 的生命周期,为 Actor 传递消息并提交返回的事件)和 Stateful Actor。Processor Actor 是特定于应用程序的,它负责完成业务活动。而 Factory Actor 通常已经实现,由在该平台上的实现框架提供。Stateful Actor 要尽量避免,它将所有状态都保存在高可用的、基于云的数据存储机制中。

响应式云 Actor 确定了四种不同的基本数据结构以保存状态:键 - 值、集合、计数器以及键列表。可以采用多种不同技术实现键 - 值结构,但典型的实现是 S3。而 Google 的 BigTable 实现(例如 DynamoDB)能够实现其它三种数据结构。

BeeHive 是响应式云 Actor 的一个 C# 实现,其中包括了一个功能丰富的电子商务示例。

本文受到了Fred George在微服务与响应式事件驱动架构上的工作的启发,因此我将本文献给他。

关于作者

Ali Kheyrollahi是一位解决方案架构师、作者、博客作者、开源作者以及贡献者,他目前在伦敦的一家大型电子商务公司就职。他对于 HTTP、Web APIs、REST、领域驱动设计(DDD)和概念建模具有很高的热情,而在处理真实业务问题上始终保持着实际的态度。他具有 12 年以上的行业经常,曾在许多蓝筹股公司中就职。他对于计算机视觉以及机器学习有着强烈的兴趣,并且在这一领域中已经发布了多篇论文。在进入这个行业之前,他曾经是一名医师,作为非专科医生工作了 5 年。他的博客地址在这里,同时他也是一位狂热的 twitter 用户,可以通过 @aliostad 找到他。

查看英文原文:Reactive Cloud Actors: An Evolvable Web of Events