Service Fabric Reliable Actors 简介

Reliable Actors 是基于 虚拟执行组件 模式的 Service Fabric 应用程序框架。 Reliable Actors API 提供单一线程编程模型,该模型是基于 Service Fabric 所提供的可扩展性和可靠性保证构建的。

什么是执行组件?

执行组件是单线程执行的计算和声明的独立单元。 执行组件模式 是并发或分布式系统的计算模型,在此类系统中,大量执行组件可同时并且相互独立地执行。 执行组件可相互通信,并且可以创建更多执行组件。

何时使用 Reliable Actors

Service Fabric Reliable Actors 是执行组件设计模式的实现。 与任何软件设计模式一样,是否使用特定模式取决于该模式能否解决相关软件设计问题。

虽然执行组件设计模式可以很好的适应很多分布式系统问题和场景的需求,但是仍有必要仔细考虑该模式及其必须实现的框架的限制。 对于以下情况,通常应考虑使用执行组件模式对问题或场景进行建模:

  • 问题空间包含大量(几千或更多)小型、独立的状态和逻辑单元。
  • 要处理不要求与外部组件进行大量交互的单线程对象,包括查询一组执行组件的状态。
  • 执行组件实例不会通过发出 I/O 操作,用不可预测的延迟阻止调用方。

Service Fabric 中的执行组件

在 Service Fabric 中,执行组件在 Reliable Actors 框架(在 Service Fabric Reliable Services 的基础上构建而成的基于执行组件模式的应用程序框架)中实现。 所编写的每个 Reliable Actor 服务实际上都是分区的有状态 Reliable Service。

每个执行组件定义为执行组件类型的一个实例,与 .NET 对象是 .NET 类型的一个实例类同。 例如,可能存在用于实现计算器功能的执行组件类型,该类型的很多执行组件分布于群集中各个节点。 每个此类执行组件都由执行组件 ID 唯一标识。

执行组件生存期

Service Fabric 执行组件是虚拟的,这表示其生存期不依赖于其内存中表示形式。 因此,不需要显式创建或销毁它们。 Reliable Actors 运行时会在它第一次接收到执行组件 ID 的请求时自动激活此执行组件。 如果一段时间未使用某个执行组件,则 Reliable Actors 运行时将回收此内存对象。 它还会保留该执行组件的存在信息,以便将来将其重新激活。 如需了解更多详情,请参阅执行组件生命周期和垃圾回收

由于采用虚拟执行组件模型,并且实际上 Reliable Actors 实现有时会偏离此模型,虚拟执行组件生命周期抽象存在一些注意事项。

  • 当第一次将消息发送到执行组件的执行组件 ID 时将自动激活此执行组件,并由此构造执行组件对象。 一段时间之后,会回收该执行组件对象。 将来可以再次使用此执行组件 ID 来构造新的执行组件对象。 如果执行组件的状态存储在状态管理器中,则其生命周期长于该对象的生命周期。
  • 针对某个执行组件 ID 调用任何执行组件方法都会激活该执行组件。 出于此原因,执行组件类型在运行时隐式调用其构造函数。 因此,虽然服务本身可将参数传递给执行组件的构造函数,但是客户端代码无法将参数传递给执行组件类型的构造函数。 所以,如果执行组件需要客户端提供初始化参数,则在执行组件上调用其他方法时,以部分初始化状态构造该执行组件。 从客户端激活执行组件不存在单一入口点。
  • 虽然 Reliable Actors 隐式创建执行组件对象,但仍可显示删除执行组件及其状态。

分布和故障转移

为提供可伸缩性和可靠性,Service Fabric 在整个群集中分布执行组件,并根据需要将其从故障节点自动迁移到正常节点。 这是对已分区的有状态可靠服务 进行抽象。 执行组件在称为 执行组件服务的有状态 Reliable Service 内部运行,因此,可提供分布、可伸缩性、可靠性和自动故障转移。

执行组件分布在执行组件服务的各个分区中,而这些分区分布在一个 Service Fabric 群集的各个节点中。 每个服务分区包含一组执行组件。 Service Fabric 管理服务分区的分布和故障转移。

