パフォーマンス チューニングのシナリオ:複数のバックエンド サービスPerformance tuning scenario: Multiple backend services

この記事では、開発チームでメトリックを使用してボトルネックを発見し、分散システムのパフォーマンスを向上させた方法について説明します。This article describes how a development team used metrics to find bottlenecks and improve the performance of a distributed system. この記事は、サンプル アプリケーションに対して行われた実際のロード テストに基づいています。The article is based on actual load testing that we did for a sample application. アプリケーションのコードは、結果の生成に使用される Visual Studio のロード テスト プロジェクトと共に GitHub から入手できます。The application code is available on GitHub, along with the Visual Studio load test project used to generate the results.

この記事はシリーズの一部です。最初のパートはこちらからお読みください。This article is part of a series. Read the first part here.

シナリオ:複数のバックエンド サービスを呼び出して情報を取得し、結果を集計します。Scenario: Call multiple backend services to retrieve information and then aggregate the results.

このシナリオでは、ドローン配達アプリケーションを扱います。This scenario involves a drone delivery application. クライアントは REST API に対してクエリを実行し、最新の請求書情報を取得することができます。Clients can query a REST API to get their latest invoice information. 請求書には、顧客の配達、荷物、全体的なドローン使用状況の要約が含まれます。The invoice includes a summary of the customer's deliveries, packages, and total drone utilization. このアプリケーションでは、Azure Kubernetes Service (AKS) 上で実行されるマイクロサービス アーキテクチャを使用し、請求書に必要な情報は複数のマイクロサービスに分散しています。This application uses a microservices architecture running on Azure Kubernetes Service (AKS), and the information needed for the invoice is spread across several microservices.

クライアントが各サービスを直接呼び出すのではなく、アプリケーションはゲートウェイ集約パターンを実装します。Rather than the client calling each service directly, the application implements the Gateway Aggregation pattern. このパターンを使用して、クライアントはゲートウェイ サービスに対して単一の要求を行います。Using this pattern, the client makes a single request to a gateway service. その後、ゲートウェイは並列でバックエンド サービスを呼び出し、結果を 1 つの応答ペイロードに集約します。The gateway in turn calls the backend services in parallel, and then aggregates the results into a single response payload.

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

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

ベースラインを確立するために、開発チームはステップロード テストから始め、合計 8 分間にわたって、1 人のシミュレートされたユーザーから最大 40 ユーザーまで負荷を上げていきました。To establish a baseline, the development team started with a step-load test, ramping the load from one simulated user up to 40 users, over a total duration of 8 minutes. Visual Studio から取得した次のグラフは、結果を示しています。The following chart, taken from Visual Studio, shows the results. 紫色の線はユーザー負荷を示し、オレンジ色の線はスループット (1 秒あたりの平均要求数) を示しています。The purple line shows the user load, and the orange line shows throughput (average requests per second).

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

グラフの最下部に沿った赤い線は、クライアントにエラーが返されなかった、つまり良い結果であることを示しています。The red line along the bottom of the chart shows that no errors were returned to the client, which is encouraging. しかし、平均スループットはテストのほぼ中間点でピークに達し、その後は負荷が上昇を続ける一方で低下しています。However, the average throughput peaks about half way through the test, and then drops off for the remainder, even while the load continues to increase. これは、バックエンドの処理能力が追いつかないことを示しています。That indicates the back end is not able to keep up. このようなパターンは、システムがリソースの制限に達しかけたときによく見られるものであり、上限に達した後は、実際にスループットが大きく低下します。The pattern seen here is common when a system starts to reach resource limits — after reaching a maximum, throughput actually falls significantly. リソースの競合、一時的なエラー、または例外発生率上昇のどれもが、このパターンの原因となる可能性があります。Resource contention, transient errors, or an increase in the rate of exceptions can all contribute to this pattern.

監視データを詳しく調べて、システムの中で何が起きているのかを突き止めましょう。Let's dig into the monitoring data to learn what's happening inside the system. 次のグラフは Application Insights から取得したものです。The next chart is taken from Application Insights. ゲートウェイからバックエンド サービスへの HTTP 呼び出しの平均継続時間を示しています。It shows the average durations of the HTTP calls from the gateway to the backend services.

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

このグラフは、特に 1 つの操作 GetDroneUtilization の平均所要時間が桁違いに長いことを示しています。This chart shows that one operation in particular, GetDroneUtilization, takes much longer on average — by an order of magnitude. ゲートウェイはこれらの呼び出しを並列で実行するため、要求全体の完了にかかる時間は最も遅い操作によって決まります。The gateway makes these calls in parallel, so the slowest operation determines how long it takes for the entire request to complete.

