個人的なメモ

Tomohiro Suzuki @hiro128_777 のブログです。Microsoft MVP for Developer Technologies 2017- 本ブログと所属組織の公式見解は関係ございません。

.NET 7 の注目機能となるか? Orleans

Orleans に関しては、さらに詳細な仕様や、本記事のローカル実行ではなく、Azure App Service にデプロイしてみた結果について以下の記事にまとめました。
 
hiro128.hatenablog.jp
 
 

.NET 7 の動向と Orleans

.NET 7 の新しいトピックとして Orleans がバージョン4となって.NET 7と共にリリースされることが発表されています。Orleans は日本ではこれまであまり注目されていなかったフレームワークですが、2010年頃から開発は続いており現時点でバージョン3.6がリリースされている息の長いフレームワークです。
 
devblogs.microsoft.com
 
上記ブログにて、「ASP.NET Core チームと Orleans チームは、Orleans 分散プログラミング モデルを ASP.NET Core とさらに調整および統合する方法を調査しています。Orleans 4 は .NET 7 と共に出荷されます。」と発表されており、docs.microsoft.com で Orleans のドキュメントが公開されたことからも .NET 7 以降 Orleans を積極的に推していく意思が感じ取れます。
 
docs.microsoft.com
 
そこで今回は、 .NET 7 における注目機能の一つになるかもしれない Orleans をご紹介します。
 
 

Orleans とは

公式ドキュメントの説明では「Orleans は、堅牢でスケーラブルな分散アプリケーションを構築するためのクロスプラットフォームフレームワークです。」とありますが、この説明では具体的に Orleans を使用することでどんなメリットがあるのかよくわかりません。

最も単純な例として、モノリシックのアプリケーションをクラウドにデプロイしスケールアウトするシナリオを考えてみましょう。何らかの工夫をしない限りアプリケーションの複数の実行単位(たとえば、App service のインスタンスや k8s の Pod)間はそれぞれお互いの実行内容を知ることはできないため、キャッシュの共有やステートの管理の考慮、HTTP リクエストごとにデータベースへのアクセスが発生することによるデータベースへの負荷、データ更新処理の競合などの問題が発生します。

このような状態を回避するために、ステート管理、処理のキューイング、共有キャッシュの制御やデータ更新処理の整合性制御などの機能が必要となります。

これらの機能はモノリシックのアプリケーションのスケール時に限らず分散アプリケーションでは必須ですが、自前で実装するのは車輪の再発明になってしまいます。また、設計やテストに非常に手間がかかり、速度や品質を担保するのも困難です。このような厄介な問題の面倒を見てくれるフレームワークが Orleans です。

Orleans を利用することによって、シングルサーバーアプリケーションの開発経験しかない開発者でも、比較的容易に回復性とスケーラビリティを備えたクラウドネイティブアプリケーションや分散アプリケーションの開発に移行できることが最大のメリットとなります。
 
 

グレインとは

グレインは「粒」という意味を持つ Orleans アプリケーションの基本的な構成要素で、状態データを持つコンピューテーションの単位です。 グレインは他のグレインとメッセージを送受信したり、クライアントからの要求に対するレスポンスを返します。

グレインはユーザー定義のID(identity)、動作(behavior)、および状態(state)で構成され、プライベートまたは共有の揮発的または永続的な状態データを保持可能で、様々なストレージシステムに格納することができます。

グレインによってアプリケーションの状態を暗黙的に分割し自動的な拡張性を実現するとともに、障害からの復旧が容易になります。グレインの状態は、グレインがアクティブな間はインメモリに保持されるため、レイテンシーが低く保たれデータストアの負荷も低くなります。
 
 

Orleans のメリット

Orleans を利用すると開発者が意識することなく分散アプリケーションに必要な機能上のメリットを享受できるため、開発者の専門性の高低を問わず生産性が向上します。こちらも公式ドキュメントの説明は抽象的でわかりにくいため、主なメリットについてもう少しわかりやすく以下でご説明します。
 

グレインのシングルスレッド実行の保証

グレインのシングルスレッド実行が保証されるため、開発者はグレインレベルでの同時実行に直面することがなく、共有データへのアクセスを制御するためにロックや他の同期メカニズムを使用する必要がなくなり、分散プリケーション開発時の考慮事項が少なくなります。
 

