Orleans 中的最佳做法

Orleans 旨在大幅简化分布式可缩放应用程序(特别是适用于云的应用程序)的构建。 Orleans 发明了虚拟执行组件模型,这是针对云方案优化的执行组件模型的一种演进。

Grain(虚拟执行组件)是基于 Orleans 的应用程序的基本构建块。 它们封装了应用程序实体的状态和行为,并维护其生命周期。 Orleans 的编程模型及其运行时特征比其他模型更适合某些类型的应用程序。 本文档的目的是介绍一些在 Orleans 正常运行的经过尝试且验证的应用程序模式。

合适的应用

考虑 Orleans 时:

  • 有大量(数百、数百万、数十亿甚至数万亿)松散耦合的实体。 从数量的角度看,Orleans 可以轻松地在一个小群集中为地球上的每个人创建一个 grain,前提是该总数中只有一部分人在任意时间点处于活动状态。
    • 示例:用户个人资料、采购订单、应用程序/游戏会话、股票。
  • 实体足够小,可以是单线程实体。
    • 示例:确定是否应根据当前价格购买股票。
  • 工作负载是交互式的。
    • 示例:请求-响应、启动/监视/完成。
  • 预期或可能需要多台服务器。
    • Orleans 在通过添加服务器扩展的群集上运行。
  • 不需要全局协调,或者每次只需在少量几个实体之间进行小规模协调。
    • 可伸缩性和执行性能是通过并行化和分布大量的、基本上独立的任务实现的,无需提供单一同步点。

不合适的应用

Orleans 不太适合以下情况:

  • 必须在实体之间共享内存。
    • 每个 grain 维护自身的状态,不应共享。
  • 少量的大实体可以是多线程实体。
    • 在单个服务中支持复杂逻辑时,微服务可能是更好的选择。
  • 需要全局协调和/或一致性。
    • 这种全局协调将严重限制基于 Orleans 的应用程序的性能。 Orleans 可以轻松扩展到全球规模,而无需深入的手动协调。
  • 长时间运行的操作。
    • 批处理作业、Single Instruction Multiple Data (SIMD) 任务。
    • 这取决于应用程序的需求,可能适合使用 Orleans。

Grain 概述

  • Grain 类似于对象。 但是,它们是分布式的、虚拟的并且异步的。
  • 它们是松散耦合、隔离并且基本上独立的。
    • 每个 grain 都是封装的,也会独立于其他 grain 保持其状态。
    • Grain 独立失败。
  • 避免在 grain 之间进行琐碎通信。
    • 直接使用内存比传递消息的开销要小得多。
    • 将过于琐碎的 grain 组合成单个 grain 可能更好。
    • 需要考虑参数和序列化的复杂性/大小。
    • 反序列化两次可能比重新发送二进制消息的开销更大。
  • 避免瓶颈 grain。
    • 单个协调器/注册表/监视器。
    • 如果需要,请执行分阶段聚合。

异步性

  • 无线程阻塞:所有项必须是异步的(任务异步编程 (TAP))。
  • await 是编写异步操作时使用的最佳语法。
  • 常见方案:
    • 返回具体值:

    • return Task.FromResult(value);

    • 返回相同类型的 Task

    • return foo.Bar();

    • await 某个 Task 并继续执行:

      var x = await bar.Foo();
      
      var y = DoSomething(x);
      
      return y;
      
    • 扇出:

      var tasks = new List<Task>();
      
      foreach (var grain in grains)
      {
          tasks.Add(grain.Foo());
      }
      await Task.WhenAll(tasks);
      
      DoMoreWork();
      

grain 的实现

  • 永远不要在 grain 中执行线程阻塞操作。 除本地计算以外的所有操作必须是显式异步操作。
    • 示例:同步等待 IO 操作或 Web 服务调用、锁定、运行等待条件的过度循环,等等。
  • 何时使用 StatelessWorkerAttribute
    • 执行解密、解压缩等功能操作时,以及在转发以进行处理之前使用。
    • 当多次激活中只需要本地 grain 时使用。
    • 示例:首先在本地 silo 中使用分阶段聚合时表现良好。
  • 默认情况下,grain 是不可重入的。
    • 由于调用周期的原因,可能会发生死锁。
    • 例子:
    • grain 调用自身。
    • 粒度 A 调用 B,后者反过来又调用 A (A -> B -> C -> A)。
    • Grain A 调用 Grain B,而 Grain B 调用 Grain A (A -> B -> A)。
    • 超时用于自动打破死锁。
    • ReentrantAttribute 可用于允许 grain 类可重入。
    • 可重入仍是单线程的,但它可能会交错(在任务之间划分处理/内存)。
    • 处理交错会增加风险,因为容易出错。
  • 继承:
    • Grain 类继承自 Grain 基类。 可将一个或多个 grain 接口添加到每个 grain。
    • 可能需要消歧以便在多个 grain 类中实现相同的接口。
  • 支持泛型。

Grain 状态持久性

