Azure Cosmos DB と .NET のパフォーマンスに関するヒント

適用対象: NoSQL

Azure Cosmos DB は、高速で柔軟性に優れた分散データベースです。待ち時間とスループット レベルが保証されており、シームレスにスケーリングできます。 Azure Cosmos DB でデータベースをスケーリングするために、アーキテクチャを大きく変更したり、複雑なコードを記述したりする必要はありません。 スケールアップとスケールダウンは、API 呼び出しを 1 回行うだけの簡単なものです。 詳細については、コンテナーのスループットのプロビジョニングまたはデータベースのスループットのプロビジョニングに関するページを参照してください。

Azure Cosmos DB にはネットワーク呼び出しによってアクセスするため、SQL .NET SDK を使うと、最高のパフォーマンスを実現するためにクライアント側の最適化を行うことができます。

データベースのパフォーマンスを向上させる場合は、次のセクションで示すオプションを検討してください。

ホスティングの推奨事項

サーバー側のガベージ コレクションを有効にする

ガベージ コレクションの頻度を減らした方がよい場合もあります。 .NET では、gcServertrue に設定します。

クライアント ワークロードをスケールアウトする

高スループット レベルでテストしたり、1 秒あたりの要求ユニット数 (RU/秒) が 50,000 を超えるレートでテストしたりすると、クライアント アプリケーションがワークロードのボトルネックになる可能性があります。 この原因として、マシンの CPU またはネットワークの使用率に上限が設定されている可能性があります。 この状態に達しても、クライアント アプリケーションを複数のサーバーにスケールアウトすることで引き続き同じ Azure Cosmos DB アカウントで対応できます。

Note

CPU 使用率が高いと、待ち時間が長くなり、要求タイムアウトの例外が発生する可能性があります。

メタデータ操作

ホット パス内や項目の操作前に、Create...IfNotExistsAsyncRead...Async を呼び出して、データベースやコンテナーが存在することを検証しないでください。 検証は、削除されていることが予想される場合に、必要に応じてアプリケーションの起動時にのみ行う必要があります (それ以外は不要です)。 これらのメタデータ操作では、追加のエンドツーエンドの待機時間が発生し、SLA はなく、データ操作のようにスケーリングしない独自の独立した制限があります。

ログとトレース

一部の環境では、.NET DefaultTraceListener が有効になっています。 DefaultTraceListener は、運用環境でのパフォーマンス上の問題を引き起こし、高い CPU 使用率や I/O のボトルネックが発生します。 調査して、運用環境では TraceListeners から DefaultTraceListener を削除することで、DefaultTraceListener がアプリケーションに対して無効になっているようにしてください。

最新の SDK バージョン (3.23.0 以上) では、これが検出されると自動的に削除されます。より古いバージョンを使用している場合は、以下によってこれを削除できます。

if (!Debugger.IsAttached)
{
    Type defaultTrace = Type.GetType("Microsoft.Azure.Cosmos.Core.Trace.DefaultTrace,Microsoft.Azure.Cosmos.Direct");
    TraceSource traceSource = (TraceSource)defaultTrace.GetProperty("TraceSource").GetValue(null);
    traceSource.Listeners.Remove("Default");
    // Add your own trace listeners
}

ネットワーク

接続ポリシー:直接接続モードを使用する

.NET V3 SDK の TCP プロトコルとの既定の接続モードは直接です。 CosmosClientOptionsCosmosClient インスタンスを作成するときに、接続モードを構成します。 さまざまな接続オプションについては、接続モードに関する記事を参照してください。

string connectionString = "<your-account-connection-string>";
CosmosClient client = new CosmosClient(connectionString,
new CosmosClientOptions
{
    ConnectionMode = ConnectionMode.Gateway // ConnectionMode.Direct is the default
});

一時的なポートの不足

インスタンスで接続量が多いか、ポートの使用率が高い場合は、まず、クライアント インスタンスがシングルトンであることを確認します。 言い換えると、クライアント インスタンスは、アプリケーションの有効期間にわたって一意である必要があります。