GetDroneUtilization 操作を詳しく調べてボトルネックを探すことが次のステップであることは明らかです。Clearly the next step is dig into the GetDroneUtilization operation and look for any bottlenecks. 1 つの可能性はリソースの枯渇です。One possibility is resource exhaustion. この特定のバックエンド サービスで CPU またはメモリが不足している可能性があります。Perhaps this particular backend service is running out of CPU or memory. AKS クラスターの場合、この情報は Azure portal でコンテナーに対する Azure Monitor 機能を使用して確認できます。For an AKS cluster, this information is available in the Azure portal through the Azure Monitor for containers feature. 次のグラフは、クラスター レベルでのリソース使用率を示しています。The following graphs show resource utilization at the cluster level:

AKS ノード使用率のグラフ

このスクリーンショットでは、平均値と最大値の両方が示されています。In this screenshot, both the average and maximum values are shown. 平均はデータの急上昇を隠すことがあるため、平均以外にも注目することが重要です。It's important to look at more than just the average, because the average can hide spikes in the data. ここでは、平均 CPU 使用率は 50%を下回っていますが、80% までの急上昇が何回か見られます。Here, the average CPU utilization stays below 50%, but there are a couple of spikes to 80%. これはキャパシティの上限付近ですが、まだ許容範囲内です。That's close to capacity but still within tolerances. 何か他のものがボトルネックの原因になっています。Something else is causing the bottleneck.

次のグラフが本当の原因を明らかにしています。The next chart reveals the true culprit. このグラフは、Delivery サービスのバックエンド データベース (この場合は Cosmos DB) からの HTTP 応答コードを示しています。This chart shows HTTP response codes from the Delivery service's backend database, which in this case is Cosmos DB. 青色の線は成功コード (HTTP 2xx) を表し、緑色の線は HTTP 429 エラーを表します。The blue line represents success codes (HTTP 2xx), while the green line represents HTTP 429 errors. HTTP 429 リターン コードは、プロビジョニングされた量を超えるリソース ユニット (RU) を呼び出し元が消費しているため、Cosmos DB が一時的に要求の帯域幅を調整していることを意味します。An HTTP 429 return code means that Cosmos DB is temporarily throttling requests, because the caller is consuming more resource units (RU) than provisioned.

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

さらに洞察を得るために、開発チームは Application Insights を使用して、代表的な要求サンプルのエンドツーエンドのテレメトリを表示しました。To get further insight, the development team used Application Insights to view the end-to-end telemetry for a representative sample of requests. 次に 1 つの例を示します。Here is one instance:

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

このビューは、1 つのクライアント要求に関連した呼び出しを、タイミング情報および応答コードと共に示します。This view shows the calls related to a single client request, along with timing information and response codes. 最上位レベルの呼び出しは、ゲートウェイからバックエンド サービスへのものです。The top-level calls are from the gateway to the backend services. GetDroneUtilization の呼び出しを展開して、外部依存関係 (この場合は Cosmos DB) の呼び出しを表示しています。The call to GetDroneUtilization is expanded to show calls to external dependencies — in this case, to Cosmos DB. 赤色の呼び出しが HTTP 429 エラーを返しました。The call in red returned an HTTP 429 error.

HTTP 429 エラーから次の呼び出しまでの間に大きなギャップがあることに注目してください。Note the large gap between the HTTP 429 error and the next call. Cosmos DB クライアント ライブラリは、HTTP 429 エラーを受信すると、自動的に処理を中止して操作の再試行を待機します。When the Cosmos DB client library receives an HTTP 429 error, it automatically backs off and waits to retry the operation. このビューは、この操作の所要時間 672 ミリ秒の大半が Cosmos DB の再試行待ちに費やされたことを示しています。What this view shows is that during the 672 ms this operation took, most of that time was spent waiting to retry Cosmos DB.

この分析に関してもう 1 つの興味深いグラフがあります。Here's another interesting graph for this analysis. これは、物理パーティションあたりの RU プロビジョニング数に対する、物理パーティションあたりの RU 消費を示しています。It shows RU consumption per physical partition versus provisioned RUs per physical partition:

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

