はじめに
分散システムを真面目に組もうとすると、スケール・可用性・整合性のどれを取るかという古い悩みに必ずぶつかる。Microsoft の .NET Orleans は、仮想アクターモデルという発想でこの悩みを開発者の視界から外しにかかったフレームワークだ。本記事では Orleans が「分散の難しさ」をどう隠し、その下で何が動いているのかを、設計判断のレベルまで降りて整理していく。
仮想アクターモデルが変えたこと
Erlang や Akka に代表される従来のアクターモデルでは、アクターを生成し、寿命を管理し、停止させる責務は基本的にアプリケーション側にあった。Orleans はここを反転させている。アクター(Orleans では Grain と呼ぶ)はキーを指定した瞬間に「存在しているもの」として扱われ、ランタイムが必要なタイミングでメモリ上に活性化し、使われなくなれば静かに退避する。
この一見ささやかな違いが、開発体験を大きく変える。クラスタのどのノードに Grain が置かれているか、生きているか、誰が起こすべきか。こうした分散特有の悩みがアプリケーションコードから消える。論理的な ID と、それを実装する物理ノードが切り離された結果、Single System Image に近い感覚でロジックを書けるようになる。
位置決定には改良版の Consistent Hashing が使われている。ノードが増減しても再配置が局所的に済むよう設計されており、各サイロは複数の仮想ノードを担当して負荷を統計的に均す。
一貫性はどこで担保しているか
Orleans の中でいちばん効いている設計判断は、Grain 内部をシングルスレッドで回すという割り切りだろう。Turn-based Concurrency と呼ばれるこの仕組みでは、ひとつの Grain は同時に一通のメッセージしか処理しない。状態の競合は構造的に発生しえず、ロックも mutex もアプリ側で書く必要がない。
シングルスレッド処理と聞くとスループットが心配になるが、Grain そのものが論理単位として大量に並ぶため、結局はクラスタ全体で十分な並列度が得られる。さらに各 Grain 内部は async/await をフルサポートしており、I/O 待ちの間にスレッドを返すので、リソース消費も抑えやすい。
2019 年に加わった分散トランザクション機能では、改良版 Two-Phase Commit と楽観的並行制御を組み合わせ、必要な箇所だけ ACID を取り戻せるようになっている。本来 Saga パターンで補償するしかなかった処理にも選択肢が広がった。
クラスタリングと障害検出の実装
クラスタ内のサイロ(プロセス)同士は、SWIM プロトコルをベースとしたゴシップ型のメンバーシップで状態を共有している。pinger と pingee の数を抑えながら O(log N) のオーダーで全体に伝播する古典的な手法だが、Orleans 流の改良として誤検出率を抑えるための間接 ping や、Azure Table Storage / DynamoDB / ZooKeeper など外部の信頼できる保管庫にメンバーシップを永続化する仕組みが組み込まれている。
サイロが落ちると、対象 Grain は次に呼ばれたタイミングで別のサイロで再活性化される。ハッシュリングが局所的に再構成されるため、関係のない Grain には影響しない。本番運用で「ローリングアップデート中も外から見ると無傷」と言える理由はここにある。
ストリーミング
Orleans Streams も「仮想化」の発想を引き継いでいる。ストリームの実体がどこにあろうと、サブスクライバはストリーム ID を指して購読すればよい。バックプレッシャーはランタイムが透過的に制御し、配信保証は At-most-once / At-least-once / Exactly-once(トランザクション併用時)から選べる。
時間窓処理やパターンマッチングといった CEP に近い処理も、Stream と Grain の組み合わせで素直に書ける。フレームワーク側に専用の DSL があるわけではなく、Grain の中で普通に async/await を回すだけだ。逆に言えば、Stream の中身を整える責務は呼び出し側が持つので、そこは流量や順序保証を最初に決めておいたほうがよい。
パフォーマンスの肌感
同一データセンター内であれば、Grain 呼び出しのラウンドトリップは大半が 1〜5ms に収まる。シリアライゼーションとネットワーク RTT が支配項なので、重い CPU バウンドな処理は素直に別のワーカーへ逃がす設計が現実的だ。
単一 Grain あたりのスループットは中身にもよるが、1万から5万メッセージ毎秒のレンジを期待してよい。クラスタ全体では Grain の独立性が効いて、ほぼ線形にスケールする。最適化を狙うなら、メッセージの自動バッチングと Co-location ヒントの 2 つはまず触ってよいスイッチだ。
設計の勘所と地雷
Orleans の設計は DDD と相性がよい。Grain を集約ルートに見立て、状態の一貫性境界をそのまま分散境界に持ち込めば、自然な構造になる。Event Sourcing や CQRS との組み合わせもストレートに書ける。
一方で、避けたいパターンもいくつかある。Grain を細かく切りすぎる(Chatty Grain)と、メッセージのオーバーヘッドで性能が落ちる。逆に責務を 1 つの Grain に集中させる(God Grain)と並列度を活かせない。そして同期的な呼び出しを連鎖させる構造(Synchronous Chain)はデッドロックと遅延を呼び込むので、設計レビューで真っ先に潰すべき対象になる。
まとめ
Orleans は分散システム研究の蓄積を、アプリケーション開発者が触れる API のすぐ下まで降ろしてくれるフレームワークだ。仮想アクターという抽象により、生死と配置という分散特有の苦労を視界から外し、Grain という単位で並列性と一貫性を同時に成立させる。マイクロサービス的な構成にも、リアルタイム処理にも、エッジ寄りのワークロードにも噛み合いやすい。実プロダクションで叩いた感覚としても、設計判断さえ間違えなければかなり長距離を走れる選択肢だと言える。
エンハンスドでは Orleans を使った大規模分散システムの設計・開発を支援しています。理論面の整理から本番運用のチューニングまで、まずはお気軽にご相談ください。
このテーマの全体像は .NET モダナイゼーション完全ガイド にまとめています。
この記事をシェア
関連記事

【.NET Orleans入門】第1回 アクターモデルで実現する高性能Webアプリケーション
多数の利用者が同時にアクセスするサービスを支えるうえで、状態を持ったまま高いスループットをさばく仕組みは避けて通れない課題になります。リアルタイム性の高いゲーム、金融取引、IoT のデバイス管理、…

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

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