次の方法で共有


Orleans 3.x から 7.0 に移行する

Orleans 7.0 では、ホスティング、カスタム シリアル化、不変性、グレイン抽象化の改善など、いくつかの役に立つ変更が導入されています。

移行

リマインダー、ストリーム、またはグレインの永続化を使用する既存のアプリケーションは、Orleans でのグレインとストリームの識別方法の変更により、Orleans 7.0 に簡単に移行できません。 これらのアプリケーションについては、移行パスを段階的に提供する予定です。

以前のバージョンの Orleans を実行しているアプリケーションは、Orleans 7.0 へのローリング アップグレードを使ってスムーズにアップグレードできません。 そのため、新しいクラスターのデプロイや前のクラスターの使用停止など、別のアップグレード戦略を使う必要があります。 Orleans 7.0 では互換性のない方法でワイヤ プロトコルが変更されているため、Orleans 7.0 ホストと、以前のバージョンの Orleans を実行しているホストを、クラスターに混在させることはできません。

このような破壊的変更は、メジャー リリースであっても長年回避してきたのに、なぜ今行うのでしょうか。 主な理由は ID とシリアル化の 2 つです。 ID に関しては、グレインとストリームの ID が文字列で構成されるようになったことで、グレインでジェネリック型の情報を適切にエンコードでき、ストリームをアプリケーション ドメインに簡単にマップできます。 これまで、グレイン型はジェネリック グレインを表すことができない複雑なデータ構造を使って識別されており、問題になることがありました。 ストリームは string 名前空間と Guid キーによって識別され、開発者がアプリケーション ドメインにマップするのは困難でしたが、効率的でした。 シリアル化がバージョン トレラントになったことで、一連の規則に従って特定の互換性のある方法で型を変更でき、シリアル化エラーなしでアプリケーションを確実にアップグレードできます。 これは、アプリケーションの型がストリームまたはグレイン ストレージで保持されるときに特に問題でした。 以下のセクションでは、主な変更について詳しく説明します。

パッケージ化の変更

プロジェクトを Orleans 7.0 にアップグレードする場合は、次のアクションを実行する必要があります。

  • すべてのクライアントで Microsoft.Orleans.Client を参照する必要があります。
  • すべてのサイロ (サーバー) で Microsoft.Orleans.Server を参照する必要があります。
  • 他のすべてのパッケージで、Microsoft.Orleans.Sdk を参照する必要があります。
    • "クライアント" と "サーバー" の両方のパッケージに、Microsoft.Orleans.Sdk への参照が含まれます。
  • Microsoft.Orleans.CodeGenerator.MSBuildMicrosoft.Orleans.OrleansCodeGenerator.Build へのすべての参照を削除します。
    • KnownAssembly の使用を GenerateCodeForDeclaringAssemblyAttribute に置き換えます。
    • Microsoft.Orleans.Sdk パッケージでは、C# ソース ジェネレーター パッケージ (Microsoft.Orleans.CodeGenerator) が参照されています。
  • Microsoft.Orleans.OrleansRuntime へのすべての参照を削除します。
    • Microsoft.Orleans.Server パッケージでは、それの代わりの Microsoft.Orleans.Runtime が参照されています。
  • ConfigureApplicationParts の呼び出しを削除します。 "アプリケーション パーツ" は削除されました。 Orleans 用の C# ソース ジェネレーターがすべてのパッケージ (クライアントとサーバーを含む) に追加され、"アプリケーション パーツ" と同等のものを自動的に生成します。
  • Microsoft.Orleans.OrleansServiceBus への参照を Microsoft.Orleans.Streaming.EventHubs に置き換えます
  • リマインダーを使っている場合は、Microsoft.Orleans.Reminders への参照を追加します
  • ストリームを使っている場合は、Microsoft.Orleans.Streaming への参照を追加します

ヒント

Orleans のすべてのサンプルは Orleans 7.0 にアップグレードされており、変更内容の参照として使用できます。 詳しくは、各サンプルで行われた変更の一覧が示されている Orleans の問題 #8035 をご覧ください。

Orleansglobal using ディレクティブ

