本文へスキップ
【.NET Orleans入門】第4回 クラスタリングと高可用性 - 本番環境でのOrleans運用のアイキャッチ画像
Architecture

【.NET Orleans入門】第4回 クラスタリングと高可用性 - 本番環境でのOrleans運用

公開: 更新: 約8分で読めます

はじめに

これまでの連載では、Orleans の基本概念、ステート管理と永続化、そして Grain 間通信とストリーミングを扱ってきました。第4回となる今回は、Orleans を本番環境で安定して動かすための「クラスタリングと高可用性」を取り上げます。

Orleans は複数のサイロ(Silo。Grain をホストするサーバープロセス)を束ねて一つのクラスターとして動かし、障害が起きても自動で復旧しながら処理を続ける仕組みを備えています。本稿では .NET 10 を前提に、メンバーシップの構成、Grain の配置とディレクトリ、障害検出とリバランス、無停止でのアップグレード、そして Kubernetes や Azure Container Apps での運用までを順に見ていきます。

複数のサイロがメンバーシッププロバイダー(Azure Table / SQL / Redis)を介してクラスターを形成し、サイロ障害時にグレインが生存サイロへリバランスされ、Kubernetes / Azure Container Apps 上でローリングアップグレードされる構成図
サイロはメンバーシップストアを共有してクラスターを形成し、障害時にはグレインが生存サイロへ自動で再配置されます

サイロをクラスターにまとめるメンバーシップ

クラスターの土台になるのがメンバーシップです。各サイロは共有ストアに自分の存在を書き込み、互いの生存を監視し合うことで、いま誰がクラスターに参加しているかという一貫した見え方を保ちます。この共有ストアを提供するのがクラスターメンバーシッププロバイダーで、運用環境に合わせて選びます。

代表的な選択肢は Azure Table Storage、ADO.NET(SQL Server / PostgreSQL / MySQL)、Redis の3つです。可用性と運用のしやすさを基準に、既存の資産に合わせて選ぶのが基本になります。.NET 10 では汎用ホストの Host.CreateApplicationBuilder に対して UseOrleans でサイロを構成します。

var builder = Host.CreateApplicationBuilder(args);

builder.UseOrleans(silo =>
{
    silo.Configure<ClusterOptions>(options =>
    {
        options.ClusterId = "production";      // クラスターの識別子
        options.ServiceId = "OrderingService"; // デプロイをまたいで不変にする
    });

    // Azure Table Storage をメンバーシップに使う
    silo.UseAzureStorageClustering(options =>
        options.TableServiceClient = new TableServiceClient(
            builder.Configuration["Orleans:TableConnectionString"]));
});

using var host = builder.Build();
await host.RunAsync();

ClusterId は同一クラスターを識別する値で、ServiceId はデプロイをまたいで不変にします。バージョンの異なるアプリを同じ ClusterId で混在させると状態が壊れる恐れがあるため、避けてください。ADO.NET や Redis を使う場合は、クラスタリングの呼び出しだけを差し替えます。

// ADO.NET(SQL Server / PostgreSQL / MySQL)
silo.UseAdoNetClustering(options =>
{
    options.Invariant = "Microsoft.Data.SqlClient";
    options.ConnectionString = configuration["Orleans:SqlConnectionString"];
});

// Redis
silo.UseRedisClustering(options =>
    options.ConfigurationOptions = ConfigurationOptions.Parse(
        configuration["Orleans:RedisConnectionString"]!));

グレインディレクトリと配置戦略

Grain は仮想アクターで、呼び出された時に初めていずれかのサイロ上で活性化(アクティベーション)されます。どの Grain がどのサイロで動いているかを解決するのがグレインディレクトリで、クラスター全体で単一のアクティベーションを保証する役割を担います。既定では分散ディレクトリがクラスター内に分かれて保持されますが、Azure Table や Redis を使ったディレクトリプロバイダーへ差し替えることもできます。

新しくアクティベーションを作る先を決めるのが配置戦略です。用途に応じて属性で指定します。

  • [ResourceOptimizedPlacement] — 各サイロの CPU やメモリの状況を見て負荷の低いサイロを選ぶ、現在の推奨戦略
  • [ActivationCountBasedPlacement] — アクティベーション数の少ないサイロを選ぶ
  • [PreferLocalPlacement] — 呼び出し元と同じサイロを優先し、通信ホップを減らす
  • [HashBasedPlacement] — キーのハッシュで配置先を固定する
[ResourceOptimizedPlacement]
public sealed class CartGrain : Grain, ICartGrain
{
    // クラスター全体の負荷を見て配置される
}

// グレインディレクトリを Azure Table に差し替える例
builder.UseOrleans(silo =>
{
    silo.AddAzureTableGrainDirectory("orders-directory", options =>
        options.TableServiceClient = new TableServiceClient(connectionString));
});

[GrainDirectory(GrainDirectoryName = "orders-directory")]
public sealed class OrderGrain : Grain, IOrderGrain { }

配置に迷ったら ResourceOptimizedPlacement を基本にしておくと、スケールアウト時の偏りを抑えられます。特定サイロにアクセスを固めたい場合だけ、目的に合った戦略を選び直します。

障害検出とリバランス

サイロは一定間隔で互いにプローブ(死活確認)を送り合い、応答しないサイロを検出します。単独の判断で相手を落とすのではなく、複数のサイロの投票で合意してから該当サイロを停止済みと宣言する仕組みになっているため、ネットワークの一時的な揺らぎでは誤検出しにくくなっています。この挙動は ClusterMembershipOptions で調整します。