このグラフを理解するには、Cosmos DB のパーティション管理のしくみを理解する必要があります。To make sense of this graph, you need to understand how Cosmos DB manages partitions. Cosmos DB のコレクションにはパーティション キーを設定できます。Collections in Cosmos DB can have a partition key. それぞれの可能なキー値は、コレクション内でのデータの論理パーティションを定義します。Each possible key value defines a logical partition of the data within the collection. Cosmos DB は、これらの論理パーティションを 1 つ以上の物理パーティションに分散させます。Cosmos DB distributes these logical partitions across one or more physical partitions. 物理パーティションの管理は Cosmos DB によって自動的に処理されます。The management of physical partitions is handled automatically by Cosmos DB. データの格納量が増えると、Cosmos DB は、物理パーティション間で負荷を分散させるために、論理パーティションを新しい物理パーティションに移動する場合があります。As you store more data, Cosmos DB might move logical partitions into new physical partitions, in order to spread load across the physical partitions.

このロード テストでは、Cosmos DB コレクションには 900 RU がプロビジョニングされています。For this load test, the Cosmos DB collection was provisioned with 900 RUs. グラフでは、物理パーティションあたりの RU 数が 100 と示されており、全部で 9 個の物理パーティションがあることを暗示しています。The chart shows 100 RU per physical partition, which implies a total of nine physical partitions. 物理パーティションのシャード化は Cosmos DB によって自動的に処理されますが、パーティション数を知ることでパフォーマンスに関する洞察が得られることがあります。Although Cosmos DB automatically handles the sharding of physical partitions, knowing the partition count can give insight into performance. 開発チームは、最適化を続けるために後でこの情報を使用します。The development team will use this information later, as they continue to optimize. 青色の線が紫色の水平線を突破している箇所では、RU の消費が RU のプロビジョニング数を超えています。Where the blue line crosses the purple horizontal line, RU consumption has exceeded the provisioned RUs. ここが、Cosmos DB が呼び出しの帯域幅調整を開始するポイントです。That's the point where Cosmos DB will begin to throttle calls.

テスト 2:リソース単位を増やすTest 2: Increase resource units

2 番目のロード テストでは、チームは Cosmos DB コレクションを 900 RU から 2500 RU にスケールアウトしました。For the second load test, the team scaled out the Cosmos DB collection from 900 RU to 2500 RU. スループットは 19 要求/秒から 23 要求/秒に上昇し、平均待機時間は 669 ミリ秒から 569 ミリ秒に短縮されました。Throughput increased from 19 requests/second to 23 requests/second, and average latency dropped from 669 ms to 569 ms.

メトリックMetric テスト 1Test 1 テスト 2Test 2
スループット (要求/秒)Throughput (req/sec) 1919 2323
平均待機時間 (ミリ秒)Average latency (ms) 669669 569569
成功した要求Successful requests 9.8 K9.8 K 11 K11 K

劇的な向上ではありませんが、経時グラフを見ると、さらに全体像がはっきりします。These aren't huge gains, but looking at the graph over time shows a more complete picture:

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

前のテストでは最初の急上昇の後に急降下が見られましたが、このテストでは前よりもスループットが安定しています。Whereas the previous test showed an initial spike followed by a sharp drop, this test shows more consistent throughput. ただし、最大スループットはそれほど上がっていません。However, the maximum throughput is not significantly higher.

Cosmos DB に対するすべての要求は 2xx ステータスを返すようになり、HTTP 429 エラーはなくなりました。All requests to Cosmos DB returned a 2xx status, and the HTTP 429 errors went away:

Cosmos DB 呼び出しのグラフ

RU の消費とプロビジョニング数の対比グラフは、十分な余裕を示しています。The graph of RU consumption versus provisioned RUs shows there is plenty of headroom. およそ 275 という物理パーティションあたりの RU 数に対し、ロード テストでの 1 秒あたりの RU 消費は 100 前後がピークとなっています。There are about 275 RUs per physical partition, and the load test peaked at about 100 RUs consumed per second.

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

もう 1 つの興味深いメトリックは、操作成功あたりの Cosmos DB の呼び出し数です。Another interesting metric is the number of calls to Cosmos DB per successful operation:

メトリックMetric テスト 1Test 1 テスト 2Test 2
操作あたりの呼び出し数Calls per operation 1111 99

エラーがないと仮定すれば、呼び出し数は実際のクエリ プランと一致するはずです。Assuming no errors, the number of calls should match the actual query plan. この場合、操作は 9 つの物理パーティションすべてにヒットするクロスパーティション クエリを伴っています。In this case, the operation involves a cross-partition query that hits all nine physical partitions. 最初のロード テストの高い値は、429 エラーを返した呼び出しの数を反映しています。The higher value in the first load test reflects the number of calls that returned a 429 error.

このメトリックは、カスタムの Log Analytics クエリを実行して算出されました。This metric was calculated by running a custom Log Analytics query:

