パフォーマンス チューニング - 複数のバックエンドサービスの呼び出し

Azure Kubernetes Service (AKS)
Azure Cosmos DB

この記事では、開発チームでメトリックを使用してボトルネックを発見し、分散システムのパフォーマンスを向上させた方法について説明します。 この記事は、サンプル アプリケーションに対して行われた実際のロード テストに基づいています。 アプリケーションは、マイクロサービスの Azure Kubernetes Service (AKS) ベースラインからのもの、また結果の生成には Visual Studio ロード テスト プロジェクトが使用されています。

この記事はシリーズの一部です。 最初のパートはこちらからお読みください。

シナリオ:複数のバックエンド サービスを呼び出して情報を取得し、結果を集計します。

このシナリオでは、ドローン配達アプリケーションを扱います。 クライアントは REST API に対してクエリを実行し、最新の請求書情報を取得することができます。 請求書には、顧客の配達、荷物、全体的なドローン使用状況の要約が含まれます。 このアプリケーションでは、AKS 上で実行されるマイクロサービス アーキテクチャを使用し、請求書に必要な情報は複数のマイクロサービスに分散しています。

クライアントが各サービスを直接呼び出すのではなく、アプリケーションはゲートウェイ集約パターンを実装します。 このパターンを使用して、クライアントはゲートウェイ サービスに対して単一の要求を行います。 その後、ゲートウェイは並列でバックエンド サービスを呼び出し、結果を 1 つの応答ペイロードに集約します。

ゲートウェイ集約パターンを示す図

テスト 1:ベースライン パフォーマンス

ベースラインを確立するために、開発チームはステップロード テストから始め、合計 8 分間にわたって、1 人のシミュレートされたユーザーから最大 40 ユーザーまで負荷を上げていきました。 Visual Studio から取得した次のグラフは、結果を示しています。 紫色の線はユーザー負荷を示し、オレンジ色の線はスループット (1 秒あたりの平均要求数) を示しています。

Visual Studio のロード テスト結果のグラフ

グラフの最下部に沿った赤い線は、クライアントにエラーが返されなかった、つまり良い結果であることを示しています。 しかし、平均スループットはテストのほぼ中間点でピークに達し、その後は負荷が上昇を続ける一方で低下しています。 これは、バックエンドの処理能力が追いつかないことを示しています。 このようなパターンは、システムがリソースの制限に達しかけたときによく見られるものであり、上限に達した後は、実際にスループットが大きく低下します。 リソースの競合、一時的なエラー、または例外発生率上昇のどれもが、このパターンの原因となる可能性があります。

監視データを詳しく調べて、システムの中で何が起きているのかを突き止めましょう。 次のグラフは Application Insights から取得したものです。 ゲートウェイからバックエンド サービスへの HTTP 呼び出しの平均継続時間を示しています。

HTTP 呼び出しの継続時間のグラフ

このグラフは、特に 1 つの操作 GetDroneUtilization の平均所要時間が桁違いに長いことを示しています。 ゲートウェイはこれらの呼び出しを並列で実行するため、要求全体の完了にかかる時間は最も遅い操作によって決まります。

GetDroneUtilization 操作を詳しく調べてボトルネックを探すことが次のステップであることは明らかです。 1 つの可能性はリソースの枯渇です。 この特定のバックエンド サービスで CPU またはメモリが不足している可能性があります。 AKS クラスターの場合、この情報は Azure portal でAzure Monitor のコンテナーの分析情報機能を使用して確認できます。 次のグラフは、クラスター レベルでのリソース使用率を示しています。

AKS ノード使用率のグラフ

このスクリーンショットでは、平均値と最大値の両方が示されています。 平均はデータの急上昇を隠すことがあるため、平均以外にも注目することが重要です。 ここでは、平均 CPU 使用率は 50%を下回っていますが、80% までの急上昇が何回か見られます。 これはキャパシティの上限付近ですが、まだ許容範囲内です。 何か他のものがボトルネックの原因になっています。