グレインのアクティブ化と非アクティブ化について開発者は意識不要

アプリケーションは、論理的に作成されたグレインの「メモリ空間」全体に、それらが特定のタイミングで物理メモリ上に存在するかどうかにかかわらず中断することなくアクセスすることができます。個々のグレインは Orleans のランタイムによって必要に応じてページイン・ページアウトされるため、グレインの物理メモリへのロード、エビクション、活性化、非活性化といったライフサイクルを開発者がアプリケーションコードによって管理する必要はなく、Orleans のランタイムによってハードウェアリソースに応じて動的にグレインのロードバランシングが最適化されます。
 

グレインの物理的な場所に関して開発者は意識不要

グレインの参照(プロキシオブジェクト)は、グレインの論理的なアイデンティティのみを含んでいます。アプリケーションコードはグレインの物理的な位置を意識することなくグレインと通信できます。グレインの物理的な位置は、障害やリソース管理、または呼び出された時点で当該のグレインが非アクティブになっているなどの理由で、時間の経過とともに変化する可能性がありますが、それを開発者がアプリケーションコードによって管理する必要はなく、意識する必要もありません。
 

永続的なストアとの統合について開発者は意識不要

Orleans は、Grainのインメモリ状態を宣言的に永続ストアにマッピングすることが可能です。また、開発者が意識することなく呼び出し側が永続的な状態が正常に更新された後にのみ結果を受け取ることが保証されます。永続ストアの選択肢の自由度は高くカスタマイズも可能です。
 

エラーの自動伝播

Orleans では、開発者はエラー処理ロジックを適切な場所に配置すればよく、処理されなかったエラーはランタイムによって自動的にコールチェーン上に伝播されます。エラーがアプリケーション内で握りつぶされることはなく、各レイヤーでエラーを手動で伝播させるという面倒な作業を行う必要はありません。
 

開発者が意識することなくスケーラビリティと高パフォーマンスを実現できるプログラミングモデル

Orleansのプログラミングモデルは実績のあるベストプラクティスとパターンを取り入れているため、アプリケーションやサービスを拡張する際に、開発者が自然にコードを記述すれば適切なコードになるように設計されています。
 
 

Orleans の HelloWorld のサンプルコード

それでは、Orleans の HelloWorld サンプルを見ていきましょう。記事の都合上さわりの説明だけに留まりますが、公式の HelloWorld サンプルに手を加え、Web API のエンドポイントを追加しグレインの活性化、非活性化を操作し可視化できるように変更しています。
 
なお、このサンプルコードは説明のために調整されたものですので、ベストプラクティスではありませんのでご注意ください。
 

Web APIのエンドポイント

Web API のエンドポイントのリファレンスは以下の通りです。

エンドポイント メソッド レスポンス
/ Get "Orleans is running." 文字列を固定で返却。この時 Grain が非活性であれば自動で活性化される。
/datetime Get 現在の日時を返却。この時 Grain が非活性であれば自動で活性化される。
/deactivate Get "HelloGrain is deactivated." を固定で返却する。その後 Grain はアイドル状態になると即時非活性となる。

 
 

ソリューションの構成

ソリューションの構成は以下の通りです。Grain の活性化、非活性化が確認できる最低限の実装です。

 
 

コード

それぞれのコードと意図は以下の通りです。
 

Program.cs
  • Orleans ホスト設定・起動して待機状態にする
  • Web API を起動して待機状態にする
using System;
using HelloWorld;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Orleans;
using Orleans.Hosting;

// Orleans ホスト設定・起動
using var orleansHost = new HostBuilder()
    .UseOrleans(builder => builder.UseLocalhostClustering())
    .Build();
await orleansHost.StartAsync();

// グレインファクトリー取得
var grainFactory = orleansHost.Services.GetRequiredService<IGrainFactory>();

// HelloGrain の参照を"friend"キーで取得
var friend = grainFactory.GetGrain<IHelloGrain>("friend");

// Web API 起動
var app = WebApplication.Create();
app.MapGet("/", async () => await friend.SayHello("Orleans is running."));
app.MapGet("/datetime", async () => await friend.SayHello($"{DateTime.Now}"));
app.MapGet("/deactivate", async () => await friend.SayHelloDeactivateOnIdle("HelloGrain is deactivated."));
await app.RunAsync();

