多数の利用者が同時にアクセスするサービスを支えるうえで、状態を持ったまま高いスループットをさばく仕組みは避けて通れない課題になります。リアルタイム性の高いゲーム、金融取引、IoT のデバイス管理、大量のイベント集計といった領域では、リクエストごとにデータベースへ問い合わせる素朴な構成では応答時間もコストも膨らみやすく、キャッシュや分散ロックを重ねるほど設計が複雑になっていきます。
.NET Orleans は、こうした分散アプリケーションの難しさを、アクターモデルという抽象で扱いやすくするフレームワークです。本連載は Orleans を用いた高性能な Web アプリケーションの構築を段階的に扱うもので、第1回では全体像として、Orleans が何を解決するのか、グレインとサイロという中心概念、なぜ高スケールなワークロードに向くのか、そして .NET 10 と ASP.NET Core への組み込みまでを、コード例を交えながら丁寧に整理します。
.NET Orleans とは何か
.NET Orleans は Microsoft が開発している分散アプリケーション向けのフレームワークで、アクターモデルをベースにしています。アクターモデルとは、状態と振る舞いを持つ小さな単位(アクター)がメッセージを受け取って処理し、互いに直接メモリを共有しないという並行計算の考え方です。Orleans はこのアクターを「バーチャルアクター」として提供し、開発者がアクターの生成や配置、破棄を明示的に管理しなくてよいように設計されています。
一般的なアクター実装では、アクターを作り、参照を保持し、不要になれば破棄するという生存管理をアプリケーション側で意識する必要がありました。Orleans のバーチャルアクターは、識別子さえ指定すれば常に「存在している」ものとして扱えます。実際にメモリ上へ配置されるのは呼び出しが発生した瞬間であり、一定時間使われなければ自動的にメモリから降ろされます。開発者から見れば、対象がどのサーバーのメモリに乗っているか、いま活性化しているかどうかを気にせず、識別子を通じて呼び出すだけで済みます。この位置透過性が、分散システムの記述を大きく簡素にします。