次のグラフが本当の原因を明らかにしています。 このグラフは、Delivery サービスのバックエンド データベース (この場合は Azure Cosmos DB) からの HTTP 応答コードを示しています。 青色の線は成功コード (HTTP 2xx) を表し、緑色の線は HTTP 429 エラーを表します。 HTTP 429 リターン コードは、プロビジョニングされた量を超えるリソース ユニット (RU) を呼び出し元が消費しているため、Azure Cosmos DB が一時的に要求の帯域幅を調整していることを意味します。

帯域幅調整された要求のグラフ

さらに洞察を得るために、開発チームは Application Insights を使用して、代表的な要求サンプルのエンドツーエンドのテレメトリを表示しました。 次に 1 つの例を示します。

エンドツーエンドのトランザクションのビューのスクリーンショット

このビューは、1 つのクライアント要求に関連した呼び出しを、タイミング情報および応答コードと共に示します。 最上位レベルの呼び出しは、ゲートウェイからバックエンド サービスへのものです。 GetDroneUtilization の呼び出しを展開して、外部依存関係 (この場合は Azure Cosmos DB) の呼び出しを表示しています。 赤色の呼び出しが HTTP 429 エラーを返しました。

HTTP 429 エラーから次の呼び出しまでの間に大きなギャップがあることに注目してください。 Azure Cosmos DB クライアント ライブラリは、HTTP 429 エラーを受信すると、自動的に処理を中止して操作の再試行を待機します。 このビューは、この操作の所要時間 672 ミリ秒の大半が Azure Cosmos DB の再試行待ちに費やされたことを示しています。

この分析に関してもう 1 つの興味深いグラフがあります。 これは、物理パーティションあたりの RU プロビジョニング数に対する、物理パーティションあたりの RU 消費を示しています。

パーティションあたりの RU 消費のグラフ

このグラフを理解するには、Azure Cosmos DB のパーティション管理のしくみを理解する必要があります。 Azure Cosmos DB のコレクションには "パーティション キー" を設定できます。 それぞれの可能なキー値は、コレクション内でのデータの論理パーティションを定義します。 Azure Cosmos DB は、これらの論理パーティションを 1 つ以上の "物理" パーティションに分散させます。 物理パーティションの管理は Azure Cosmos DB によって自動的に処理されます。 データの格納量が増えると、Azure Cosmos DB は、物理パーティション間で負荷を分散させるために、論理パーティションを新しい物理パーティションに移動する場合があります。

このロード テストでは、Azure Cosmos DB コレクションには 900 RU がプロビジョニングされています。 グラフでは、物理パーティションあたりの RU 数が 100 と示されており、全部で 9 個の物理パーティションがあることを暗示しています。 物理パーティションのシャード化は Azure Cosmos DB によって自動的に処理されますが、パーティション数を知ることでパフォーマンスに関する洞察が得られることがあります。 開発チームは、最適化を続けるために後でこの情報を使用します。 青色の線が紫色の水平線を突破している箇所では、RU の消費が RU のプロビジョニング数を超えています。 ここが、Azure Cosmos DB が呼び出しの帯域幅調整を開始するポイントです。

テスト 2:リソース単位を増やす

2 番目のロード テストでは、チームは Azure Cosmos DB コレクションを 900 RU から 2500 RU にスケールアウトしました。 スループットは 19 要求/秒から 23 要求/秒に上昇し、平均待機時間は 669 ミリ秒から 569 ミリ秒に短縮されました。

メトリック テスト 1 テスト 2
スループット (要求/秒) 19 23
平均待機時間 (ミリ秒) 669 569
成功した要求 9.8 K 11 K

劇的な向上ではありませんが、経時グラフを見ると、さらに全体像がはっきりします。

より一貫したスループットを示す Visual Studio ロード テスト結果のグラフ。

前のテストでは最初の急上昇の後に急降下が見られましたが、このテストでは前よりもスループットが安定しています。 ただし、最大スループットはそれほど上がっていません。

Azure Cosmos DB に対するすべての要求は 2xx ステータスを返すようになり、HTTP 429 エラーはなくなりました。

Azure Cosmos DB の呼び出しのグラフ