// Orleans ホスト終了
await orleansHost.StopAsync();

 

IHelloGrain.cs
  • 文字列キーを持つ Grain のインターフェース
  • メソッドを定義する
using System.Threading.Tasks;
using Orleans;

namespace HelloWorld
{
    public interface IHelloGrain : IGrainWithStringKey
    {
        Task<string> SayHello(string greeting);
        Task<string> SayHelloDeactivateOnIdle(string greeting);
    }
}

 

HelloGrain.cs
  • Grain の実装クラス
  • 活性化、非活性化のステータス遷移時にその旨をコンソールに出力する
using System;
using System.Threading.Tasks;
using Orleans;

namespace HelloWorld
{
    public class HelloGrain : Grain, IHelloGrain
    {
        public Task<string> SayHello(string greeting)
        {
            return Task.FromResult($"Hello, {greeting}");
        }

        public Task<string> SayHelloDeactivateOnIdle(string greeting)
        {
            DeactivateOnIdle();
            return SayHello(greeting);
        }

        public override Task OnActivateAsync()
        {
            Console.WriteLine($"{DateTime.Now} Grain status : HelloGrain({IdentityString}) is activating...");
            return base.OnActivateAsync();
        }

        public override Task OnDeactivateAsync()
        {
            Console.WriteLine($"{DateTime.Now} Grain status : HelloGrain({IdentityString}) is deactivating...");
            return base.OnDeactivateAsync();
        }
    }
}

 
 

動作の説明

では、このコードを実際に実行して以下の4つのステップの基本的な動作について確認していきましょう。

  1. アプリケーション起動
  2. API の動作確認
  3. Grain を非活性に遷移させる
  4. アクセス検知により Grain を再度活性に遷移させる

 

1. アプリケーション起動

サンプルアプリケーションを起動すると、Grain が活性化状態になります。
 
コンソールの出力

2022/08/21 17:55:46 Grain status : HelloGrain(*grn/HelloWorld.HelloGrain/0+friend-0x6EDED8DA) is activating...

https://localhost:5001/ にアクセスすると、固定文字列のレスポンスが返却されます。

 

2. API の動作確認

https://localhost:5001/datetime にアクセスすると、現在の日時のレスポンスが返却されます。

 

3. Grain を非活性に遷移させる

https://localhost:5001/deactivate にアクセスすると、固定文字列のレスポンスが返却されます。

 
レスポンス返却後アイドル状態となるため Grain が非活性に状態遷移します。
 
コンソールの出力

2022/08/21 17:56:10 Grain status : HelloGrain(*grn/HelloWorld.HelloGrain/0+friend-0x6EDED8DA) is deactivating...

 

4. アクセス検知により Grain を再度活性に遷移させる

再度、https://localhost:5001/datetime にアクセスすると、まず Grain が非活性から活性に状態遷移します。
 
コンソールの出力

2022/08/21 17:56:26 Grain status : HelloGrain(*grn/HelloWorld.HelloGrain/0+friend-0x6EDED8DA) is activating…

Grain が活性化されたのち、現在の日時のレスポンスが返却されます。

 
 
以上の通り、開発者が Grain の状態遷移を意識することなく単純にロジックを実装するだけで、Orleans のランタイムによって状態遷移が管理されていることがわかります。
 
 

まとめ

Orleans の最も基本的な動作を見ていきましたが、とても興味深いフレームワークであることがわかります。今回のサンプルはローカルで動作する HelloWorld レベルですが、公式ドキュメントには Azure の App Service にショッピング カートアプリをデプロイするようなさらに実践的な情報もありますので興味を持たれた方はぜひそちらもご覧ください。
 
docs.microsoft.com


繰り返しになりますが、Orleans 4 は .NET 7 と共に出荷され、今後 Orleans 分散プログラミング モデルは ASP.NET Core に統合されていくとのことですので、11月の .NET 7 のリリースが楽しみです。11月8日から10日(現地時間)には、ローンチイベント「.NET Conf 2022」も開催予定ですので、今後の情報公開が楽しみです。