クライアントが TCP プロトコル上で実行されている場合は、有効期間が長い接続を使用して待ち時間が最適化されます。 これは、非アクティブ状態が 2 分間続くと接続を終了する HTTPS プロトコルとは対照的です。

アクセス頻度が低く、ゲートウェイ モード アクセスと比べて接続数が多い場合は、次のことが可能です。

  • CosmosClientOptions.PortReuseMode プロパティを PrivatePortPool に構成します (フレームワーク バージョン 4.6.1 以降および .NET Core バージョン 2.0 以降で有効)。 このプロパティにより、SDK はさまざまな Azure Cosmos DB の宛先エンドポイントに対して一時的なポートの小さなプールを使用できます。
  • CosmosClientOptions.IdleTcpConnectionTimeout プロパティを 10 分以上に構成します。 推奨値は 20 分~ 24 時間です。

パフォーマンスを確保するために同じ Azure リージョン内にクライアントを併置する

可能であれば、Azure Cosmos DB を呼び出すアプリケーションを Azure Cosmos DB データベースと同じリージョンに配置します。 大ざっぱな比較ですが、Azure Cosmos DB の呼び出しは、同じリージョン内であれば 1 から 2 ミリ秒 (ms) 以内で終了するのに対し、米国西部と米国東部との間では待ち時間が 50 ミリ秒を超えます。 要求がクライアントから Azure データセンターの境界まで流れるときに使用されるルートに応じて、この待機時間が要求ごとに異なる可能性があります。

最短の待ち時間は、プロビジョニングされた Azure Cosmos DB エンドポイントと同じ Azure リージョン内に呼び出し元アプリケーションを配置することによって実現できます。 利用可能なリージョンの一覧については、「Azure リージョン」をご覧ください。

クライアントを同じリージョンに併置する

スレッドまたはタスクの数を増やす

Azure Cosmos DB の呼び出しはネットワーク経由で行われるため、クライアント アプリケーションで要求間の待ち時間を最短にするために、要求のコンカレンシーの次数を変えることが必要な場合があります。 たとえば、.NET の タスク並列ライブラリを使用する場合、Azure Cosmos DB に対する読み取りタスクまたは書き込みタスクを 100 件単位で作成してください。

高速ネットワークを有効にして待機時間と CPU ジッターを削減する

パフォーマンスを最大限に高めるために、手順に従って Windows (クリックして手順を参照) または Linux (クリックして手順を参照) Azure VM で高速ネットワークを有効にすることをお勧めします。

高速ネットワークを使用しない場合、Azure VM と他の Azure リソース間を通過する IO は、VM とそのネットワーク カードの間にあるホストと仮想スイッチを介して不必要にルーティングされる可能性があります。 データパスにインラインでホストと仮想スイッチがあると、通信チャネルで待機時間とジッターが増加するだけでなく、VM から CPU サイクルが奪われます。 高速ネットワークを使用すると、VM は中継なしで NIC と直接やり取りします。ホストと仮想スイッチによって処理されていたネットワーク ポリシーの詳細は、NIC のハードウェアで処理されるようになり、ホストと仮想スイッチはバイパスされます。 通常、高速ネットワークを有効にすると、待機時間の短縮とスループットの向上だけでなく、より "一貫した" 待機時間と CPU 使用率の削減が期待できます。

制限事項: 高速ネットワークは、VM の OS でサポートされている必要があり、VM が停止され、割り当てが解除されている場合にのみ有効にすることができます。 Azure Resource Manager を使用して VM をデプロイすることはできません。 App Service では高速ネットワークが有効になっていません。

詳細については、Windows および Linux の手順を参照してください。

SDK の使用

最新の SDK をインストールする

Azure Cosmos DB SDK は、最適なパフォーマンスを提供するために頻繁に改善されています。 最新の SDK を確認し、改善点をレビューするには、Azure Cosmos DB SDK を参照してください。

ストリーム API を使用する

.NET SDK V3 には、シリアル化せずにデータを受信して返すことができるストリーム API シリーズが含まれています。