silo.Configure<ClusterMembershipOptions>(options =>
{
    options.NumMissedProbesLimit = 3;          // 連続何回の失敗で疑うか
    options.ProbeTimeout = TimeSpan.FromSeconds(5);
    options.NumVotesForDeathDeclaration = 2;   // 停止宣言に必要な投票数
    options.IAmAliveTablePublishTimeout = TimeSpan.FromMinutes(5);
});

あるサイロが失われると、そこで動いていた Grain のアクティベーションも失われます。Orleans は次にその Grain が呼ばれた時点で、生存しているサイロ上に自動で活性化し直します。ステートは第2回で扱った永続化ストアから読み直されるため、状態を保ったまま処理を継続できます。ここで大切なのは、Grain がいつ再活性化されてもよいように、OnActivateAsync でステートを読み込む設計を徹底しておくことです。

逆に、新しいサイロが参加すると、以後の新規アクティベーションはそのサイロにも配置され、時間とともに負荷が平準化していきます。既存のアクティベーションをその場で移動させるわけではない点は押さえておくとよいでしょう。負荷を早く分散させたい場合は、配置戦略とアクティベーションの寿命(アイドル時の非活性化)の設計で調整します。

ローリングアップグレードと無停止デプロイ

本番でバージョンを上げる時は、全サイロを同時に落とさず、少数ずつ入れ替えるローリングアップグレードが基本です。Orleans はサイロのグレースフルシャットダウンに対応しており、host.StopAsync()(あるいは SIGTERM の受信)を受けると、新規アクティベーションの受け入れを止め、担当していた Grain を非活性化してからクラスターを離脱します。非活性化の際にステートが書き戻されるため、入れ替え中もデータを失いません。

// SIGTERM を受けてからの猶予を十分に取る
builder.Services.Configure<HostOptions>(options =>
    options.ShutdownTimeout = TimeSpan.FromMinutes(3));

// 明示的に停止する場合
await host.StopAsync();

注意点として、Grain のインターフェースやシリアライズ対象の型を変更する時は後方互換に気をつける必要があります。ローリング中は新旧のサイロがクラスターに混在する瞬間があるため、メンバーの削除や非互換な型変更は避け、[Id] を付与したメンバーの番号を後から再利用しないようにします。互換性を壊す変更が避けられない場合は、いったんクラスターを分けてから切り替える運用を検討します。

Kubernetes と Azure Container Apps での運用

Kubernetes 上では、各 Pod が一つのサイロになります。Deployment でレプリカを並べ、メンバーシップは前述の Azure Table などの外部ストアに任せるのが素直な構成です。Pod 間でサイロポートとゲートウェイポートが通るようにし、readiness / liveness プローブと十分な終了猶予を設定します。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: orleans-silo
  namespace: ordering
spec:
  replicas: 3
  selector:
    matchLabels:
      app: orleans-silo
  template:
    metadata:
      labels:
        app: orleans-silo
    spec:
      containers:
        - name: silo
          image: myregistry.azurecr.io/ordering-silo:1.4.0
          ports:
            - { containerPort: 11111, name: silo }     # サイロ間通信
            - { containerPort: 30000, name: gateway }   # クライアント接続
          env:
            - name: POD_IP
              valueFrom:
                fieldRef:
                  fieldPath: status.podIP
            - name: ORLEANS_CLUSTER_ID
              value: production
          readinessProbe:
            httpGet: { path: /health/ready, port: 8080 }
            initialDelaySeconds: 10
          livenessProbe:
            httpGet: { path: /health/live, port: 8080 }
            initialDelaySeconds: 20
      terminationGracePeriodSeconds: 200   # グレースフルシャットダウンの猶予

サイロが自分をクラスターに正しく広告できるよう、広告アドレスに Pod の IP を渡します。

silo.Configure<EndpointOptions>(options =>
{
    options.AdvertisedIPAddress = IPAddress.Parse(
        Environment.GetEnvironmentVariable("POD_IP")!);
    options.SiloPort = 11111;
    options.GatewayPort = 30000;
});

Azure Container Apps でも同様に、外部メンバーシップストアを使えば Orleans クラスターを構成できます。ACA では各レプリカが環境内の内部ネットワークで直接通信できるため、サイロ間通信が成立します。スケールインでサイロが落とされてもメンバーシップが吸収しますが、ステートフルなサイロではゼロスケール(最小レプリカ数を 0)は避け、最低限のレプリカ数を確保しておくと安全です。スケールアウトはレプリカ数を増やすだけでよく、新しいサイロは参加後に自動で負荷を引き受けます。ローリング更新中の非活性化が途中で打ち切られないよう、終了猶予も長めに取っておきます。

まとめ

本番環境での Orleans 運用は、メンバーシップの選択と可用性設計の積み重ねで決まります。最後に押さえておきたい勘所を整理します。

  • メンバーシップ — Azure Table / ADO.NET / Redis から、可用性の高いマネージドサービスを選ぶ
  • 冗長性 — 最低3サイロを目安に、1台落ちても処理能力と定足数を保てる余裕を持たせる
  • 配置 — 迷ったら ResourceOptimizedPlacement を基本にし、Grain のキー設計でホットスポットを避ける
  • 無停止更新 — グレースフルシャットダウンの猶予を長めに取り、型変更では後方互換を守る
  • 可観測性 — アクティブサイロ数・アクティベーション数・呼び出し遅延を継続的に監視する

エンハンスド株式会社では、Orleans を使った分散システムの設計から、Kubernetes / Azure Container Apps での本番運用、可用性とスケーラビリティの改善までを支援しています。

次回の第5回では、パフォーマンスチューニングとトラブルシューティングを取り上げ、Orleans アプリの性能を引き出す実践的な手法を解説します。

この記事をシェア

コピーしました

関連記事