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# 源生成器会被添加到所有包(包括客户端和服务器)中,并自动生成等同于应用程序部件的内容。 - 将对
Microsoft.Orleans.OrleansServiceBus
的引用替换为 MicrosoftOrleansStreaming.EventHubs。 - 如果使用提醒,请添加对 Microsoft.Orleans的引用。提醒。
- 如果使用流处理,请添加对 Microsoft.Streaming 的引用。
提示
所有 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类型将被IHostBuilder上的UseOrleansClient扩展方法替代。
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 和用于跟踪的 ActivitySource。
在此过程中,将删除现有 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.Runtime
Microsoft.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 迁移脚本:
为所使用的数据库选择文件,并按顺序应用它们。