SDK からの応答を直接使用せずに他のアプリケーション層にリレーする中間層アプリケーションでは、ストリーム API を利用するメリットがあります。 ストリーム処理の例については、項目管理のサンプルを参照してください。

アプリケーションの有効期間中はシングルトン Azure Cosmos DB クライアントを使用する

CosmosClient インスタンスはスレッドセーフであり、直接モードで動作しているときには効率的な接続管理とアドレスのキャッシュが実行されます。 効率的な接続管理と SDK クライアントのパフォーマンス向上を実現するために、アプリケーションの有効期間中、アプリケーションが対話する各アカウントについて、AppDomain ごとの単一のインスタンスを使用することをお勧めします。

複数のアカウントを処理するマルチテナント アプリケーションについては、関連するベスト プラクティスを参照してください。

Azure Functions で作業する場合、インスタンスは既存のガイドラインにも従って、1 つのインスタンスを維持する必要があります。

ブロックキング呼び出しを避ける

Azure Cosmos DB SDK では、多くの要求を同時に処理できるように設計する必要があります。 非同期 API では、ブロッキング呼び出しが実行されるのを待たないことによって、小さなスレッド プールでも数千の要求を同時に処理できます。 各スレッドで、時間のかかる同期的なタスクの処理が完了するのを待たずに、別の要求を処理します。

Azure Cosmos DB SDK を使用するアプリでよくあるパフォーマンスの問題は、ブロッキング呼び出しが非同期の可能性があることです。 多くの同期的なブロッキング呼び出しを行うと、スレッド プールの枯渇や応答時間の増加が起こります。

してはいけないこと:

  • Task.WaitTask.Result を呼び出して非同期処理を妨げる。
  • Task.Run を使用して同期 API を非同期にする。
  • 共通のコードのパスでロックを取得する。 Azure Cosmos DB .NET SDK では、コードを並列実行するよう設計すると、パフォーマンスが最もよくなります。
  • Task.Run を呼び出して直ちに await を使用する。 既に ASP.NET Core で通常のスレッド プールのスレッドを使用してアプリのコードを実行しているため、Task.Run を呼び出しても、スレッド プールに対する無駄なスケジューリングを行うだけです。 Task.Run では、スケジュールされたコードによってスレッドがブロックされることも防げません。
  • Container.GetItemLinqQueryable<T>() に ToList() を使用し、ブロッキング呼び出しでクエリを同期的に drain しない。 ToFeedIterator() を使用して、クエリを非同期でドレインしてください。

すべきこと:

  • Azure Cosmos DB .NET API を非同期で呼び出します。
  • コール スタック全体を非同期にして、async/await の組み合わせを有効活用する。

PerfView などのプロファイラーを使用すれば、スレッド プールに頻繁に追加されるスレッドを把握できます。 Microsoft-Windows-DotNETRuntime/ThreadPoolWorkerThread/Start イベントは、スレッド プールにスレッドを追加したことを示します。

書き込み操作に対するコンテンツ応答を無効にする

高負荷の作成ペイロードがあるワークロードでは、EnableContentResponseOnWrite 要求オプションを false に設定します。 サービスは、作成または更新されたリソースを SDK に返さなくなります。 通常、アプリケーションには作成済みのオブジェクトがあるため、サービスから返される必要はありません。 ヘッダー値には、まだ要求の料金と同様にアクセスできます。 応答コンテンツを無効にすると、SDK がメモリを割り当てたり応答の本文をシリアル化したりする必要がなくなるため、パフォーマンスを向上させることができます。 また、ネットワーク帯域幅の使用量も削減され、パフォーマンスがさらに向上します。

ItemRequestOptions requestOptions = new ItemRequestOptions() { EnableContentResponseOnWrite = false };
ItemResponse<Book> itemResponse = await this.container.CreateItemAsync<Book>(book, new PartitionKey(book.pk), requestOptions);
// Resource will be null
itemResponse.Resource

待ち時間ではなくスループットを最適化するために一括処理を有効にする

ワークロードが大量のスループットを必要とし、待ち時間はそれほど重要ではないシナリオでは、一括処理を有効にします。 一括処理機能を有効にする方法と、その機能を使用する必要があるシナリオの詳細については、一括処理の概要に関するページを参照してください。

