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

適用対象: NoSQL

Azure Cosmos DB は、高速で柔軟性に優れた分散データベースです。待機時間とスループットが保証されており、シームレスにスケーリングできます。 Azure Cosmos DB でデータベースをスケーリングするために、アーキテクチャを大きく変更したり、複雑なコードを記述したりする必要はありません。 スケールアップとスケールダウンは、API 呼び出しを 1 回行うだけの簡単なものです。 詳細については、コンテナーのスループットをプロビジョニングする方法またはデータベースのスループットをプロビジョニングする方法に関するページを参照してください。 ただし、Azure Cosmos DB にはネットワーク呼び出しによってアクセスするため、SQL .NET SDK を使うと、最高のパフォーマンスを実現するためにクライアント側の最適化を行うことができます。

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

.NET V3 SDK へのアップグレード

.NET v3 SDK がリリースされました。 .NET v3 SDK を使用する場合、次の情報については、.NET v3 パフォーマンス ガイドを参照してください。

  • 直接 TCP モードの規定値
  • Stream API のサポート
  • System.Text.JSON の使用を許可するカスタム シリアライザーのサポート
  • バッチと一括の統合サポート

ホスティングの推奨事項

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

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

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

高スループット レベル (1 秒あたりの要求ユニット数が 50,000 超) でテストを行っている場合、コンピューターが CPU 使用率またはネットワーク使用率の上限に達したことでクライアント アプリケーションがボトルネックになることがあります。 この状態に達しても、クライアント アプリケーションを複数のサーバーにスケールアウトすることで引き続き同じ Azure Cosmos DB アカウントで対応できます。

Note

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

メタデータ操作

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

ログとトレース

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

最新の SDK バージョン (2.16.2以上) では、検出時に自動的に削除されます。古いバージョンでは、次の方法で削除できます。

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

ネットワーク

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

.NET V2 SDK の既定の接続モードはゲートウェイです。 ConnectionPolicy パラメーターを使用して DocumentClient インスタンスを構築するときに接続モードを構成します。 直接モードを使用する場合、ConnectionPolicy パラメーターを使用して Protocol を設定する必要もあります。 さまざまな接続オプションについては、接続モードに関する記事を参照してください。

Uri serviceEndpoint = new Uri("https://contoso.documents.net");
string authKey = "your authKey from the Azure portal";
DocumentClient client = new DocumentClient(serviceEndpoint, authKey,
new ConnectionPolicy
{
   ConnectionMode = ConnectionMode.Direct, // ConnectionMode.Gateway is the default
   ConnectionProtocol = Protocol.Tcp
});

一時的なポートの不足

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

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

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

  • ConnectionPolicy.PortReuseMode プロパティを PrivatePortPool に構成します (フレームワークバージョン >= 4.6.1 および .NET core バージョン >= 2.0 で有効): このプロパティにより、SDK は異なる Azure Cosmos DB の宛先エンドポイントに対して一時的なポートの小さなプールを使用できます。
  • ConnectionPolicy.IdleConnectionTimeout プロパティを構成します。これは 10 分以上である必要があります。 推奨値は 20 分から 24 時間です。

OpenAsync を呼び出して最初の要求での開始時の待機時間を回避する

既定では、最初の要求でアドレス ルーティング テーブルを取得する必要があるため、最初の要求の待機時間が長くなります。 SDK V2 を使用しているとき、最初の要求でこの開始時の待機時間を回避するには、初期化中に OpenAsync() を 1 回呼び出します。 呼び出しは次のようになります: await client.OpenAsync();

Note

OpenAsync によって、アカウント内のすべてのコンテナーのアドレス ルーティング テーブルを取得する要求が生成されます。 アカウントに多数のコンテナーがあるにも関わらず、アプリケーションではそのサブセットにしかアクセスしないようなケースでは、OpenAsync によって必要以上のトラフィックが生成され、初期化に時間がかかることがあります。 このシナリオではアプリケーションの起動速度が低下するため、OpenAsync を使用しても効果がない可能性があります。

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

可能であれば、Azure Cosmos DB を呼び出すアプリケーションを Azure Cosmos DB データベースと同じリージョンに配置します。 大ざっぱな比較ですが、Azure Cosmos DB の呼び出しは、同じリージョン内であれば 1 から 2 ミリ秒以内で完了するのに対し、米国西部と米国東部との間では待ち時間が 50 ミリ秒を超えます。 要求がクライアントから Azure データセンターの境界まで流れるときに使用されるルートに応じて、この待機時間が要求ごとに異なる可能性があります。 最短の待機時間は、プロビジョニングされた Azure Cosmos DB エンドポイントと同じ Azure リージョン内に呼び出し元アプリケーションを配置することによって実現できます。 利用可能なリージョンの一覧については、「Azure リージョン」をご覧ください。

