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")]
中的相应“名称”。
- 通常,grain 在 grain 方法结束时调用
存储提供程序
内置存储提供程序:
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 协定(
Write
、Clear
、ReadStateAsync
)。 - 存储行为通常可配置(批量写入、硬删除或软删除等),并由存储提供程序定义。
群集管理
-
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 表中列出的网关匹配。
- 检查客户端日志,以验证客户端是否能够连接到一个或多个网关。