すべての Microsoft.Orleans.Sdk プロジェクトでは、Orleans NuGet パッケージを直接または間接的に参照します。 Orleans プロジェクトが暗黙的な using を "有効" にするように構成されている場合 (例: Orleans.Hosting)、<ImplicitUsings>enable</ImplicitUsings>Orleans の両方の名前空間が暗黙的に使われます。 これは、アプリ コードでこれらのディレクティブが必要ないことを意味します。

詳細については、ImplicitUsings および dotnet/orleans/src/Orleans.Sdk/build/Microsoft.Orleans.Sdk.targets を参照してください。

Hosting

ClientBuilder 型は、IHostBuilderUseOrleansClient 拡張メソッドに置き換えられました。 IHostBuilder 型は、Microsoft.Extensions.Hosting NuGet パッケージから取得されます。 つまり、依存関係挿入コンテナーを別に作成しなくても、既存のホストに Orleans クライアントを追加できます。 クライアントは起動時にクラスターに接続します。 IHost.StartAsync が完了すると、クライアントは自動的に接続されます。 IHostBuilder に追加されたサービスは登録の順序で開始されるため、ConfigureWebHostDefaults を呼び出す前に UseOrleansClient を呼び出すと、たとえば ASP.NET Core が起動する前に Orleans が確実に開始され、ASP.NET Core アプリケーションからクライアントにすぐにアクセスできるようになります。

以前の ClientBuilder の動作をエミュレートしたい場合は、HostBuilder を別に作成し、Orleans クライアントでそれを構成できます。 IHostBuilder では、Orleans クライアントまたは Orleans サイロを構成できます。 すべてのサイロで、アプリケーションで使用できる IGrainFactory のインスタンスと IClusterClient が登録されるため、クライアントを個別に構成することは不要であり、サポートされていません。

OnActivateAsyncOnDeactivateAsync のシグネチャの変更

Orleans では、アクティブ化と非アクティブ化の間にグレインでコードを実行できます。 これを使って、ストレージまたはログ ライフサイクル メッセージからの状態の読み取りなどのタスクを実行できます。 Orleans 7.0 では、これらのライフサイクル メソッドのシグネチャが変更されました。

  • OnActivateAsync() は、CancellationToken パラメーターを受け取るようになりました。 CancellationToken が取り消されたら、アクティブ化プロセスを中止する必要があります。
  • OnDeactivateAsync() は、DeactivationReason パラメーターと CancellationToken パラメーターを受け取るようになりました。 DeactivationReason は、アクティブ化が非アクティブ化される理由を示します。 開発者は、ログと診断にこの情報を使う必要があります。 CancellationToken が取り消されたら、非アクティブ化プロセスを直ちに完了する必要があります。 ホストはいつでも失敗する可能性があるため、クリティカル状態の保持などの重要なアクションの実行を OnDeactivateAsync に依存することはお勧めしません。

これらの新しいメソッドをオーバーライドするグレインの次の例を検討してください。

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 基底クラスまたはその他のクラスから継承する必要がなくなりました。 この機能は、POCO グレイン と呼ばれます。 次のいずれかのような拡張メソッドにアクセスするには:

グレインでは、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 型を変更なしで表現できました。 置き換えは延び延びになっていましたが、ユーザーは依然としてその型の忠実度の高い表現を必要としています。 そのため、.NET 型の忠実度の高い表現をサポートしながら、型の進化も可能な代替シリアライザーが Orleans 7.0 で導入されました。 新しいシリアライザーは、以前のシリアライザーよりはるかに効率的であるため、エンド ツー エンドのスループットが最大で 170% 高くなります。

詳しくは、Orleans 7.0 に関連する次の記事をご覧ください。

グレインの ID

各グレインは、グレインの型とそのキーで構成される一意の ID を持っています。 以前のバージョンの Orleans では、次のいずれかのグレイン キーをサポートするため、GrainId に複合型が使われていました。

これには、グレイン キーの処理に関して複雑さが伴います。 グレイン ID は、型とキーの 2 つのコンポーネントで構成されます。 型コンポーネントは、以前は数値型のコード、カテゴリ、3 バイトのジェネリック型情報で構成されていました。

