8.5K Views
February 19, 23
スライド概要
Orleans の概要と基本構成のデプロイまでについての調査結果です。
SIerの人。Xamarin.iOS が好き。Azure と戯れるのが仕事。Visual Studio for Mac が Win版と同等になる日を切望。Microsoft MVP Developer Technologies 2017-
Orleans の概要と 基本構成のデプロイまで Tomohiro Suzuki @hiro128_777 2023-2-24 C# Tokyo
自己紹介 鈴木友宏 M365、Azure、Power Platform、.NET などの Microsoft 製品を用いたシステムの構築をしています。 Twitter:@hiro128_777 blog:https://hiro128.hatenablog.jp/ 2
.NET 7 がリリースされましたが… なにやら見慣れないものがありました(私が不勉強なだけかもですが) Orleans is 何? 3
Orleans(オーリンズ) とは とりあえず、Microsoft learn の公式ドキュメントを一通り見てみるが、 いつも通りよくわかりません… • • • • • Orleans は何者なのか Orleans がどういう動機で開発されたのか Orleans を採用するとどんなことが嬉しいのか アプリの基本構成とデプロイについて のようなことがよくわかりにくい… 本日のゴール これらを理解して ・learn のドキュメントを読む ・サンプルアプリをデプロイしてみる ための下地を作る 4
結局 Orleans is 何? Orleans は Microsoft による「仮想アクターモデルの実装」です。 アクターモデルとは、1973年にカール・ヒューイットらによって提唱された、「近い将来、数 百・数千のマイクロプロセッサから構成され、個々にローカルメモリを持ち、高性能通信 ネットワークで通信を行う並列コンピュータが登場するとの予測」から開発された、ロック ベースの同期の必要性を取り除き並行計算を行うための方法論です。 ※ロックこそが並行計算の一番の妨げであるため、それをなくしたいという動機 アクターモデルと仮想アクターモデルの主な違いは、後者はアクターの物理的なインスタン ス化を完全に抽象化し、それをランタイムで管理できるようにしたものです。 仮想アクターは、仮想的に常に存在する論理エンティティであり、明示的に作成したり破 棄したりすることはできません。また、その仮想的な存在は、それを実行するサーバーの障 害に影響されません。 アクターは常に存在し続け、常にアドレス指定が可能です。 5
Orleans 何がうれしいの すごく、簡単に言えば、 「分散アプリケーションを開発するときに発生するいろいろな面倒な ことについて自前で実装することなく、いい感じで面倒を見てくれる ので、メリットだけを享受することができます!」ということでした。 ちなみに、Teams や HALO のバックエンドでも利用されています。 6
Orleans の動機 7
古典的なWebアプリの問題点 www • 読み取り要求のたびにデータベースにアクセ スするため、 データベースの負荷が大きい Load Balancer App App App • アプリのインスタンスの状態をお互いが把握 できず、データベースへの書き込み競合が発 生する • 各々のアプリのインスタンスが状態を個別に 持ちリソースの効率が悪い。 DB 8
対策としてキャッシュ、キューなどを追加するも新たな問題が • 読み取り要求のデータベース負荷対策とし てキャッシュを追加 →キャッシュの一貫性の問題が発生 www Load Balancer App Cache App Queue • データベースへの書き込み競合対策として キューを追加 →非同期書き込みの待ちが発生 App DB 問題は軽減しているが、インフラを追加して厄介 ごとをオフロードしているだけなので根本的な解決 ではない… スケーラビリティも確保できていない 9
Orleans のコンセプト(根本解決のためのアプローチ) • アプリインスタンスがお互いに会話し互いに 連携することで分散アプリの問題を解決す る www Load Balancer App • データベースを唯一の「真実のソース」とし、 キャッシュやキューなどの余計なインフラ自体 をなくす App App Cache DB (True Source) Queue • 高スループット • 低レイテンシー • 高スケーラビリティ が確保できるはず 10
結局 Orleans とは www Orleans Load Balancer Orleans は、アプリケーションのフロントエンド と永 続化ストレージ(DBなど)の間のステートフルでス マートな中間層を提供するフレームワークです。 メリット ・DB への書き込み競合の考慮が不要 App App App DB ・キャッシュ不要 ・Appインスタンス間の一貫性考慮が不要 ・Appインスタンスを追加すれば自動でクラスターが構成される 分散アプリケーションを開発するときに発生するいろいろ な面倒なことについて、いい感じで面倒を見てくれるので、 メリットだけを享受することができます。Teams など超大 規模サービスでの実績もある安定したフレームワークです 11
Orleans の構成と機能 12
Orleans の 概念 REST APIが定義されたエンドポイント。REST API と Orleans 独自の世界を相互変換する 境界 HTTP リクエストを適切なグレインにルーティン グする HTTP Response HTTP Request REST API www Grain Call Orleans Cluster Silo クラスターを1つの巨大なコンピューターのよう に扱う Silo Glain Glain Glain Glain Glain Glain Data Result Glain Glain Glain Glain Data Query Orleans 独自の世界 Silo Grain Response Front End Glain Glain スケールアウトやアップグレードによるインスタ ンス追加や、障害時のインスタンスの突然 停止に対応するため、サイロは、Container Apps、App Service、マネージド k8s などの SaaS にデプロイする DB (Persistent Storage) 13
Orleans の 重要なプリミティブ(構成要素) HTTP Response HTTP Request REST API Orleans の全体像を把握するために、重要な構成 要素は以下の3つです。 Grain Call Silo プリミティブ Glain Glain Glain Glain Glain Glain Data Result Glain Glain Glain 概要 Frontend フロントエンド Orleans の世界と外部とのゲートウェイ HTTP Request/Response と Grain Call/Response の変換を行います。 Grain グレイン 仮想アクターの役割を果たし、サービスの構成要 素となり、ビジネスロジックをメソッドとして提供し ます。 クラスターでホストされているグレインは、1 つのプロ セス内にあるかのように相互に通信できます。 Silo サイロ グレインをホストするインスタンス クラスターとして実行され、サイロ同士は互いに連 携して作業を分散し、失敗を検出して復旧でき ます。 Silo Glain Data Query Orleans 独自の世界 Silo Grain Response Front End Glain Glain 14
フロントエンドの役割 HTTP Response HTTP Request REST API www Silo Grain Call Orleans 独自の世界 Orleans Cluster Grain Response Front End Glain Glain 公開されている REST API への HTTP リクエストを受信して、リクエ ストを処理すべき Grain に対してメソッドをコールし、 Grain からレ スポンスを受け取り、 HTTP リクエストにレスポンスを返します。 Glain Glain Glain Glain Silo フロントエンドは、www と Orleans の世界のゲートウェイの役割を 果たします。 ( REST API と Orleans 独自の世界を相互変換する境界) Glain Glain なお、 フロントエンドは Orleans クラスターから見ればクライアントで もあります。 15
フロントエンドの実装例 Minimum なフロントエンドの実装としては、ASP.NET Core Minimal Web API のプロジェクト で REST API を定義し、Generic Host で Orleans クラアントを起動し、Grain プロキシオブジェ クトのメソッドをコールし、戻り値を返します。 var host = await StartClientAsync(); // Host の構築は次ページを参照 var client = host.Services.GetRequiredService<IClusterClient>(); var writeLargeData = client.GetGrain<IWriteLargeData>(Guid.NewGuid()); var app = WebApplication.Create(); app.MapGet("/writelargedata", async () => await writeLargeData.WriteLargeData()); await app.RunAsync(); 16
フロントエンドの実装
Host の構築
static async Task<IHost> StartClientAsync()
{
var connectionString = Environment.GetEnvironmentVariable("ORLEANS_AZURE_STORAGE_CONNECTION_STRING");
var builder = new HostBuilder()
.UseOrleansClient(client =>
{
client
.UseAzureStorageClustering(options => options.ConfigureTableServiceClient(connectionString))
.Configure<ClusterOptions>(options =>
{
options.ClusterId = "PoCCluster";
options.ServiceId = "OrleansPoC";
});
})
.ConfigureLogging(logging => logging.AddConsole());
var host = builder.Build();
await host.StartAsync();
return host;
}
17
グレインの役割 HTTP Response HTTP Request REST API www Silo Grain Call Orleans 独自の世界 Orleans Cluster Grain Response Front End Glain Glain Glain var host = await StartClientAsync(); var writeLargeData = client.GetGrain<IWriteLargeData>(Guid.NewGuid()); グレインのプロキシオブジェクト Silo Glain Glain Glain Glain Glain グレインはユーザー定義のID(identity)、業務ロジック (behavior)、およびインメモリの状態(state)をカプセル化し、他 のグレインとメッセージを送受信したり、クライアントからの要求に対す るレスポンスを返します。 グレインの種類 GrainID var app = WebApplication.Create(); app.MapGet("/writelargedata", async () => await writeLargeData.WriteLargeData()); await app.RunAsync(); グレインの物理的な場所に関して開発者は意識不要です。 同一インターフェースで指定したグレインのプロキシオブジェクトを同一 の GrainID で取得すると、どのフロントエンド(クライアント)から呼 び出したとしても同一のグレインの実体が呼び出されます。 18
グレインの実装例
グレインは、インターフェースと実装のペアで構成されます。
インターフェース
実装(抜粋)
public interface IHello : Orleans.IGrainWithGuidKey
{
Task<string> Call();
}
グレインの ID の種類
public class HelloGrain : Orleans.Grain, IHello
{
private readonly ILogger _logger;
public HelloGrain(ILogger<HelloGrain> logger)
{
_logger = logger;
}
Task<string> IHello.Call()
{
_logger.LogInformation($"\n{DateTime.Now} Call recieved.");
return Task.FromResult($"\n{DateTime.Now} Call recieved.");
}
19
グレインのその他の特長 • グレインはシングルスレッド実行が保証され、ロックや一貫性の問題の考慮が不要です。 • 個々のグレインは Orleans のランタイムによって、開発者は意識することなく、必要に応じて ページイン(アクティブ化)・ページアウト(非アクティブ化)されるため、メモリの使用量やグ レインのロードバランシングは動的に最適化されます。 • GrainID によって呼び出されるグレインの参照(プロキシオブジェクト)は、グレインの論理的 なアイデンティティのみを含んでいます。アプリケーションコードはグレインの物理的な位置を意 識することなくグレインと通信できます。グレインの物理的な位置は、Orleans のランタイムに よって、障害やリソース管理、または呼び出された時点での当該のグレインが非アクティブに なっているなどの理由で、時間の経過とともに変化する可能性がありますが、それを開発者 がアプリケーションコードによって管理する必要はなく、意識する必要もありません。 20
サイロの役割 HTTP Response HTTP Request REST API www • サイロは複数の仮想アクターつまりグレインをホストするコンテナです。サ イロは単一ではその効力を発揮できません。複数のサイロによって構成 されるグループを一緒に実行しクラスターを形成することでスケーラビリ ティや耐障害性を提供する役割を果たします。 Silo Grain Call Orleans 独自の世界 Orleans Cluster Grain Response Front End Glain Glain Glain Glain Glain Glain Silo • サイロのクラスターにより、アプリケーションの状態は物理的に暗黙的・ 透過的に分割され、負荷に応じて並列に処理が実行されますが、開 発者はそれを意識することはなく Orleans を利用するだけでスケーラビ リティを確保することができます。 Glain Glain • サイロのクラスターにより、分割されたアプリケーションの状態のバック アップを使用することで、回復力が向上し、障害からの回復性が高ま ります。 21
サイロの実装例
サイロの実装では以下を行います。
• IP、ポート番号などネットワーク関連の設定
• クラスタの設定、ロギングの設定
var siloHost = await StartSiloAsync();
// Silo 動作確認用
var app = WebApplication.Create();
app.MapGet("/", () => $"Silo is activated {DateTime.Now}");
await app.RunAsync();
static async Task<IHost> StartSiloAsync()
{
var builder = Host.CreateDefaultBuilder()
.UseOrleans((context, builder) =>
{
var siloIPAddress = IPAddress.Parse(context.Configuration["WEBSITE_PRIVATE_IP"] ?? "");
// WEBSITE_PRIVATE_PORTS は再起動で変更されるのでWEBSITE_PRIVATE_PORTS として取得し Parse すること
var strPorts = (context.Configuration["WEBSITE_PRIVATE_PORTS"] ?? "").Split(',’);
if (strPorts.Length < 2)
throw new Exception("Insufficient private ports configured.");
var (siloPort, gatewayPort) = (int.Parse(strPorts[0]), int.Parse(strPorts[1]));
var connectionString = context.Configuration["ORLEANS_AZURE_STORAGE_CONNECTION_STRING"];
22
サイロの実装例(続き)
builder
.ConfigureEndpoints(siloIPAddress, siloPort, gatewayPort)
.Configure<ClusterOptions>(
options =>
{
options.ClusterId = "PoCCluster";
options.ServiceId = "OrleansPoC";
})
.UseAzureStorageClustering(options => options.ConfigureTableServiceClient(connectionString))
.AddAzureTableGrainStorage(
"PocStore",
options => options.ConfigureTableServiceClient(connectionString)
)
.ConfigureLogging(logging => logging.AddConsole());
});
var host = builder.Build();
await host.StartAsync();
return host;
}
サイロのアプリケーションコードはデプロイするインスタンスが単一であっても複数(N個)であっ
てもコード自体は同一になります。(スケールアウト、スケールインへの対応)
23
永続化ストレージの役割 例として、サイロメンバーシップの管理とグレインの永続化ストレージとして Azure Table Storage を使用した場合について見てみます。 「orleanspoc」という名前のストレージアカウントを作成し、下記のように接続文字列を指定し てサイロを構成すると初回に自動的にテーブルが作成されます。 グレインの状態の永続化用テーブル OrleansGrainState の指定 サイロメンバーシップの管理用のテーブル OrleansSiloInstances の指定 24
永続化ストレージの詳細(OrleansGrainState) グレインの状態の永続化用テーブル OrleansGrainState のレコードを確認すると以下のようになっています。 App Service にサイロをデプロイした時のレコードを確認して各項目について調べてみました。 列名 値のソース、形式 説明 PartitionKey [ClusterOption.ServiceID]_[グレイン名のプレフィックス]_[連番] 永続化したグレインを一意に特定するキー RowKey PersistentStateAttribute.StateName グレインは複数の状態を持てる。グレインが保持している状態を特定するキー Timestamp タイムスタンプ タイムスタンプ Data Data1 Data2 … シリアライズかつバイナリ化されたグレインが保持している状態の実体 1列に収まらなければ、連番なし、1、2 … N のように複数列に格納される シリアライズかつバイナリ化されているため直接レコードを見ても中身はわからない状態に なっている 規定では Orleans.Storage.JsonGrainStorageSerializer が利用される 参考:https://learn.microsoft.com/ja-jp/dotnet/orleans/host/configurationguide/serialization?pivots=orleans-7-0 25
永続化ストレージの詳細(OrleansSiloInstances ①) サイロメンバーシップの管理用のテーブル OrleansSiloInstances のレコードを確認すると以下のようになっています。(列数が多いため次ページ にまたがります) App Service にサイロをデプロイした時のレコードと以下のソースコードなどを確認して各項目について調べてみました。 https://github.com/dotnet/orleans/blob/4a6ddcdb059f4f113998b4ee4fcb1879a9697900/src/Azure/Orleans.Clustering.AzureStora ge/SiloInstanceTableEntry.cs 列名 値のソース、形式 説明 PartitionKey ClusterOption.ClusterId サイロとクライアントがメンバーシップテーブルでお互いを見つけるためのキー RowKey [ConfigureEndpoints のパラメーター advertisedIP] -[ConfigureEndpoints のパラメーター siloPort] -[Generation] advertisedIP:クラスタリングに使用される IP アドレス(サイロのエンドポイントの IP アドレス) siloPort:サイロ間通信の際に当該のサイロが使用するポート(サイロのエンドポイントのポート) Generation(epoch):サイロインスタンスの世代。2022/1/1 00:00:00 (UTC) を0とした現時刻 (UTC) までの Tick 数 Timestamp タイムスタンプ タイムスタンプ Address EndpointOptions.AdvertisedIPAddress クラスタリングに使用される IP アドレス DeploymentId PartitionKey の値 Orleans 2.0以前は DeploymentId と呼ばれていた名残り Generation 2022/1/1 00:00:00 (UTC) を0とした現時刻 (UTC) までの Tick 数 サイロインスタンスの世代。learn のドキュメントでは「epoch」と呼称されている 参考:https://learn.microsoft.com/ja-jp/dotnet/orleans/implementation/cluster-management#the-basic-membership-protocol HostName サイロの DNS ホスト名 Dns.GetHostName() と同じ結果となる。サイロの起動時に設定される IAmAliveTime DateTimeOffset 当該のサイロが生きていることを最後に報告した日時 Orleans ランタイムによる死活診断やトラブルシューティング時に利用する 26
永続化ストレージ(OrleansSiloInstances ②) 列名 値のソース、形式 説明 InstanceName 古いカラムのため未調査 後方互換性のために残されている古いカラム。 Port EndpointOptions.SiloPort サイロ間通信(silo-to-silo)の際に当該のサイロが使用するポート(サイロのエンドポイントのポート)規定値は11111 ProxyPort EndpointOptions.GatewayPort サイロからクライアント(この文脈では REST API <-> Grain Call のフロントエンドのこと)への TCP ポート。サイロの起動時に設定される。規定値は 30000 RoleName 実行中のアセンブリの名前。 SiloName サイロ ホストが付けたサイロ名。サイロの起動時に設定。 StartTime DateTimeOffset サイロインスタンスのが起動した日時 Status サイロの状態 状態の詳細は右記を参照:https://learn.microsoft.com/ja-jp/dotnet/api/orleans.runtime.silostatus?view=orleans-7.0 メンバーシッププロトコルで管理される。 SuspectingSilos [サイロの IPアドレス]-[サイロの Port]@Generation 当該のサイロが Active ではないことを疑っているサイロのリスト。メンバーシッププロトコルで管理される。 SuspectingTimes DateTimeOffset 当該のが Active ではないことに対する疑いが生じた日時 UpdateZone MembershipVersion Azure Cloud Services では利用されていたが、App Service での利用状況は不明 Int64 TableVersion の場合のみ、現在のメンバーシップ構成の最新バージョン。MembershipEntry の 場合は null 27
その他Orleans のメリット(高スループット、低レイテンシー) • ホットデータもウォームデータもすべてクラスターのどこかのメモリにグレインの状態として保存されている ため、データベースの読み取り回数が減り、データベースの読み込み負荷を軽減することができます。 • よって別途キャッシュを用意する必要がなくなることで、そもそもキャッシュの一貫性の問題が発生せ ず、高スループット、低レイテンシーに寄与します。 • 各グレインが独自の状態を持ち、データベースへの書き込みをグレイン同士て通信し調整のうえ実 行するため、複数の場所から一度に書き込まれるようなホットな行がデータベース内に発生すること はなく、書き込み競合の調整が不要となります。 • 上記によりレイテンシも低く抑えられるため、書き込みの調整に非同期の Queue Worker などを 使用する必要がなく、グレインから直接データベースに対して非常に迅速な書き込みが実行でき高 スループットに寄与します。 28
その他Orleans のメリット(スケーラビリティ) • キャッシュや非同期の Queue Worker が排除され、データベースの読み込み、書き込み負 荷を最小限に保たれるため、アプリケーション・インスタンスのリソースに余裕があり、スケーラビ リティの向上に寄与します。 • グレイン同士が分割して状態を保持しているため、保持している状態の重複が発生せず、ア プリケーション・インスタンス全体で見てのメモリの利用効率が向上し、スケーラビリティの向上 に寄与します。 29
Orleans を App Service にデプロイする 3
App Service へのデプロイ(構成図) 役割 リソース パブリック アクセスの受信 プライベート アクセスの受信 プライベート エンドポイント リージョン VNet 統合 WebSocket フロントエンド App Service ○ X X ○ ○(ポート数:2) サイロ01 App Service X ○ ○ ○ ○(ポート数:2) サイロ02 App Service X ○ ○ ○ ○(ポート数:2) 永続化ストレージ Table Storage X ○ ○ ーーー ーーー VNet 10.0.0.0/16 Subnet(受信) 10.0.30.0/24 Subnet(送信) 10.0.21.0/24 Subnet(送信) 10.0.1.0/24 永続化ストレージ Table Storage Subnet(受信) 10.0.10.0/24 サイロ01 App Service www フロントエンド App Service サイロ02 App Service Subnet(送信) 10.0.22.0/24 31
App Service へのデプロイ(概念図と構成図の対比) Orleans Cluster www HTTP Response Front End HTTP Request Silo Glain Grain Call Glain Glain Grain Response Silo Glain Data Query Data Result Glain Glain VNet 10.0.0.0/16 Glain Glain Subnet(受信) 10.0.30.0/24 Subnet(送信) 10.0.21.0/24 Subnet(送信) 10.0.1.0/24 DB (Persistent Storage) 永続化ストレージ Table Storage Subnet(受信) 10.0.10.0/24 サイロ01 App Service www フロントエンド App Service サイロ02 App Service Subnet(送信) 10.0.22.0/24 32
基盤構成のポイント①:VNet の設定 構成では、フロントエンドのみパブリックアクセス可能、2つのサイロとストレージはプライベートアクセスのみ可能とします が、プライベートアクセスはリージョン VNet 統合とプライベートエンドポイントで実現します。 そのための VNet の設定は以下のようになります。 アドレス空間 接続デバイス (プライベートエンドポイント) サブネット 33
基盤構成のポイント②:App Service の プライベートポート サイロのインスタンス同士やサイロとフロントエンド間で WebSocket 通信を 行えるようにするために、WebSocket をオンに設定します。 今回はサイロは プライベート エンドポイント接続とVNet 統合でパブリック アクセスができないようにしているので、プライベートポートが2個(SiloPort、 GatewayPort)必要になります。 この値はポータル上からは設定できないので、PowerShell で設定するか Bicep などの IaC で設定する必要があります。 https://learn.microsoft.com/jajp/dotnet/api/microsoft.azure.management.websites.models.siteco nfigresource.vnetprivateportscount?view=azure-dotnet 34
基盤構成のポイント②:App Service の プライベートポート
PowerShell
$resourceGroupName = '<リソースグループ名>’
$appServiceName = '<App Service 名>’
$webApp = Get-AzResource -ResourceType Microsoft.Web/sites -ResourceGroupName $resourceGroupName -ResourceName $appServiceName
$webApp.Properties.siteConfig.vnetPrivatePortsCount=2
$webApp | Set-AzResource -Force
設定されたポートは、環境変数 WEBSITE_PRIVATE_PORTS で取得できま
す。なお、このポートはスケールユニット内の他のインスタンスのポート使用状
況に応じて動的に設定される(再起動時に変更される)ので、コード内か
らは必ず環境変数から取得して利用する必要があります。
C#
// WEBSITE_PRIVATE_PORTS は再起動で変更されるので WEBSITE_PRIVATE_PORTS として取得し Parse すること
var strPorts = context.Configuration["WEBSITE_PRIVATE_PORTS"].Split(',');
if (strPorts.Length < 2)
throw new Exception("Insufficient private ports configured.");
var (siloPort, gatewayPort) = (int.Parse(strPorts[0]), int.Parse(strPorts[1]));
35
動作確認 フロントエンド パブリックアクセス可 サイロ01 パブリックアクセス不可 サイロ02 パブリックアクセス不可 Azure Table サイロ01、サイロ02のインスタンスのステータスが Active 36
終わりに これまでご説明した予備知識がある状態で、公式ドキュメントを読むとかなり読 みやすくなると思います。 というわけで、.NET 7 の注目機能だと(私が勝手に思っている)Orleans をデ プロイしてみて遊んでみましょう! https://learn.microsoft.com/ja-jp/dotnet/orleans/ 37
ご清聴ありがとうございました