RU の消費とプロビジョニング数の対比グラフは、十分な余裕を示しています。 およそ 275 という物理パーティションあたりの RU 数に対し、ロード テストでの 1 秒あたりの RU 消費は 100 前後がピークとなっています。

十分なヘッドルームがあることを示す RU の消費とプロビジョニング数の対比グラフ。

もう 1 つの興味深いメトリックは、操作成功あたりの Azure Cosmos DB の呼び出し数です。

メトリック テスト 1 テスト 2
操作あたりの呼び出し数 11 9

エラーがないと仮定すれば、呼び出し数は実際のクエリ プランと一致するはずです。 この場合、操作は 9 つの物理パーティションすべてにヒットするクロスパーティション クエリを伴っています。 最初のロード テストの高い値は、429 エラーを返した呼び出しの数を反映しています。

このメトリックは、カスタムの Log Analytics クエリを実行して算出されました。

let start=datetime("2020-06-18T20:59:00.000Z");
let end=datetime("2020-07-24T21:10:00.000Z");
let operationNameToEval="GET DroneDeliveries/GetDroneUtilization";
let dependencyType="Azure DocumentDB";
let dataset=requests
| where timestamp > start and timestamp < end
| where success == true
| where name == operationNameToEval;
dataset
| project reqOk=itemCount
| summarize
    SuccessRequests=sum(reqOk),
    TotalNumberOfDepCalls=(toscalar(dependencies
    | where timestamp > start and timestamp < end
    | where type == dependencyType
    | summarize sum(itemCount)))
| project
    OperationName=operationNameToEval,
    DependencyName=dependencyType,
    SuccessRequests,
    AverageNumberOfDepCallsPerOperation=(TotalNumberOfDepCalls/SuccessRequests)

まとめると、2 番目のロード テストは改善を示しています。 しかし、GetDroneUtilization 操作はまだ、次に遅い操作に比べて桁違いの時間を要しています。 エンドツーエンドのトランザクションを見れば、その理由の説明に役立ちます。

改善を示す 2 番目のロード テストのスクリーンショット。

前述したように、GetDroneUtilization 操作は Azure Cosmos DB に対するクロスパーティション クエリを伴います。 これは、Azure Cosmos DB クライアントがクエリを各物理パーティションに散開して結果を収集する必要があることを意味します。 エンドツーエンドのトランザクションのビューが示すように、これらのクエリは直列で実行されています。 操作にはすべてのクエリの合計と同じだけの時間がかかります。データのサイズが大きくなり、物理パーティションがさらに追加されるのに伴い、この問題は悪化の一途をたどります。

テスト 3:並列クエリ

以上の結果を踏まえれば、待機時間を縮める明白な方法は、クエリを並列で発行することです。 Azure Cosmos DB クライアント SDK には、並列処理の最大範囲を制御する設定があります。

説明
0 並列処理なし (既定)
> 0 最大限の並列呼び出し
-1 クライアント SDK が並列処理の最適な範囲を選択する

3 番目のロード テストでは、この設定が 0 から -1 に変更されました。 結果を次の表にまとめています。

メトリック テスト 1 テスト 2 テスト 3
スループット (要求/秒) 19 23 42
平均待機時間 (ミリ秒) 669 569 215
成功した要求 9.8 K 11 K 20 K
スロットルされた要求 2.72 K 0 0

ロード テストのグラフを見ると、全体的なスループット (オレンジ色の線) が大きく上昇しているだけでなく、スループットと負荷 (紫色の線) の変化が同じようなペースになっています。

負荷に対応するより高い全体的なスループットを示す Visual Studio のロード テスト結果のグラフ。

エンドツーエンドのトランザクションのビューを見ると、Azure Cosmos DB クライアントが並列でクエリを実行していることが確認できます。

Azure Cosmos DB クライアントが並列でクエリを作成していることを示すエンドツーエンドのトランザクション ビューのスクリーンショット。

興味深いことに、スループット上昇の副作用として、1 秒あたりの RU 消費数も増加しています。 このテスト中、Azure Cosmos DB は要求の帯域幅調整を行いませんでしたが、消費はプロビジョニングされた RU 制限に近い値になりました。