例如,如果有 3 个节点采用默认执行组件分区放置,则部署到这些节点的 9 个分区的执行组件服务将分布如下:

Reliable Actors 分布

执行组件框架用于管理分区方案和键范围设置。 这可以简化一些选项,但也带来了以下注意事项:

  • Reliable Services 允许选择分区方案、键范围(当使用范围分区方案时)和分区计数。 Reliable Actors 仅限于使用范围分区方案(统一 Int64 方案),要求用户使用完整的 Int64 键范围。
  • 默认情况下,执行组件随机放入各分区,形成统一分布。
  • 由于执行组件随机分布,因此可预期执行组件操作将始终需要网络通信,包括对方法调用数据的序列化和反序列化,这会产生延迟和开销。
  • 在高级方案中,可使用映射到特定分区的 Int64 执行组件 ID 控制执行组件分区放置。 但是,这样做会导致执行组件在各分区间分布不均衡。

若要详细了解如何对执行组件服务进行分区,请参阅执行组件的分区概念

执行组件通信

执行组件交互在接口中定义,实现该接口的执行组件和通过同一接口获取执行组件代理的客户端共享该接口。 因为该接口用于异步调用执行组件方法,所以该接口上使用的每个方法都必须返回任务。

由于方法调用及其响应最终导致群集范围的网络请求,因此必须通过平台对这些请求的参数及其所返回任务的结果类型进行序列化。 特别是,它们必须可进行数据协定序列化

执行组件代理

Reliable Actors 客户端 API 提供执行组件实例和执行组件客户端之间的通信。 为与执行组件进行通信,客户端需创建实现执行组件接口的执行组件代理对象。 客户端通过在代理对象上调用方法与执行组件进行交互。 执行组件代理可以用于从客户端到执行组件以及从执行组件到执行组件的通信。

// Create a randomly distributed actor ID
ActorId actorId = ActorId.CreateRandom();

// This only creates a proxy object, it does not activate an actor or invoke any methods yet.
IMyActor myActor = ActorProxy.Create<IMyActor>(actorId, new Uri("fabric:/MyApp/MyActorService"));

// This will invoke a method on the actor. If an actor with the given ID does not exist, it will be activated by this method call.
await myActor.DoWorkAsync();
// Create actor ID with some name
ActorId actorId = new ActorId("Actor1");

// This only creates a proxy object, it does not activate an actor or invoke any methods yet.
MyActor myActor = ActorProxyBase.create(actorId, new URI("fabric:/MyApp/MyActorService"), MyActor.class);

// This will invoke a method on the actor. If an actor with the given ID does not exist, it will be activated by this method call.
myActor.DoWorkAsync().get();

请注意,用于创建执行组件代理对象的两条信息是执行组件 ID 和应用程序名称。 执行组件 ID 用于唯一标识执行组件,而应用程序名称用于标识其中部署执行组件的 Service Fabric 应用程序