現在のグレイン ID の形式は type/key であり、typekey はどちらも文字列です。 最もよく使われるグレイン キー インターフェイスは IGrainWithStringKey です。 これにより、グレイン ID の動作が大幅に簡素化され、汎用グレイン型のサポートが向上します。

また、グレイン インターフェイスは、ハッシュ コードとジェネリック型パラメーターの文字列表現の組み合わせではなく、人間が判読できる名前を使って表されるようになりました。

新しいシステムはカスタマイズできる範囲が広がり、これらのカスタマイズは属性を使って行うことができます。

  • グレイン classGrainTypeAttribute(String) は、グレイン ID の "型" の部分を指定します。
  • グレイン interfaceDefaultGrainTypeAttribute(String) は、グレインの参照を取得するときに IGrainFactory により既定で解決する必要があるグレインの "型" を指定します。 たとえば、IGrainFactory.GetGrain<IMyGrain>("my-key") を呼び出すと、IMyGrain で前述の属性が指定されている場合は、グレイン ファクトリからグレイン "my-type/my-key" への参照が返されます。
  • GrainInterfaceTypeAttribute(String) ではインターフェイス名をオーバーライドできます。 このメカニズムを使って明示的に名前を指定すると、既存のグレイン参照との互換性を損なうことなく、インターフェイス型の名前を変更できます。 ID がシリアル化される可能性があるため、この場合はインターフェイスにも AliasAttribute が必要であることに注意してください。 型エイリアスの指定について詳しくは、シリアル化に関するセクションをご覧ください。

前に説明したように、型の既定のグレイン クラスとインターフェイス名をオーバーライドすると、既存のデプロイとの互換性を損なうことなく、基になる型の名前を変更できます。

ストリームの ID

最初にリリースされたときの Orleans ストリームは、Guid を使うことによってのみ識別できました。 これはメモリ割り当ての点では効率的でしたが、ユーザーが意味のあるストリーム ID を作ることは難しく、多くの場合、特定の目的に適したストリーム ID を決めるためにエンコードまたは間接参照が必要でした。

Orleans 7.0 のストリームは、文字列を使って識別されるようになりました。 Orleans.Runtime.StreamIdstruct には、StreamId.NamespaceStreamId.KeyStreamId.FullKey の 3 つのプロパティが含まれています。 これらのプロパティ値は、UTF-8 文字列でエンコードされます。 たとえば、StreamId.Create(String, String) のようにします。

BroadcastChannel での SimpleMessageStreams の置き換え

7.0 では SimpleMessageStreams (SMS とも呼ばれます) が削除されました。 SMS には Orleans.Providers.Streams.PersistentStreams と同じインターフェイスがありましたが、グレイン間の直接呼び出しに依存していたため、その動作は非常に異なっていました。 混乱を避けるため、SMS は削除され、代わりに 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

Orleans 7.0 ではテレメトリ システムが更新され、メトリック用の .NET Metrics やトレース用の ActivitySource などの標準化された .NET API が優先して使われるようになったため、以前のシステムは削除されました。

その一環として、既存の Microsoft.Orleans.TelemetryConsumers.* パッケージが削除されています。 Orleans によって出力されるメトリックを、ユーザーが選んだ監視ソリューションに統合するプロセスを効率化するために、新しいパッケージ セットの導入が検討されています。 いつものように、フィードバックと投稿をお寄せください。

dotnet-counters ツールは、アドホックな正常性監視と第 1 レベルのパフォーマンス調査のためのパフォーマンス監視機能を備えています。 Orleans カウンターの場合、dotnet-counters ツールを使って監視できます。

dotnet counters monitor -n MyApp --counters Microsoft.Orleans

同様に、次のコードで示すように、OpenTelemetry メトリックは Microsoft.Orleans メーターを追加できます。

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() など)。 それらの呼び出しを更新しないとコンパイル エラーが発生し、何が変更されたかわからない場合は、必要なコードの変更がはっきりしない可能性があります。

トランザクション クライアント

