从 Orleans 3.x 迁移到 7.0

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.MSBuildMicrosoft.Orleans.OrleansCodeGenerator.Build 的引用。
  • 删除所有对 Microsoft.Orleans.OrleansRuntime 的引用。
  • 删除对 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>),项目将隐式使用OrleansOrleans.Hosting命名空间。 这意味着应用代码不需要这些 using 指令。

有关详细信息,请参阅 ImplicitUsingsdotnet/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集群。 所有仓库都注册了一个 IGrainFactoryIClusterClient 实例,应用程序可以使用,因此不需要单独配置客户端,也不支持这么做。

OnActivateAsyncOnDeactivateAsync 签名更改

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。 若要访问以下任一扩展方法:

粒度必须实现 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 还定义了 OnActivateAsyncOnDeactivateAsync 的默认实现, 如果需要,允许粒度参与其生命周期:

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形式,其中typekey都是字符串。 最常用的粒度键接口是 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.NamespaceStreamId.KeyStreamId.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.StreamingOrleans.RemindersOrleans.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 迁移脚本:

为所使用的数据库选择文件,并按顺序应用它们。