客户端上的 ActorProxy(C#) / ActorProxyBase(Java) 类执行必要的解析工作,以便按 ID 查找执行组件,并使用该执行组件打开信道。 如果出现通信故障和故障转移,它还会重新尝试查找执行组件。 因此,消息传送具有以下特征:

  • 消息传送将尽力而为。
  • 执行组件可能会收到来自同一客户端的重复消息。

并发

Reliable Actors 运行时提供基于轮次的简单访问模型,用于访问执行组件方法。 这意味着在执行组件对象的代码内,任何时候都不可能超过一个活动线程。 基于轮次的访问极大简化了并发系统,因为无需使用数据访问的同步机制。 它还意味着系统的设计必须特别考虑每个执行组件实例的单线程访问性质。

  • 单个执行组件实例一次只能处理一个请求。 如果期望一个执行组件实例处理并发请求,它会导致吞吐量瓶颈。
  • 如果两个执行组件之间存在循环请求,并同时向其中一个执行组件进行了外部请求,则这两个执行组件可能相互死锁。 执行组件运行时对于执行组件调用会自动超时,并向调用方抛出异常,以便中断可能出现的死锁情况。

Reliable Actors 通信

基于轮次的访问

一个轮次包含完全执行用于响应其他执行组件或客户端的请求的执行组件方法,或完全执行计时器/提醒回叫。 即使这些方法和回调是异步的,执行组件运行时也不会交替执行它们。 必须彻底完成一个轮次,才允许启动新的轮次。 换言之,必须彻底完成当前正在执行的执行组件方法或计时器/提醒回调,才允许使用新的方法调用或回调。 如果执行已从方法或回调中返回,且由方法或回调返回的任务已完成,则将此方法或回调视为已完成。 值得强调的是,即使在不同的方法、计时器和回调中,也遵从基于轮次的并发执行。

通过在轮次的开始获取按执行组件锁并在轮次的结束释放锁,执行组件运行时强制执行基于轮次的并发。 因此,基于轮次的并发按执行组件执行,而不跨执行组件执行。 执行组件方法和计时器/提醒回调可以代表不同执行组件同时执行。

以下示例对上述概念进行了说明。 假设一种类型的执行组件实现两个异步方法(即 Method1 和 Method2)、一个计时器和一个提醒。 下面的示例图展示了代表属于这种执行组件类型的两个执行组件(ActorId1 和 ActorId2)执行这些方法和回叫的时间线。

Reliable Actors 运行时基于轮次的并发执行和访问

该图遵循以下约定:

  • 每条垂直线显示代表特定执行组件执行方法或回调的逻辑流。
  • 每条垂直线上标记的事件按时间顺序发生,较新事件发生在较旧事件之后。
  • 时间线上使用不同的颜色对应不同的执行组件。
  • 突出显示用于指示代表方法或回调保持按执行组件锁的持续时间。

要重点考虑的几点:

  • 当代表 ActorId2 执行 Method1 以响应客户端请求 xyz789 时,收到另一个客户端请求 (abc123),也要求由 ActorId2 执行 Method1。 但是,在前面的执行完成之前,不会开始 Method1 的第二次执行。 同样,在执行 Method1 以响应客户端请求 xyz789 时,触发了 ActorId2 注册的提醒。 仅在这两个 Method1 执行都完成之后才执行提醒回调。 所有这些都是由于正在为 ActorId2强制执行基于轮次的并发。
  • 同样,也为 ActorId1 强制执行了基于轮次的并发,代表 ActorId1 以串行方式执行 Method1、Method2 和计时器回叫就表明了这一点。
  • 代表 ActorId1 执行 Method1 与代表 ActorId2 执行此方法相互重叠。 这是因为仅在同一个执行组件内,而不是在不同执行组件之间强制执行基于轮次的并发执行。
  • 在一些方法/回叫执行中,方法/回叫返回的 Task(C#) / CompletableFuture(Java) 在方法返回之后完成。 在另一些方法/回叫执行中,异步操作在方法/回叫返回时就已完成。 在这两种情况下,只有在方法/回叫返回且异步操作已完成后,才会解除每个执行组件锁定。

重新进入

执行组件运行时默认情况下允许重新进入。 也就是说,如果 Actor A 的执行组件方法调用 Actor B 上的方法,后者反过来又调用 Actor A 上的另一个方法,则允许运行另一个方法。 这是因为它是同一逻辑调用链上下文的一部分。 所有计时器和提醒调用都以新的逻辑上下文开始。 如需了解更多详情,请参阅 Reliable Actors 可重入性

并发保证的范围

执行组件运行时控制这些方法的调用时,将提供这些并发保证。 例如,它为响应客户端请求而执行的方法调用和计时器与提醒回调提供这些保证。 但是,如果执行组件代码在执行组件运行时提供的机制之外直接调用这些方法,则运行时不能提供任何并发保证。 例如,如果在与执行组件方法返回的任务不相关联的某项任务的上下文中调用方法,则运行时不能提供并发性保证。 如果通过执行组件依据其自身创建的线程调用方法,那么运行时也不能提供并发性保证。 因此,若要执行后台操作,执行组件应使用遵从基于轮次的并发的执行组件计时器和执行组件提醒

后续步骤