Orleans 7.0 引入了一些有益的更改,包括对托管、自定义序列化、不可变性和 grain 抽象的改进。
迁移
由于 Orleans 标识 grain 和 stream 的方式发生更改,将使用提醒、stream 或 grain 持久性的现有应用程序迁移到 Orleans 7.0 目前并不容易。
无法通过滚动升级顺利地将运行旧Orleans版本的应用程序升级到Orleans 7.0。 因此,使用不同的升级策略,例如部署新的集群并停用上一个集群。 Orleans 7.0 更改了线协议,导致不兼容,这意味着群集不能包含 7.0 主机和运行以前版本Orleans的主机。
这些重大更改多年来一直被避免,即使在主要版本中也是如此。 为什么是现在? 有两个主要原因:标识和序列化。 关于标识,粒度和流标识现在由字符串组成。 这允许粒度正确编码泛型类型信息,并使映射流更容易地映射到应用程序域。 以前,Orleans 使用一种无法表示通用谷物的复杂数据结构来识别谷物类型,这导致了一些特殊情况。 流由 string 命名空间和 Guid 键标识,这很高效,然而难以映射到应用程序域。 序列化现在是版本容错的。 这意味着可以采用某些兼容方式修改类型,并遵循一组规则,确信可以升级应用程序而不出现序列化错误。 当应用程序类型保留在流或粒度存储中时,此功能特别有用。 以下各节详细介绍了主要更改,并进一步讨论这些更改。
打包更改
将项目升级到 Orleans 7.0 时,执行以下作:
- 所有客户端都应引用 Microsoft.Orleans.Client。
- 所有接收器(服务器)都应引用 Microsoft.Orleans.Server。
- 所有其他包都应引用 Microsoft.Orleans.Sdk。
- 客户端和服务器包都包含对 Microsoft..Sdk 的引用。
- 删除所有对
Microsoft.Orleans.CodeGenerator.MSBuild和Microsoft.Orleans.OrleansCodeGenerator.Build的引用。- 将
KnownAssembly用法替换为 GenerateCodeForDeclaringAssemblyAttribute。 -
Microsoft.Orleans.Sdk包引用 C# 源生成器包 (Microsoft.Orleans.CodeGenerator)。
- 将
- 删除所有对
Microsoft.Orleans.OrleansRuntime的引用。-
Microsoft.Orleans.Server 包引用它的替代项
Microsoft.Orleans.Runtime。
-
Microsoft.Orleans.Server 包引用它的替代项
- 删除对
ConfigureApplicationParts的调用。 应用程序部件 已删除。 C# 源生成器会被添加到所有包(包括客户端和服务器)中,并自动生成等同于Orleans的内容。 - 将对
Microsoft.Orleans.OrleansServiceBus的引用替换为 MicrosoftOrleansStreaming.EventHubs。 - 如果使用提醒,请添加对 Microsoft.Orleans的引用。提醒。
- 如果使用流处理,请添加对 Orleans 的引用。
提示
所有 Orleans 示例都已升级到 Orleans 7.0,可以作为一个参考,看看进行了哪些更改。 有关详细信息,请参阅 Orleans 问题 #8035,其中逐条列出了对每个示例所做的更改。
Orleans 全局 using 指令
所有 Orleans 项目都直接或间接引用了 Microsoft.Orleans.Sdk NuGet 包。
Orleans将项目配置为启用隐式使用(例如,<ImplicitUsings>enable</ImplicitUsings>),项目将隐式使用Orleans和Orleans.Hosting命名空间。 这意味着应用代码不需要这些 using 指令。
有关详细信息,请参阅 ImplicitUsings 和 dotnet/orleans/src/Orleans.Sdk/build/Microsoft.Orleans.Sdk.targets。
托管
该ClientBuilder类型将被UseOrleansClient上的IHostBuilder扩展方法替代。
IHostBuilder 类型源自 Microsoft.Extensions.Hosting NuGet 包。 这意味着 Orleans 客户端可以添加到现有主机,而无需创建单独的依赖项注入容器。 客户端在启动期间连接到群集。 完成IHost.StartAsync后,客户端会自动连接。 按注册顺序添加到 IHostBuilder 启动的服务。 例如,先调用UseOrleansClient再调用ConfigureWebHostDefaults,可确保Orleans在 ASP.NET Core 启动之前启动,这样就可以从 ASP.NET Core 应用程序立即访问客户端。
若要模拟之前的 ClientBuilder 行为,请创建一个单独的 HostBuilder,并使用 Orleans 客户端对其进行配置。
IHostBuilder可以配置为Orleans客户端或Orleans集群。 所有仓库都注册了一个 IGrainFactory 和 IClusterClient 实例,应用程序可以使用,因此不需要单独配置客户端,也不支持这么做。
OnActivateAsync 和 OnDeactivateAsync 签名更改
Orleans 允许 grain 在激活和停用期间执行代码。 使用此功能可以执行读取存储中的状态或记录生命周期消息等任务。 在 Orleans 7.0 中,这些生命周期方法的签名已更改:
- OnActivateAsync() 现在接受 CancellationToken 参数。 一旦CancellationToken取消,即放弃激活过程。
-
OnDeactivateAsync() 现在接受 DeactivationReason 参数和
CancellationToken参数。DeactivationReason指示激活被停用的原因。 使用此信息进行日志记录和诊断。 取消CancellationToken后,请立即快速完成停用过程。 请注意,由于任何主机随时都可能失败,因此不建议依赖OnDeactivateAsync执行重要作,例如保留关键状态。
来看看以下重写这些新方法的 grain 示例:
public sealed class PingGrain : Grain, IPingGrain
{
private readonly ILogger<PingGrain> _logger;
public PingGrain(ILogger<PingGrain> logger) =>
_logger = logger;
public override Task OnActivateAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("OnActivateAsync()");
return Task.CompletedTask;
}
public override Task OnDeactivateAsync(DeactivationReason reason, CancellationToken token)
{
_logger.LogInformation("OnDeactivateAsync({Reason})", reason);
return Task.CompletedTask;
}
public ValueTask Ping() => ValueTask.CompletedTask;
}
POCO 粒度和 IGrainBase
Orleans 中的 grain 不再需要从 Grain 基类或任何其他类继承。 此功能称为 POCO grain。 若要访问以下任一扩展方法:
- DeactivateOnIdle
- AsReference
- Cast
- GetPrimaryKey
- GetReminder
- GetReminders
- RegisterOrUpdateReminder
- UnregisterReminder
- GetStreamProvider
粒度必须实现 IGrainBase 或继承自 Grain。 下面是在粒度类上实现 IGrainBase 的示例:
public sealed class PingGrain : IGrainBase, IPingGrain
{
public PingGrain(IGrainContext context) => GrainContext = context;
public IGrainContext GrainContext { get; }
public ValueTask Ping() => ValueTask.CompletedTask;
}
IGrainBase 还定义了 OnActivateAsync 和 OnDeactivateAsync 的默认实现, 如果需要,允许粒度参与其生命周期:
public sealed class PingGrain : IGrainBase, IPingGrain
{
private readonly ILogger<PingGrain> _logger;
public PingGrain(IGrainContext context, ILogger<PingGrain> logger)
{
_logger = logger;
GrainContext = context;
}
public IGrainContext GrainContext { get; }
public Task OnActivateAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("OnActivateAsync()");
return Task.CompletedTask;
}
public Task OnDeactivateAsync(DeactivationReason reason, CancellationToken token)
{
_logger.LogInformation("OnDeactivateAsync({Reason})", reason);
return Task.CompletedTask;
}
public ValueTask Ping() => ValueTask.CompletedTask;
}
序列化
Orleans 7.0 中最繁琐的变更是引入版本容错序列化程序。 之所以进行此更改是因为应用程序倾向于演变,这给开发人员带来了重大缺陷,因为以前的序列化程序无法容忍向现有类型添加属性。 另一方面,以前的序列化程序很灵活,允许大多数 .NET 类型的表示形式不受修改,包括泛型、多态性和引用跟踪等功能。 更换早已迫在眉睫,但仍然需要高保真的类型表示。 因此, Orleans 7.0 引入了支持 .NET 类型的高保真表示形式的替换序列化程序,同时允许类型发展。 新的序列化程序比上一个序列化程序更高效,导致高达 170% 更高的端到端吞吐量。
有关详细信息,请参阅与 Orleans 7.0 相关的以下文章:
grain 标识
每个粒度都具有由粒度的类型及其密钥组成的唯一标识。 以前的 Orleans 版本使用复合类型来支持 GrainId 的分粒键,以支持以下任一项:
此方法涉及处理粒度键时的一些复杂性。 grain 标识由两个部分组成:类型和键。 类型部分以前由一个数字类型代码、一个类别和 3 个字节的泛型类型信息组成。
粒度标识现在采用type/key形式,其中type和key都是字符串。 最常用的粒度键接口是 IGrainWithStringKey。 这大大简化了 grain 标识的工作流程,并改进了对泛型 grain 类型的支持。
现在,粒度接口还使用人类可读的名称表示,而不是哈希代码和任何泛型类型参数的字符串表示形式的组合。
新系统更具可自定义性,这些自定义项可以由属性驱动。
-
GrainTypeAttribute(String) 上的粒度
class指定其粒度 ID 的 Type 部分。 -
DefaultGrainTypeAttribute(String)上的粒度
interface指定在获取粒度引用时应默认解析的粒度IGrainFactory。 例如,调用IGrainFactory.GetGrain<IMyGrain>("my-key")时,如果指定了上述属性,则粮食工厂将返回对粒度"my-type/my-key"IMyGrain的引用。 - GrainInterfaceTypeAttribute(String) 允许重写接口名称。 使用此机制显式指定名称允许重命名接口类型,而不会中断与现有粒度引用的兼容性。 请注意,在这种情况下,接口也应包含AliasAttribute,因为它的标识可能会被序列化。 有关指定类型别名的详细信息,请参阅有关序列化的部分。
如上所述,重写类型的默认粒度类和接口名称允许重命名基础类型,而不会中断与现有部署的兼容性。
流标识
当 Orleans 流第一次发布时,只能使用 Guid 来标识流。 这种方法在内存分配方面很有效,但使得创建有意义的流标识变得困难,通常需要一些编码或间接性来确定给定用途的相应流标识。
在 Orleans 7.0 中,使用字符串标识流。 包含 Orleans.Runtime.StreamIdstruct 三个属性: StreamId.Namespace、 StreamId.Key和 StreamId.FullKey。 这些属性值是经过编码的 UTF-8 字符串。 有关示例,请参阅 StreamId.Create(String, String)。
将 SimpleMessageStreams 替换为 BroadcastChannel
SimpleMessageStreams (也称短信)在 7.0 中删除。 短信的接口与Orleans.Providers.Streams.PersistentStreams相同,但它的行为非常不同,因为它依赖于直接的内部调用。 为了避免混淆,短信已被删除,并引入了一个名为Orleans.BroadcastChannel的新替代项。
BroadcastChannel 仅支持隐式订阅,在这种情况下可以直接替换。 如果需要显式订阅或必须使用 PersistentStream 接口(例如,如果测试中使用的是 SMS,而生产环境中使用的是 EventHub),那么 MemoryStream 是最佳候选项。
BroadcastChannel 的行为与 SMS 相同,而 MemoryStream 的行为与其他流提供程序相似。 来看看下面的广播频道使用示例:
// Configuration
builder.AddBroadcastChannel(
"my-provider",
options => options.FireAndForgetDelivery = false);
// Publishing
var grainKey = Guid.NewGuid().ToString("N");
var channelId = ChannelId.Create("some-namespace", grainKey);
var stream = provider.GetChannelWriter<int>(channelId);
await stream.Publish(1);
await stream.Publish(2);
await stream.Publish(3);
// Simple implicit subscriber example
[ImplicitChannelSubscription]
public sealed class SimpleSubscriberGrain : Grain, ISubscriberGrain, IOnBroadcastChannelSubscribed
{
// Called when a subscription is added to the grain
public Task OnSubscribed(IBroadcastChannelSubscription streamSubscription)
{
streamSubscription.Attach<int>(
item => OnPublished(streamSubscription.ChannelId, item),
ex => OnError(streamSubscription.ChannelId, ex));
return Task.CompletedTask;
// Called when an item is published to the channel
static Task OnPublished(ChannelId id, int item)
{
// Do something
return Task.CompletedTask;
}
// Called when an error occurs
static Task OnError(ChannelId id, Exception ex)
{
// Do something
return Task.CompletedTask;
}
}
}
迁移到 MemoryStream 更容易,因为只有配置需要更改。 请考虑以下 MemoryStream 配置:
builder.AddMemoryStreams<DefaultMemoryMessageBodySerializer>(
"in-mem-provider",
_ =>
{
// Number of pulling agent to start.
// DO NOT CHANGE this value once deployed, if you do rolling deployment
_.ConfigurePartitioning(partitionCount: 8);
});
OpenTelemetry
遥测系统在 7.0 版本中已更新,并移除以前的系统,以支持标准化的 .NET API,例如用于指标的 .NET Metrics 和用于跟踪的 Orleans。
在此过程中,将删除现有 Microsoft.Orleans.TelemetryConsumers.* 包。 正在考虑一组新的包,以简化将 Orleans 所发出的指标集成到所选的监视解决方案中。 和往常一样,欢迎提供反馈和建议。
dotnet-counters 工具的特点是性能监测,用于临时运行状况监视和初级性能调查。 对于 Orleans 计数器,请使用 dotnet-counters 工具监视它们:
dotnet counters monitor -n MyApp --counters Microsoft.Orleans
同样,将 Microsoft.Orleans 计量添加到 OpenTelemetry 指标,如以下代码所示:
builder.Services.AddOpenTelemetry()
.WithMetrics(metrics => metrics
.AddPrometheusExporter()
.AddMeter("Microsoft.Orleans"));
若要启用分布式跟踪,请配置 OpenTelemetry,如以下代码所示:
builder.Services.AddOpenTelemetry()
.WithTracing(tracing =>
{
tracing.SetResourceBuilder(ResourceBuilder.CreateDefault()
.AddService(serviceName: "ExampleService", serviceVersion: "1.0"));
tracing.AddAspNetCoreInstrumentation();
tracing.AddSource("Microsoft.Orleans.Runtime");
tracing.AddSource("Microsoft.Orleans.Application");
tracing.AddZipkinExporter(options =>
{
options.Endpoint = new Uri("http://localhost:9411/api/v2/spans");
});
});
在前面的代码中,已将 OpenTelemetry 配置为监视以下内容:
Microsoft.Orleans.RuntimeMicrosoft.Orleans.Application
若要传播活动,请调用 AddActivityPropagation:
builder.Host.UseOrleans((_, clientBuilder) =>
{
clientBuilder.AddActivityPropagation();
});
将功能从核心包重构为单独的包
在 Orleans 7.0 中,扩展被分解为不依赖 Orleans.Core的单独包。 即,Orleans.Streaming、Orleans.Reminders和Orleans.Transactions从核心分离。 这意味着这些包完全为使用的内容付费,核心中Orleans没有代码专用于这些功能。 此方法缩小了核心 API 图面和程序集大小,简化了核心并提高了性能。 关于性能,以前 Orleans 中的事务需要在每个方法中执行一定的代码来协调潜在的事务。 现在,这种协调逻辑已移到了逐个方法的层面。
这是一项编译中断性变更。 通过调用以前在基类上 Grain 定义的方法来与提醒或流交互的现有代码可能会中断,因为这些代码现在是扩展方法。 更新未指定 this 的调用(例如,GetReminders),使其包括 this(例如,this.GetReminders()),因为扩展方法必须明确规定。 如果未更新这些调用,将会发生编译错误,并且如果不清楚发生了哪些更改,所需的代码更改可能并不明显。
事务客户端
Orleans7.0 引入了协调事务的新抽象: Orleans.ITransactionClient 以前,只有粮食可以协调事务。 通过 ITransactionClient依赖项注入提供,客户端还可以协调事务,而无需中间粒度。 以下示例从一个帐户提取额度,并在单个事务中将其存入另一个帐户。 从粒子或从依赖注入容器中检索到ITransactionClient的外部客户端调用此代码。
await transactionClient.RunTransaction(
TransactionOption.Create,
() => Task.WhenAll(from.Withdraw(100), to.Deposit(100)));
对于客户端协调的事务,客户端必须在配置期间添加所需的服务:
clientBuilder.UseTransactions();
BankAccount 示例演示了 ITransactionClient 的用法。 有关详细信息,请参阅 Orleans 事务。
调用链重入
grain 是单线程的,默认情况下,从开始到完成逐个处理请求。 换句话说,默认情况下,grain 不是可重入的。 将ReentrantAttribute添加到粒度类中允许粒度以交错方式并发处理多个请求,同时保持单线程处理。 此功能对不具有内部状态或负责执行许多异步操作的粒子(例如发出 HTTP 调用或写入数据库)非常有用。 当请求可以交错时,需要额外注意:在执行await语句之前所观察到的粒的状态,在异步操作完成并且方法恢复执行时可能会发生变化。
例如,下面的 grain 表示一个计数器。 它已标记 Reentrant,允许多个调用交错。
Increment() 方法应递增内部计数器值并返回观察到的值。 但是,由于 Increment() 方法正文在点 await 之前观察粒度的状态,并随后对其进行更新,因此多次交错执行 Increment() 可能会导致 _value 的结果小于 Increment() 方法调用的总数。 这是由于不正确地使用重入而导致的错误。
删除 ReentrantAttribute 就足以解决这个问题。
[Reentrant]
public sealed class CounterGrain : Grain, ICounterGrain
{
int _value;
/// <summary>
/// Increments the grain's value and returns the previous value.
/// </summary>
public Task<int> Increment()
{
// Do not copy this code, it contains an error.
var currentVal = _value;
await Task.Delay(TimeSpan.FromMilliseconds(1_000));
_value = currentVal + 1;
return currentValue;
}
}
为防止出现此类错误,grain 默认是不可重入的。 由于粒子在其实现中执行异步操作时,在等待异步操作完成期间无法处理其他请求,因此吞吐量有所降低。 为了缓解这种情况,Orleans 提供了几个选项,在某些情况下允许重入:
- 对于整个过程:将 ReentrantAttribute 放在谷物上可以使向谷物发出的任何请求与其他请求交错。
- 对于某些方法的子集:将AlwaysInterleaveAttribute 放置在粒度接口方法上,可以允许对该方法的请求与任何其他请求交错,并允许任何其他请求交错到该方法的请求。
- 对于方法的子集:将粒度接口方法置于 ReadOnlyAttribute 粒度 接口 方法上可允许向该方法发出的请求与任何其他
ReadOnly请求交错,并允许任何其他ReadOnly请求将请求交错到该方法。 从这个意义上说,这是一种更受限的形式AlwaysInterleave。 - 对于调用链中的任何请求: RequestContext.AllowCallChainReentrancy() 并允许 RequestContext.SuppressCallChainReentrancy() 选择加入和退出允许下游请求重新输入粒度。 这两个调用都返回一个在退出请求时 必须 释放的值。 因此,请使用它们,如下所示:
public Task<int> OuterCall(IMyGrain other)
{
// Allow call-chain reentrancy for this grain, for the duration of the method.
using var _ = RequestContext.AllowCallChainReentrancy();
await other.CallMeBack(this.AsReference<IMyGrain>());
}
public Task CallMeBack(IMyGrain grain)
{
// Because OuterCall allowed reentrancy back into that grain, this method
// will be able to call grain.InnerCall() without deadlocking.
await grain.InnerCall();
}
public Task InnerCall() => Task.CompletedTask;
选择参与每个粒度、每个调用链的调用链重新进入。 例如,考虑两个粒度:A 和 B。如果粒度 A 在调用粒度 B 之前启用调用链重新进入,则粒度 B 可以在该调用中回调到粒度 A。 如果粒 B 尚未启用调用链的重入性,则粒 A 无法回调到粒 B。 它已在粒度级别和调用链级别启用。
grain 还可以使用 using var _ = RequestContext.SuppressCallChainReentrancy() 抑制调用链重入信息沿调用链向下流动。 这可以防止后续调用的再次执行。
ADO.NET 迁移脚本
为了确保与 Orleans 依赖 ADO.NET 的群集、持久性和提醒的向前兼容性,需要适当的 SQL 迁移脚本:
为所使用的数据库选择文件,并按顺序应用它们。