プロビジョニングされた RU の上限に近づいている RU の消費量のグラフ。

このグラフは、データベースをさらにスケールアウトする契機になる場合があります。 ただし、代わりにクエリを最適化できることがわかりました。

手順 4:クエリを最適化する

前のロード テストは、待機時間とスループットの面でパフォーマンスの向上を示しました。 要求の平均待機時間は 68% 短縮され、スループットは 220% 上昇しました。 ただし、クロスパーティション クエリは懸念事項です。

クロスパーティション クエリの問題は、すべてのパーティションにわたって RU のコストが発生することです。 クエリがたまに (たとえば、1 時間に 1 回) しか実行されない場合、そのことは問題になりません。 ただし、クロスパーティション クエリを伴った、読み取りが多いワークロードがある場合は常に、パーティション キーを含めることによってクエリを最適化できるかどうか試してみることを推奨します。 (異なるパーティション キーを使用するために、コレクションの再設計が必要になる場合があります。)

この特定のシナリオのクエリを次に示します。

SELECT * FROM c
WHERE c.ownerId = <ownerIdValue> and
      c.year = <yearValue> and
      c.month = <monthValue>

このクエリは、特定の所有者 ID と月/年に一致するレコードを選択します。 元の設計では、これらのプロパティはどれもパーティション キーではありません。 そのため、クライアントはクエリを各物理パーティションに散開して結果を収集する必要があります。 クエリのパフォーマンスを改善するために、開発チームは、所有者 ID がコレクションのパーティション キーになるように設計を変更しました。 そうすれば、クエリは特定の物理パーティションをターゲットにできます。 (Azure Cosmos DB はこれを自動的に処理します。パーティション キーの値と物理パーティションとの間のマッピングを人間が管理する必要はありません。)

コレクションを新しいパーティション キーに切り替えた後、RU 消費に劇的な改善がみられ、これがコストの削減に直結します。

メトリック テスト 1 テスト 2 テスト 3 テスト 4
操作あたりの RU 数 29 29 29 3.4
操作あたりの呼び出し数 11 9 10 1

エンドツーエンドのトランザクションのビューは、予測どおり、クエリが 1 つの物理パーティションのみを読み取ることを示しています。

クエリが 1 つの物理パーティションのみを読み取ることを示すエンドツーエンドのトランザクション ビューのスクリーンショット。

ロード テストは、スループットと待機時間の改善を示しています。

メトリック テスト 1 テスト 2 テスト 3 テスト 4
スループット (要求/秒) 19 23 42 59
平均待機時間 (ミリ秒) 669 569 215 176
成功した要求 9.8 K 11 K 20 K 29 K
スロットルされた要求 2.72 K 0 0 0

パフォーマンス改善の結果、ノードの CPU 使用率が非常に高くなります。

ノードの CPU 使用率が高いことを示すグラフ。

ロード テストの終了に向けて、平均 CPU は約 90% に達し、最大 CPU は 100% に達しました。 このメトリックは、CPU がシステムの次のボトルネックであることを示しています。 より高いスループットが必要な場合、次のステップは、より多くのインスタンスに Delivery サービスをスケールアウトすることかもしれません。

まとめ

このシナリオでは、次のボトルネックが特定されました。

  • RU のプロビジョニング不足が原因で Azure Cosmos DB が要求の帯域幅を調整した。
  • 複数のデータベース パーティションを並列でクエリすることが原因で待機時間が長くなった。
  • クエリにパーティション キーが含まれないためクロスパーティション クエリが非効率的だった。

さらに、より大規模な環境では CPU 使用率が潜在的なボトルネックになることが特定されました。 これらの問題を診断するために、開発チームは次のことに注目しました。

  • ロード テストでの待機時間とスループット。
  • Azure Cosmos DB のエラーと RU 消費。
  • Application Insight のエンドツーエンドのトランザクションのビュー。
  • Azure Monitor のコンテナーの分析情報での CPU およびメモリ使用率。

次のステップ

パフォーマンスのアンチパターンを確認します