グレインとサイロ — Orleans を構成する二つの要素
Orleans におけるバーチャルアクターは「グレイン(Grain)」と呼ばれます。グレインは業務上の一つの実体、たとえば特定のユーザー、注文、デバイス、ゲームセッションといった単位に対応させて設計します。各グレインは自身の状態を保持し、その状態を触る操作はインターフェースとして公開されたメソッド越しにのみ行われます。
グレインを実際に動かすホストプロセスが「サイロ(Silo)」です。サイロはグレインの活性化、配置、ライフサイクル管理、メッセージのルーティングを担い、複数のサイロが集まって一つの「クラスター」を形成します。あるグレインへの呼び出しがどのサイロで処理されるかはランタイムが決定し、負荷や障害の状況に応じて配置が調整されます。サイロを増やせばクラスター全体の処理能力が上がり、グレインは空いているサイロへ分散して活性化されていきます。
重要な性質として、一つのグレインの活性化はクラスター内で同時にひとつだけであり、そのグレインへのメソッド呼び出しは一度に一件ずつ順番に処理されます。つまり単一グレインの内部は実質的にシングルスレッドで動くため、グレインが持つ状態を更新する際にロックを書く必要がありません。並行性の制御をランタイムに委ねられることが、状態を持つロジックの実装を素直にしてくれます。
// グレインのインターフェース。識別子の型で継承元を選ぶ
public interface ICounterGrain : IGrainWithStringKey
{
Task<int> IncrementAsync();
Task<int> GetCountAsync();
}
// 呼び出し側は識別子を渡してグレインの参照を得るだけ
var counter = grainFactory.GetGrain<ICounterGrain>("page-views:/pricing");
var current = await counter.IncrementAsync();
ここで GetGrain はグレインを新しく作る操作ではなく、指定した識別子のグレインへの参照を得る操作です。その識別子のグレインがまだメモリ上になければ、最初のメソッド呼び出しの時点でランタイムがいずれかのサイロに活性化します。呼び出す側はその過程を意識しません。
なぜ高性能・高スケールなワークロードに向くのか
Orleans が高いスループットを出しやすい第一の理由は、状態がメモリ上のグレインに保持される点にあります。よく使われるユーザーやセッションのグレインは活性化されたままメモリに乗り続けるため、読み取り操作はデータベースを介さずに応答できます。リクエストのたびに外部ストアへ往復する構成に比べ、応答時間のばらつきが抑えられ、データベースへの負荷も軽くなります。
第二に、グレイン単位でシングルスレッド実行が保証されるため、状態更新にロックやトランザクション調整を持ち込まずに整合性を保てます。ロック競合はスケールアウトのボトルネックになりやすい要素ですが、Orleans ではグレインという細かい単位に処理が分かれており、それぞれが独立して進むため、全体としては高い並行度を保ったままスケールします。
第三に、負荷の増加へはサイロを追加することで対応できます。クラスターへ新しいサイロが加わると、ランタイムはグレインの配置を調整して負荷を分散させます。サイロが障害で失われた場合も、そこで活性化していたグレインは別のサイロで活性化し直され、永続化されている状態から復元されます。開発者がフェイルオーバーの配線を手書きする必要はありません。こうした水平スケールと障害復旧の仕組みがフレームワークに組み込まれていることが、大規模なワークロードで Orleans が選ばれる理由になっています。
状態管理の考え方
グレインの状態には二つの層があります。ひとつは活性化中にメモリへ保持される作業中の状態、もうひとつはグレインが降ろされても失われないよう外部ストアへ書き出す永続状態です。Orleans はこの永続状態を IPersistentState<T> という抽象で扱い、コンストラクタで注入して使います。状態の読み込みは活性化時に自動で行われ、書き込みは明示的に WriteStateAsync を呼んだ時点で永続化されます。
// 永続化する状態。Orleans のシリアライザ向けに属性を付ける
[GenerateSerializer]
public sealed class CounterState
{
[Id(0)] public int Count { get; set; }
}
public sealed class CounterGrain : Grain, ICounterGrain
{
private readonly IPersistentState<CounterState> _state;
// "counter" は状態名、"default" は使用するストレージプロバイダー名
public CounterGrain(
[PersistentState("counter", "default")] IPersistentState<CounterState> state)
{
_state = state;
}
public async Task<int> IncrementAsync()
{
_state.State.Count++;
await _state.WriteStateAsync(); // この時点で永続ストアへ反映
return _state.State.Count;
}
public Task<int> GetCountAsync() => Task.FromResult(_state.State.Count);
}
永続状態の保存先はストレージプロバイダーとして差し替えられます。開発中はメモリ上のプロバイダーで手軽に動かし、本番では Azure Table Storage や Cosmos DB、あるいは PostgreSQL や SQL Server といった選択肢へ切り替えるという運用が一般的です。グレインのコードはプロバイダー名を参照するだけで、保存先の詳細に依存しません。
設計上の勘所は、書き込みの頻度と状態の粒度です。毎回の更新で永続化するか、一定間隔でまとめて書き出すかは要件次第で選べますし、一つのグレインに状態を詰め込みすぎないよう、業務上の実体に沿った適切な単位でグレインを分けることが、後々のスケールと保守のしやすさにつながります。状態管理と永続化の踏み込んだ手法は次回で扱います。
典型的なユースケース
Orleans が力を発揮するのは、多数の独立した実体がそれぞれ状態を持ち、頻繁に更新されるようなワークロードです。以下のような領域が代表的です。
- リアルタイム性の高いゲーム — プレイヤーやセッションをグレインにして、位置やスコアをメモリ上で即応的に更新する
- IoT のデバイス管理 — デバイス一台ごとをグレインとして、テレメトリの受信や状態の集約、コマンド送信を担わせる
- 金融・取引系 — 口座やポートフォリオをグレインにして、整合性を保ちながら残高やポジションを更新する
- リアルタイム集計・ランキング — カウンターや集計単位をグレインに割り当て、大量のイベントをメモリ上で加算する
いずれも「対象が多数あり」「それぞれが状態を持ち」「更新が頻繁」という特徴を共有しています。グレインという細かい単位に処理と状態を割り当てられることが、これらのワークロードと素直に噛み合います。次に示すのは、こうした用途で最初に置くことになるグレインインターフェースの一例です。
// IoT デバイス一台を表すグレイン
public interface IDeviceGrain : IGrainWithStringKey
{
Task ReportTelemetryAsync(TelemetryReading reading);
Task<DeviceStatus> GetStatusAsync();
Task SendCommandAsync(DeviceCommand command);
}
// ゲームセッションを表すグレイン
public interface IGameSessionGrain : IGrainWithGuidKey
{
Task JoinAsync(string playerId);
Task LeaveAsync(string playerId);
Task<GameSnapshot> GetSnapshotAsync();
}
最小構成のセットアップと .NET 10 / ASP.NET Core との統合
Orleans を試す最短の経路は、ASP.NET Core のアプリケーションにサイロを同居させる構成です。かつては Web API プロジェクトとサイロプロジェクトを分け、クライアント経由でグレインを呼ぶ形が一般的でしたが、現在の Orleans は Web ホストと同じプロセス内でサイロを動かす同居(co-hosting)に対応しており、小さく始めるにはこの形が扱いやすくなっています。まず必要なパッケージを追加します。
# .NET 10 の Web プロジェクトを作成
dotnet new web -n OrleansDemo
cd OrleansDemo
# Orleans のサーバー側パッケージを追加
dotnet add package Microsoft.Orleans.Server
次に Program.cs でサイロを構成します。ローカル開発向けのクラスタリングとメモリストレージを指定すれば、それだけで単一プロセスの Orleans が立ち上がります。同じ WebApplication 上で通常どおり HTTP のエンドポイントも定義できます。
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.UseOrleans(silo =>
{
silo.UseLocalhostClustering(); // ローカル単一サイロの構成
silo.AddMemoryGrainStorage("default"); // 開発用のメモリストレージ
});
var app = builder.Build();
// Minimal API から同じプロセス内のグレインを呼ぶ
app.MapPost("/counters/{id}/increment", async (string id, IGrainFactory grains) =>
{
var counter = grains.GetGrain<ICounterGrain>(id);
var count = await counter.IncrementAsync();
return Results.Ok(new { id, count });
});
app.MapGet("/counters/{id}", async (string id, IGrainFactory grains) =>
{
var counter = grains.GetGrain<ICounterGrain>(id);
return Results.Ok(new { id, count = await counter.GetCountAsync() });
});
app.Run();
ここでは IGrainFactory をエンドポイントのハンドラーに直接注入し、識別子を渡してグレインを呼び出しています。同居構成では別途クライアントを立てる必要がなく、依存性注入から取り出したファクトリーがそのままグレインへの入口になります。ICounterGrain と CounterGrain は先の節で定義したものをそのまま使えます。
この状態でアプリケーションを起動し、/counters/pricing/increment へ POST すれば、識別子 pricing のグレインが活性化されてカウントが増え、応答が返ります。データベースを用意しなくても、メモリストレージのおかげでグレインの状態は活性化中は保持されます。ここから本番へ向かう際は、クラスタリングの構成を実運用向けに変え、ストレージプロバイダーを永続的なものに差し替え、サイロを複数に増やしていくという順序で拡張できます。アプリケーション側のグレインのコードはその過程でほとんど変わりません。
まとめ
第1回では、.NET Orleans の全体像を概観しました。Orleans はバーチャルアクターモデルによって、分散システムに付きまとう配置や生存管理、並行制御の複雑さをランタイム側へ引き受けさせ、開発者が業務ロジックの記述に集中できるようにするフレームワークです。状態を持つ実体をグレインとして表し、サイロのクラスター上で位置透過に呼び出せること、グレイン単位のシングルスレッド実行によってロックなしで整合性を保てること、そしてサイロの追加で水平にスケールし障害時には状態から復元されることが、高性能で高スケールなワークロードに向く理由でした。ASP.NET Core と同居させる最小構成なら、少ないコードで実際に動かして手応えを確かめられます。
次回は、状態管理と永続化を主題に、ストレージプロバイダーの選択、書き込み戦略、障害時のリカバリーといった、ステートフルなサービスを信頼性高く運用するための具体的な手法へ踏み込みます。
エンハンスド株式会社では、Orleans をはじめとする分散アーキテクチャを用いた高性能システムの設計・開発を支援しています。リアルタイム処理や大規模なスケールが求められるサービスについて、技術選定の妥当性の検証から、既存システムのモダナイゼーション、本番運用を見据えた設計・実装までを、実務に即した形でご一緒します。分散システムの構築を具体的に進めたい方は、お気軽にご相談ください。
このテーマの全体像は .NET モダナイゼーション完全ガイド にまとめています。
この記事をシェア
この記事に関連する支援サービス
記事で扱っている領域について、支援内容と事例をまとめたページがあります。
関連記事

.NET Orleans分散アーキテクチャ徹底解説:アクターモデルが実現する次世代分散システムの理論と実装
分散システムを真面目に組もうとすると、スケール・可用性・整合性のどれを取るかという古い悩みに必ずぶつかる。Microsoft の .NET Orleans は、…

【.NET Orleans入門】第2回 ステート管理と永続化 - 信頼性の高いステートフルサービスの構築
第1回では、.NET Orleans の全体像として、バーチャルアクターモデル、グレインとサイロという中心概念、そして高スケールなワークロードに向く理由を整理しました。…

【.NET Orleans入門】第6回 実践ケーススタディ - 注文と在庫のリアルタイム処理システム
「.NET Orleans入門」も今回で第6回、シリーズの最終回を迎えます。これまで、バーチャルアクターモデルの基礎(第1回)、状態管理と永続化(第2回)、グレイン間通信とストリーミング(第3回)、…