ゲートウェイ モードを使用するときにホストあたりの System.Net MaxConnections を増やす

ゲートウェイ モードを使用すると、Azure Cosmos DB の要求は HTTPS/REST を介して行われます。 それらは、ホスト名または IP アドレスごとの既定の接続数の上限に従います。 場合によっては、Azure Cosmos DB に対する複数の同時接続をクライアント ライブラリで使用できるよう、MaxConnections を高い値 (100 ~ 1,000) に増やす必要があります。 .NET SDK 1.8.0 以降では、ServicePointManager.DefaultConnectionLimit の既定値は 50 です。 値を変更するには、Documents.Client.ConnectionPolicy.MaxConnectionLimit を高い値に設定します。

スレッドまたはタスクの数を増やす

この記事の「ネットワーク」セクションの「スレッドまたはタスクの数を増やす」を参照してください。

クエリ操作

クエリ操作については、クエリ パフォーマンスのヒントに関するページを参照してください。

インデックス作成ポリシー

インデックス作成から未使用のパスを除外して書き込みを高速化する

Azure Cosmos DB のインデックス作成ポリシーでは、インデックス作成パス (IndexingPolicy.IncludedPaths および IndexingPolicy.ExcludedPaths) を使用して、インデックス作成に含めたり除外したりするドキュメント パスも指定できます。

必要なパスだけでインデックスを作成すると、クエリのパターンが事前にわかっている場合に、書き込みパフォーマンスの向上、書き込み操作に対する RU 料金の削減、およびインデックス ストレージの削減が可能になります。 これは、インデックス作成コストは、インデックスが作成される一意のパスの数に直接関係するためです。 たとえば、次のコードは、ワイルドカード "*" を使用して、ドキュメントのセクション全体 (サブツリー) をインデックス作成から除外する方法を示しています。