Orleans 的 grain 状态持久性 API 简单易用,并提供可扩展的存储功能。

  • Orleans.IGrainState 由 .NET 接口扩展,该接口包含的字段应包含在 grain 的持久化状态中。
  • Grain 是通过 IPersistentState<TState> 持久化的,由 grain 类扩展,该类将强类型 State 属性添加到 grain 的基类中。
  • 初始 Grain<TGrainState>.ReadStateAsync() 在针对 grain 调用 ActiveAsync() 之前自动发生。
  • 当 grain 的状态对象的数据发生变化时,grain 应该调用 Grain<TGrainState>.WriteStateAsync()
    • 通常,grain 在 grain 方法结束时调用 State.WriteStateAsync() 以返回写入约定。
    • 存储提供程序可以尝试批处理写入以提高效率,但行为协定和配置与 grain 使用的存储 API 无关(是独立的)。
    • 计时器是定期写入更新的另一种方法。
    • 应用程序可以通过计时器确定允许的“最终一致性”/无状态数量。
    • 还可以控制更新时间(立即/无/分钟)。
    • 与其他 grain 类一样,PersistentStateAttribute 修饰类只能与一个存储提供程序相关联。
    • StorageProvider(ProviderName = "name") 属性将 grain 类与特定的提供程序相关联。
    • <StorageProvider> 需要添加到 silo 配置文件中,该文件也应包含 [StorageProvider(ProviderName="name")] 中的相应“名称”。

存储提供程序

内置存储提供程序:

  • Orleans.Storage 包含所有内置存储提供程序。

  • MemoryStorage(存储在内存中的没有持久性的数据)仅用于调试和单元测试。

  • AzureTableStorage:

    • 使用可选的 AzureTableStorageOptions.DeleteStateOnClear(硬删除或软删除)配置 Azure 存储帐户信息。
    • Orleans 序列化程序有效地将 JSON 数据存储在一个 Azure 表单元格中。
    • 数据大小限制 == Azure 列的最大大小,即 64kb 二进制数据。
    • 社区贡献的代码,扩展了多个表列的用途,将整体最大大小提高到 1MB。

存储提供程序调试提示

  • TraceOverride Verbose3 将记录更多有关存储操作的信息。
    • 更新 silo 配置文件。
    • 所有提供程序的 LogPrefix="Storage",或使用 "Storage.Memory"/"Storage.Azure"/"Storage.Shard" 的特定类型。

如何处理存储操作失败:

  • Grain 和存储提供程序可以根据需要等待存储操作并重试失败的操作。
  • 未处理的失败将传播回到调用方,并且被客户端视为违反约定。
  • 除了初始读取之外,没有哪个概念可以在存储操作失败时自动销毁激活。
  • 重试故障存储不是内置存储提供程序的默认功能。

Grain 持久性提示

Grain 大小:

  • 最佳吞吐量是使用多个较小的 grain 而不是少量较大 grain 实现的。 但是,有关选择 grain 大小和类型的最佳做法取决于应用程序域模型。
    • 示例:用户、订单等。

外部变化的数据:

  • Grain 可以使用 State.ReadStateAsync() 从存储中重新读取当前状态数据。

  • 也可以使用计时器定期从存储中重新读取数据。

    • 功能要求可以取决于信息的适当“陈旧性”。
    • 示例:内容缓存 grain。
  • 添加和删除字段。

    • 存储提供程序将确定在其持久状态中添加和删除附加字段的影响。
    • Azure 表不支持架构,应自动根据附加字段调整。

编写自定义提供程序:

  • 存储提供程序易于编写,这也是 Orleans 的重要扩展要素。
  • API GrainState API 协定驱动存储 API 协定(WriteClearReadStateAsync)。
  • 存储行为通常可配置(批量写入、硬删除或软删除等),并由存储提供程序定义。

群集管理

  • Orleans 会自动管理群集。
    • 故障节点(即随时可能发生故障并加入的节点)由 Orleans 自动处理。
    • 为群集协议创建的同一个 silo 实例表也可用于诊断。 该表保留群集中所有 silo 的历史记录。
    • 还有严格或较宽松的故障检测配置选项。
  • 故障随时可能发生,属于正常现象。
    • 如果某个 silo 发生故障,在该 silo 上激活的 grain 稍后将在群集中的其他 silo 上自动重新激活。
    • Grain 可能超时。 Polly 等重试解决方案可以帮助重试。
    • Orleans 提供消息传递保证,每条消息最多传递一次。
    • 调用方责任根据需要重试任何失败的调用。
    • 常见做法是从客户端/前端进行端到端重试。

部署和生产管理

横向扩展和缩减:

  • 监视服务级别协议 (SLA)
  • 添加或删除实例
  • Orleans 自动重新平衡并利用新硬件。 但是,将新 silo 添加到群集时,已激活的 grain 不会重新平衡。

日志记录和测试

  • 日志记录、跟踪和监视:
    • 使用依赖项注入来注入日志记录:

      public HelloGrain(ILogger<HelloGrain> logger)
      {
          _logger = logger;
      }
      
    • Microsoft.Extensions.Logging 用于功能性和灵活的日志记录。

测试:

  • Microsoft.Orleans.TestingHost NuGet 包包含可用于创建内存中群集的 TestCluster,该群集默认由两个 silo 组成,可用于测试 grain。
  • 有关详细信息,请参阅 Orleans 单元测试

疑难解答:

  • 使用基于 Azure 表的成员身份进行开发和测试。
    • 与 Azure 存储模拟器一起使用以进行本地故障排除。
    • OrleansSiloInstances 表显示群集的状态。
    • 使用唯一部署 ID(分区键)来简化操作。
  • Silo 未启动。
    • 检查 OrleansSiloInstances 以确定 silo 是否已在其中注册。
    • 确保在防火墙中打开 TCP 端口:11111 和 30000。
    • 检查日志,包括包含启动错误的附加日志。
  • 前端(客户端)无法连接到 silo 群集。
    • 客户端必须托管在 silo 所在的同一服务中。
    • 检查 OrleansSiloInstances 以确保 silo(网关)已注册。
    • 检查客户端日志,以确保网关与 OrleansSiloInstances 表中列出的网关匹配。
    • 检查客户端日志,以验证客户端是否能够连接到一个或多个网关。