let start=datetime("2019-06-18T20:59:00.000Z");
let end=datetime("2019-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 番目のロード テストは改善を示しています。To summarize, the second load test shows improvement. しかし、GetDroneUtilization 操作はまだ、次に遅い操作に比べて桁違いの時間を要しています。However, the GetDroneUtilization operation still takes about an order of magnitude longer than the next-slowest operation. エンドツーエンドのトランザクションを見れば、その理由の説明に役立ちます。Looking at the end-to-end transactions helps to explain why:

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

前述したように、GetDroneUtilization 操作は Cosmos DB に対するクロスパーティション クエリを伴います。As mentioned earlier, the GetDroneUtilization operation involves a cross-partition query to Cosmos DB. これは、Cosmos DB クライアントがクエリを各物理パーティションに散開して結果を収集する必要があることを意味します。This means the Cosmos DB client has to fan out the query to each physical partition and collect the results. エンドツーエンドのトランザクションのビューが示すように、これらのクエリは直列で実行されています。As the end-to-end transaction view shows, these queries are being performed in serial. 操作にはすべてのクエリの合計と同じだけの時間がかかります。データのサイズが大きくなり、物理パーティションがさらに追加されるのに伴い、この問題は悪化の一途をたどります。The operation takes as long as the sum of all the queries — and this problem will only get worse as the size of the data grows and more physical partitions are added.

テスト 3:並列クエリTest 3: Parallel queries

以上の結果を踏まえれば、待機時間を縮める明白な方法は、クエリを並列で発行することです。Based on the previous results, an obvious way to reduce latency is to issue the queries in parallel. Cosmos DB クライアント SDK には、並列処理の最大範囲を制御する設定があります。The Cosmos DB client SDK has a setting that controls the maximum degree of parallelism.

Value 説明Description
00 並列処理なし (既定)No parallelism (default)
> 0> 0 最大限の並列呼び出しMaximum number of parallel calls
-1-1 クライアント SDK が並列処理の最適な範囲を選択するThe client SDK selects an optimal degree of parallelism

3 番目のロード テストでは、この設定が 0 から -1 に変更されました。For the third load test, this setting was changed from 0 to -1. 結果を次の表にまとめています。The following table summarizes the results:

メトリックMetric テスト 1Test 1 テスト 2Test 2 テスト 3Test 3
スループット (要求/秒)Throughput (req/sec) 1919 2323 4242
平均待機時間 (ミリ秒)Average latency (ms) 669669 569569 215215
成功した要求Successful requests 9.8 K9.8 K 11 K11 K 20 K20 K
スロットルされた要求Throttled requests 2.72 K2.72 K 00 00

ロード テストのグラフを見ると、全体的なスループット (オレンジ色の線) が大きく上昇しているだけでなく、スループットと負荷 (紫色の線) の変化が同じようなペースになっています。From the load test graph, not only is the overall throughput much higher (the orange line), but throughput also keeps pace with the load (the purple line).

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

エンドツーエンドのトランザクションのビューを見ると、Cosmos DB クライアントが並列でクエリを実行していることが確認できます。We can verify that the Cosmos DB client is making queries in parallel by looking at the end-to-end transaction view:

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

興味深いことに、スループット上昇の副作用として、1 秒あたりの RU 消費数も増加しています。Interestingly, a side effect of increasing the throughput is that the number of RUs consumed per second also increases. このテスト中、Cosmos DB は要求の帯域幅調整を行いませんでしたが、消費はプロビジョニングされた RU 制限に近い値になりました。Although Cosmos DB did not throttle any requests during this test, the consumption was close to the provisioned RU limit:

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

このグラフは、データベースをさらにスケールアウトする契機になる場合があります。This graph might be a signal to further scale out the database. ただし、代わりにクエリを最適化できることがわかりました。However, it turns out that we can optimize the query instead.

手順 4:クエリを最適化するStep 4: Optimize the query

前のロード テストは、待機時間とスループットの面でパフォーマンスの向上を示しました。The previous load test showed better performance in terms of latency and throughput. 要求の平均待機時間は 68% 短縮され、スループットは 220% 上昇しました。Average request latency was reduced by 68% and throughput increased 220%. ただし、クロスパーティション クエリは懸念事項です。However, the cross-partition query is a concern.

クロスパーティション クエリの問題は、すべてのパーティションにわたって RU のコストが発生することです。The problem with cross-partition queries is that you pay for RU across every partition. クエリがたまに (たとえば、1 時間に 1 回) しか実行されない場合、そのことは問題になりません。If the query is only run occasionally — say, once an hour — it might not matter. ただし、クロスパーティション クエリを伴った、読み取りが多いワークロードがある場合は常に、パーティション キーを含めることによってクエリを最適化できるかどうか試してみることを推奨します。But whenever you see a read-heavy workload that involves a cross-partition query, you should see whether the query can be optimized by including a partition key. (異なるパーティション キーを使用するために、コレクションの再設計が必要になる場合があります。)(You might need to redesign the collection to use a different partition key.)

この特定のシナリオのクエリを次に示します。Here's the query for this particular scenario:

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

このクエリは、特定の所有者 ID と月/年に一致するレコードを選択します。This query selects records that match a particular owner ID and month/year. 元の設計では、これらのプロパティはどれもパーティション キーではありません。In the original design, none of these properties is the partition key. そのため、クライアントはクエリを各物理パーティションに散開して結果を収集する必要があります。That requires the client to fan out the query to each physical partition and gather the results. クエリのパフォーマンスを改善するために、開発チームは、所有者 ID がコレクションのパーティション キーになるように設計を変更しました。To improve query performance, the development team changed the design so that owner ID is the partition key for the collection. そうすれば、クエリは特定の物理パーティションをターゲットにできます。That way, the query can target a specific physical partition. (Cosmos DB はこれを自動的に処理します。パーティション キーの値と物理パーティションとの間のマッピングを人間が管理する必要はありません。)(Cosmos DB handles this automatically; you don't have to manage the mapping between partition key values and physical partitions.)

コレクションを新しいパーティション キーに切り替えた後、RU 消費に劇的な改善がみられ、これがコストの削減に直結します。After switching the collection to the new partition key, there was a dramatic improvement in RU consumption, which translates directly into lower costs.

メトリックMetric テスト 1Test 1 テスト 2Test 2 テスト 3Test 3 テスト 4Test 4
操作あたりの RU 数RUs per operation 2929 2929 2929 3.43.4
操作あたりの呼び出し数Calls per operation 1111 99 1010 11

エンドツーエンドのトランザクションのビューは、予測どおり、クエリが 1 つの物理パーティションのみを読み取ることを示しています。The end-to-end transaction view shows that as predicted, the query reads only one physical partition:

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

ロード テストは、スループットと待機時間の改善を示しています。The load test shows improved throughput and latency:

メトリックMetric テスト 1Test 1 テスト 2Test 2 テスト 3Test 3 テスト 4Test 4
スループット (要求/秒)Throughput (req/sec) 1919 2323 4242 5959
平均待機時間 (ミリ秒)Average latency (ms) 669669 569569 215215 176176
成功した要求Successful requests 9.8 K9.8 K 11 K11 K 20 K20 K 29 K29 K
スロットルされた要求Throttled requests 2.72 K2.72 K 00 00 00

パフォーマンス改善の結果、ノードの CPU 使用率が非常に高くなります。A consequence of the improved performance is that node CPU utilization becomes very high:

AKS ノード使用率のグラフ

ロード テストの終了に向けて、平均 CPU は約 90% に達し、最大 CPU は 100% に達しました。Toward the end of the load test, average CPU reached about 90%, and maximum CPU reached 100%. このメトリックは、CPU がシステムの次のボトルネックであることを示しています。This metric indicates that CPU is the next bottleneck in the system. より高いスループットが必要な場合、次のステップは、より多くのインスタンスに Delivery サービスをスケールアウトすることかもしれません。If higher throughput is needed, the next step might be scaling out the Delivery service to more instances.

まとめSummary

このシナリオでは、次のボトルネックが特定されました。For this scenario, the following bottlenecks were identified:

  • RU のプロビジョニング不足が原因で Cosmos DB が要求の帯域幅を調整した。Cosmos DB throttling requests due to insufficient RUs provisioned.
  • 複数のデータベース パーティションを並列でクエリすることが原因で待機時間が長くなった。High latency caused by querying multiple database partitions in serial.
  • クエリにパーティション キーが含まれないためクロスパーティション クエリが非効率的だった。Inefficient cross-partition query, because the query did not include the partition key.

さらに、より大規模な環境では CPU 使用率が潜在的なボトルネックになることが特定されました。In addition, CPU utilization was identified as a potential bottleneck at higher scale. これらの問題を診断するために、開発チームは次のことに注目しました。To diagnose these issues, the development team looked at:

  • ロード テストでの待機時間とスループット。Latency and throughput from the load test.
  • Cosmos DB のエラーと RU 消費。Cosmos DB errors and RU consumption.
  • Application Insight のエンドツーエンドのトランザクションのビュー。The end-to-end transaction view in Application Insight.
  • コンテナーに対する Azure Monitor での CPU およびメモリ使用率。CPU and memory utilization in Azure Monitor for containers.

次のステップNext steps

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