var containerProperties = new ContainerProperties(id: "excludedPathCollection", partitionKeyPath: "/pk" );
containerProperties.IndexingPolicy.IncludedPaths.Add(new IncludedPath { Path = "/*" });
containerProperties.IndexingPolicy.ExcludedPaths.Add(new ExcludedPath { Path = "/nonIndexedContent/*");
Container container = await this.cosmosDatabase.CreateContainerAsync(containerProperties);

詳細については、Azure Cosmos DB インデックス作成ポリシーに関するページをご覧ください。

スループット

低 RU/秒の使用状況を測定して調整する

Azure Cosmos DB には、データベース操作の豊富なセットが用意されています。 ユニバーサル ディスク フォーマット (UDF)、ストアド プロシージャ、トリガーを使用したリレーショナル クエリや階層クエリなどの操作があります。これらの操作はすべて、データベース コレクション内のドキュメントに対して実行できます。

これらの操作のそれぞれに関連付けられたコストは、操作を完了するために必要な CPU、IO、およびメモリに応じて異なります。 ハードウェア リソースの検討や管理をする代わりに、要求ユニットをさまざまなデータベース操作の実行やアプリケーション要求の処理に必要なリソースの 1 つのメジャーとして考えることができます。

コンテナーごとに設定された要求ユニットの数に基づいて、スループットをプロビジョニングします。 要求単位の消費は 1 秒あたりのレートとして評価されます。 コンテナーのプロビジョニング済み要求ユニット レートを超過したアプリケーションは、レートがそのコンテナーにプロビジョニングされているレベルを下回るまで制限されます。 アプリケーションでより高いスループットが必要になった場合は、追加の要求ユニットをプロビジョニングしてスループットを増やすことができます。

クエリの複雑さは、操作で消費される要求ユニット数に影響します。 述語の数、述語の特性、UDF ファイルの数、ソース データ セットのサイズのすべてがクエリ操作のコストに影響します。

操作 (作成、更新、または削除) のオーバーヘッドを測定するには、x-ms-request-charge ヘッダー (あるいは、.NET SDK の ResourceResponse<T> または FeedResponse<T> の同等の RequestCharge プロパティ) を調べて、これらの操作で使用される要求ユニット数を測定します。

// Measure the performance (Request Units) of writes
ItemResponse<Book> response = await container.CreateItemAsync<Book>(myBook, new PartitionKey(myBook.PkValue));
Console.WriteLine("Insert of item consumed {0} request units", response.RequestCharge);
// Measure the performance (Request Units) of queries
FeedIterator<Book> queryable = container.GetItemQueryIterator<ToDoActivity>(queryString);
while (queryable.HasMoreResults)
    {
        FeedResponse<Book> queryResponse = await queryable.ExecuteNextAsync<Book>();
        Console.WriteLine("Query batch consumed {0} request units", queryResponse.RequestCharge);
    }

このヘッダーで返される要求の使用量は、プロビジョニングしたスループット (2,000 RU/秒) の一部です。 たとえば、上記のクエリが 1 KB のドキュメントを 1000 個返した場合、この操作のコストは 1000 になります。 そのため、後続の要求をレート制限する前に、サーバーは 1 秒以内にこのような要求を 2 つだけ受け付けます。 詳細については、要求ユニットに関する記事および要求ユニット計算ツールのページを参照してください。

レート制限と大きすぎる要求レートに対処する

クライアントがアカウントの予約済みスループットを超えようとしても、サーバーでパフォーマンスの低下が発生することはなく、予約済みのレベルを超えてスループット容量が使用されることもありません。 サーバーは、RequestRateTooLarge (HTTP 状態コード 429) を使用して要求をプリエンプティブに終了します。 それからは、要求を再試行する前にユーザーが待機する必要がある時間 (ミリ秒単位) を示す、x-ms-retry-after-ms ヘッダーが返されます。

    HTTP Status 429,
    Status Line: RequestRateTooLarge
    x-ms-retry-after-ms :100

SDK はすべてこの応答を暗黙的にキャッチし、サーバーが指定した retry-after ヘッダーを優先して要求を再試行します。 アカウントに複数のクライアントが同時アクセスしている状況でなければ、次回の再試行は成功します。

累積的に動作する複数のクライアントがあり、要求レートを常に超えている場合は、現在クライアントによって内部的に 9 に設定される既定の再試行回数では、十分ではない可能性があります。 このような場合、クライアントではアプリケーションに対して状態コード 429 の CosmosException がスローされます。

CosmosClientOptions インスタンスで RetryOptions を設定することにより、既定の再試行回数を変更できます。 既定では、要求レートを超えて要求が続行されている場合に、30 秒の累積待ち時間を過ぎると、状態コードが 429 の CosmosException が返されます。 このエラーは、現在の値が既定値の 9 かユーザー定義の値かにかかわらず、現在の再試行回数が最大再試行回数より少ない場合でも返されます。

自動再試行動作は、ほとんどのアプリケーションで回復性と使いやすさを向上させるのに役立ちます。 ただし、パフォーマンス ベンチマークを実行しているときは (特に待機時間を測定するとき)、最適な動作ではない可能性があります。 実験でサーバー スロットルが発生し、クライアント SDK によって警告なしに再試行が行われると、クライアントが監視する待機時間が急増します。 パフォーマンスの実験中に待ち時間が急増するのを回避するには、各操作で返される使用量を測定し、予約済みの要求レートを下回った状態で要求が行われていることを確認します。

詳細については、要求ユニットに関する記事を参照してください。

スループットを向上させるためにサイズの小さいドキュメントに合わせて設計する

特定の操作の要求の使用量 (要求処理コスト) は、ドキュメントのサイズに直接関係します。 サイズの大きいドキュメントの操作は、サイズの小さいドキュメントの操作よりもコストがかかります。

次のステップ

少数のクライアント コンピューターでの高パフォーマンス シナリオで Azure Cosmos DB の評価に使用されるサンプル アプリケーションについては、「Azure Cosmos DB のパフォーマンスとスケールのテスト」を参照してください。

スケーリングと高パフォーマンスのためのアプリケーションの設計について詳しくは、「Azure Cosmos DB でのパーティション分割とスケーリング」をご覧ください。