Azure Cosmos DB の接続ポリシー

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

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

高速ネットワークの有効化

待機時間と CPU ジッターを減らすために、クライアントの仮想マシンでは高速ネットワークを有効にすることをお勧めします。 「高速ネットワークを使った Windows 仮想マシンの作成」または「高速ネットワークを使った Linux 仮想マシンの作成」を参照してください。

SDK の使用

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

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

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

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

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

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 では、スケジュールされたコードによってスレッドがブロックされることも防げません。
  • ブロック呼び出しを使用してクエリを同期的にドレインする DocumentClient.CreateDocumentQuery(...) で ToList() を使用します。 AsDocumentQuery() により、非同期でクエリを drain します。

すべきこと:

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

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

ゲートウェイ モードを使用するときはホストあたりの 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 をより高い値に設定できます。

RetryAfter 間隔でバックオフを実装する

パフォーマンス テストでは、調整される要求の割合がわずかになるまで負荷を上げる必要があります。 要求がスロットル状態になった場合、クライアント アプリケーション側でバックオフ値を適用し、サーバー側によって指定された再試行間隔のスロットル時間を後退させるようにしてください。 バックオフにより、再試行までの待ち時間を最小限に抑えることができます。

再試行ポリシーは、次の SDK でサポートされています。

詳細については、RetryAfter に関するページを参照してください。

.NET SDK のバージョン 1.19 以降では、次の例のように、追加の診断情報をログに記録し、待ち時間に関する問題をトラブルシューティングするメカニズムがあります。 読み取り待ち時間の長い要求についての診断文字列をログに記録できます。 取得した診断文字列は、特定の要求に対して 429 エラーを受け取った回数を把握するのに役立ちます。

ResourceResponse<Document> readDocument = await this.readClient.ReadDocumentAsync(oldDocuments[i].SelfLink);
readDocument.RequestDiagnosticsString 

ドキュメント URI をキャッシュして読み取り待機時間を減らす

最適な読み取りパフォーマンスを実現するために、できる限りドキュメント URI をキャッシュします。 リソースを作成するときに、リソース ID をキャッシュするロジックを定義する必要があります。 リソース ID に基づく参照は名前ベースの参照よりも高速であるため、これらの値をキャッシュすると、パフォーマンスが向上します。

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

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

クエリ操作

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

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

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

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

var collection = new DocumentCollection { Id = "excludedPathCollection" };
collection.IndexingPolicy.IncludedPaths.Add(new IncludedPath { Path = "/*" });
collection.IndexingPolicy.ExcludedPaths.Add(new ExcludedPath { Path = "/nonIndexedContent/*");
collection = await client.CreateDocumentCollectionAsync(UriFactory.CreateDatabaseUri("db"), collection);

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

スループット

測定と調整によって 1 秒あたりの要求ユニットの使用量を削減する

Azure Cosmos DB には、データベース操作の豊富なセットが用意されています。 UDF、ストアド プロシージャ、トリガーを使用したリレーショナル クエリや階層クエリなどの操作があります。これらの操作はすべて、データベース コレクション内のドキュメントに対して実行できます。 これらの操作のそれぞれに関連付けられたコストは、操作を完了するために必要な CPU、IO、およびメモリに応じて異なります。 ハードウェア リソースの管理について考える代わりに、各種のデータベース操作を実行しアプリケーション要求を処理するのに必要なリソースに関する単一の測定単位として要求ユニット (RU) を考えることができます。

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

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

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

// Measure the performance (Request Units) of writes
ResourceResponse<Document> response = await client.CreateDocumentAsync(collectionSelfLink, myDocument);
Console.WriteLine("Insert of document consumed {0} request units", response.RequestCharge);
// Measure the performance (Request Units) of queries
IDocumentQuery<dynamic> queryable = client.CreateDocumentQuery(collectionSelfLink, queryString).AsDocumentQuery();
while (queryable.HasMoreResults)
    {
        FeedResponse<dynamic> queryResponse = await queryable.ExecuteNextAsync<dynamic>();
        Console.WriteLine("Query batch consumed {0} request units", queryResponse.RequestCharge);
    }

このヘッダーで返される要求の使用量は、プロビジョニングしたスループット (2000 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 の DocumentClientException がスローされます。

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

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

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

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

次のステップ

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

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