Orleans 7.0 では、トランザクションを調整するための新しい抽象化 Orleans.ITransactionClient が導入されました。 以前は、グレインでのみトランザクションを調整できました。 依存関係の挿入を介して使用できる ITransactionClient を使うと、クライアントは中間グレインを必要とせずにトランザクションを調整することもできます。 次の例では、1 つのトランザクション内で、ある口座からクレジットを引き出し、別の口座に入金しています。 このコードは、グレイン内から、または依存関係挿入コンテナーから ITransactionClient を取得した外部クライアントから、呼び出すことができます。

await transactionClient.RunTransaction(
  TransactionOption.Create,
  () => Task.WhenAll(from.Withdraw(100), to.Deposit(100)));

クライアントによって調整されるトランザクションの場合は、構成時にクライアントで必要なサービスを追加する必要があります。

clientBuilder.UseTransactions();

BankAccount サンプルでは、ITransactionClient の使用方法が示されています。 詳細については、「Orleans トランザクション」を参照してください。

呼び出しチェーンの再入可能性

グレインはシングル スレッドであり、既定では要求を開始から完了まで 1 つずつ処理します。 つまり、グレインは既定では再入可能ではありません。 ReentrantAttribute をグレイン クラスに追加すると、シングル スレッドのまま、複数の要求をインターリーブ方式で同時に処理できます。 これは、内部状態を保持しないグレインや、HTTP 呼び出しの発行やデータベースへの書き込みのような多くの非同期操作を実行するグレインの場合に役立ちます。 要求がインターリーブ可能なときは、特別な注意が必要です。非同期操作が完了してメソッドが実行を再開するまでに、await ステートメントが変更する前にグレインの状態が観察される可能性があります。

たとえば、次のグレインはカウンターを表します。 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;
    }
}

このようなエラーを防ぐため、グレインは既定では再入不可能になります。 このようにした場合の短所は、グレインが非同期操作の完了を待機している間に他の要求を処理できないため、実装で非同期操作を実行するグレインのスループットが低下することです。 これを軽減するため、Orleans には、特定のケースで再入を許可するいくつかのオプションが用意されています。

  • クラス全体の場合: グレインで ReentrantAttribute を設定すると、グレインへの要求を他の要求とインターリーブできるようになります。
  • メソッドのサブセットの場合: グレインの "インターフェイス" メソッドで AlwaysInterleaveAttribute を設定すると、そのメソッドへの要求を他の要求とインターリーブし、そのメソッドへの要求が他の要求によってインターリーブされるようになります。
  • メソッドのサブセットの場合: グレインの "インターフェイス" メソッドで ReadOnlyAttribute を設定すると、そのメソッドへの要求を他の ReadOnly 要求とインターリーブし、そのメソッドへの要求が他の ReadOnly 要求によってインターリーブされるようになります。 この意味では、それは AlwaysInterleave のより制限された形式です.
  • 呼び出しチェーン内の要求の場合: RequestContext.AllowCallChainReentrancy() と <xref:Orleans.Runtime.RequestContext.SuppressCallChainReentrancy?displayProperty=nameWithType を使うと、ダウンストリームの要求がグレインに再入することを許可するかどうかを選択できます。 両方の呼び出しが返す値は、要求の終了時に破棄する "必要があります"。 そのため、適切な使用方法は次のとおりです。
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 という 2 つのグレインについて考えてみます。グレイン A がグレイン B を呼び出す前に呼び出しチェーンの再入を有効にした場合、グレイン B はその呼び出し内でグレイン A にコールバックし直すことができます。 ただし、グレイン B "でも" 呼び出しチェーンの再入が有効になっているのでない場合、グレイン A はグレイン B にコールバックできません。 これは、グレイン単位、呼び出しチェーン単位です。

グレインでは、using var _ = RequestContext.SuppressCallChainReentrancy() を使って、呼び出しチェーンの再入情報が、呼び出しチェーンの下流に渡されるのを抑制することもできます。 これにより、後続の呼び出しは再入できなくなります。

ADO.NET 移行スクリプト

ADO.NET に依存する Orleans のクラスタリング、永続化、リマインダーとの上位互換性を確保するには、適切な SQL 移行スクリプトが必要です。

使われているデータベースのファイルを選び、順番に適用します。