😎

Azure Load Testingを使った負荷試験とサイジング検証

2024/01/29に公開

はじめに

サーバリソースのスペック検討を実施する機会に備えて、AzureのLoad Testingを使いつつサイジングもしてみた。

環境構成

Environment

App ServiceへのアプリリリースはAzure Pipelinesで実施している。

手順

App Serviceプラン

App Serviceのサイジングを行うため最小構成のBasic B1を選択。
スケールアウトは手動かつ、初期インスタンスは1つのままにする。

App Service

ランタイムはJava17
Application Insightsは有効化

Azure PostgreSQL フレキシブルサーバ

最小構成のStandard B1msを採用(1コア、メモリ2GB)

サンプルコード(Java)作成

Qiitaの記事を参考にSpring Bootで実装。
(ChatGPTに書かせてみたのだがエラー連発でアプリレイヤのトラシューも分からないので断念)

CI/CD構成 作成

Azure Pipelinesにて実装。
パイプラインの作成時にはApp Serviceまでのデプロイを考慮して以下を選択。

Maven packageの実行やApp Serviceへのデプロイを目的としたyamlテンプレートが作成される。

yamlの解読について

yaml全量
# Maven package Java project Web App to Linux on Azure
# Build your Java project and deploy it to Azure as a Linux web app
# Add steps that analyze code, save build artifacts, deploy, and more:
# https://docs.microsoft.com/azure/devops/pipelines/languages/java

trigger:
- main

variables:

  # Azure Resource Manager connection created during pipeline creation
  azureSubscription: '2f067e4f-a4fb-4399-984b-d9c61f289a5f'

  # Web app name
  webAppName: '<WEB APP NAME>'

  # Environment name
  environmentName: '<ENVIRONMENT NAME>'

  # Agent VM image name
  vmImageName: 'ubuntu-latest'

stages:
- stage: Build
  displayName: Build stage
  jobs:
  - job: MavenPackageAndPublishArtifacts
    displayName: Maven Package and Publish Artifacts
    pool:
      vmImage: $(vmImageName)

    steps:
    - task: Bash@3
      inputs:
        targetType: 'inline'
        script: |
          # Write your commands here
          
          java --version
    - task: JavaToolInstaller@0
      inputs:
        versionSpec: '17'
        jdkArchitectureOption: 'x64'
        jdkSourceOption: 'PreInstalled'
    - task: Maven@3
      displayName: 'Maven Package'
      inputs:
        mavenPomFile: './demo/pom.xml'
        publishJUnitResults: true
        testResultsFiles: '**/surefire-reports/TEST-*.xml'
        javaHomeOption: 'JDKVersion'
        mavenVersionOption: 'Default'
        mavenAuthenticateFeed: false
        effectivePomSkip: false
        sonarQubeRunAnalysis: false

    - task: CopyFiles@2
      displayName: 'Copy Files to artifact staging directory'
      inputs:
        SourceFolder: '$(System.DefaultWorkingDirectory)'
        Contents: '**/target/*.?(war|jar)'
        TargetFolder: $(Build.ArtifactStagingDirectory)

    - upload: $(Build.ArtifactStagingDirectory)
      artifact: drop
      displayName: upload

    - script: |
       echo $(Build.ArtifactStagingDirectory)

- stage: Deploy
  displayName: Deploy stage
  dependsOn: Build
  condition: succeeded()
  jobs:
  - deployment: DeployLinuxWebApp
    displayName: Deploy Linux Web App
    environment: $(environmentName)
    pool:
      vmImage: $(vmImageName)
    strategy:
      runOnce:
        deploy:
          steps:
          - script: |
             ls $(System.DefaultWorkingDirectory)
          - task: AzureWebApp@1
            displayName: 'Azure Web App Deploy: <WEB APP NAME>'
            inputs:
              azureSubscription: $(azureSubscription)
              appType: webAppLinux
              appName: $(webAppName)
              package: '$(Pipeline.Workspace)/drop/**/target/*.?(war|jar)'

ビルドしたパッケージを直接Artifactにプッシュするのではなく、Build.ArtifactStagingDirectoryというローカル領域を経由するようである。
そのため、System.DefaultWorkingDirectory→Build.ArtifactStagingDirectory→パイプライン成果物(drop)にパブリッシュという3段階の流れ。

Artifactにパブリッシュされた成果物は以下の画面から確認できる。パイプラインの成果物としてパブリッシュされている(利用範囲はパイプライン)ため、いわゆるAzure Artifactにパブリッシュされている訳ではないということが確認できる。

テンプレートではmvn packageを利用している。packageの場合は、コンパイル&ビルドまでする(デフォルトだとjar形式)。
mvn installとの違いは、作成後のjarをローカルリポジトリ(.m2)に配置するかどうか。配置することによって、他のローカルプロジェクトからの利用が可能になる。
packageの場合はtargetフォルダ配下に格納されるだけなので、別プロジェクトからの利用はできない。

App Serviceの場合は、一度リスタートしないと反映されなさそうである。

Application Gateway 作成

AppGWとApp Serviceの連携をするときはホストのオーバライドが必要になる。
ホスト名のオーバーライドが一番分かりやすい解説資料
todo:実際に開発者ツールでヘッダーを見てみたいところ。

Azure Load Testing 作成

URLベースのテストを作成するで実施。JMeterのスクリプトをアップロードしてテストする選択肢も存在する。
URLベースのテストの場合は、リクエスト先のURLと負荷のかけ方を設定できる。
tsetcreate1

  • 仮想ユーザの数
    最大の同時接続数。スレッド数のイメージ。
  • 増加時間(分)
    仮想ユーザの数に何分で到達するか。1分の場合は、テスト実行から徐々に増えていき1分後に仮想ユーザの数に到達する。

監視タブでは、サーバ側のメトリックも監視できる。例えばApp ServiceやApplication Insightsを追加できる。クライアント側のメトリックだけではない監視ができるのは良い点。作成したらテストの実行が始まる。

性能試験結果まとめ

初回の実行結果は以下の通り。
結果1
結果2

実行結果の見方の補足

90 パーセンタイルの応答時間とは、結果のうち合格ラインに含まれる90%の値を抽出したもの。残り10%を外れ値扱いとする。システム要件としては「90 パーセンタイルの応答時間が3秒以下である」といった使われ方がされるそう。
RPSやTPSといった用語も出てくるので、それは別途スクラップ記事を見る感じ。

結果の考察

同時リクエストが増えるにしたがって、レスポンスタイムの上昇と504エラーの発生が見受けられた。
一方で、DBやApp Serviceのリソース高騰は見受けられなかった。ということで、以下について追加考察をしてみる。

  • Application Gatewayのネットワークパフォーマンス
    以下の記事を参考に考察

https://learn.microsoft.com/ja-jp/azure/application-gateway/application-gateway-metrics#metrics-supported-by-application-gateway-v2-sku

appgawemetric

バックエンド先頭バイト応答時間に急増傾向が見られる一方、バックエンド接続時間が安定傾向を見せている場合、Application Gateway からバックエンドへの待機時間と、接続の確立にかかった時間が安定しており、急増はバックエンド アプリケーションの応答時間の増加が原因であると推測できます

引用分の事象が受けられているため、AppGwのパフォーマンスよりはバックエンド側の応答時間の増加が原因だと考えられる。

一方、バックエンド先頭バイト応答時間の急増がバックエンド接続時間に対応する急増に関連付けられている場合は、Application Gateway とバックエンドサーバーの間のネットワーク、またはバックエンド サーバーの TCP スタックが飽和状態になっていることが推測できます。

特に問題ない。

バックエンド最終バイト応答時間が急増しているにもかかわらず、バックエンド先頭バイト応答時間が安定している場合は、この急増は要求されているファイルが大きいことが原因であると推測できます

こちらも問題なし。(DBへの要求データ自体は大きなデータではない)

Application Gateway の合計時間が急増しているにもかかわらず、バックエンド最終バイト応答時間が安定している場合は、Application Gateway でのパフォーマンスのボトルネック、またはクライアントと Application Gateway 間のネットワークのボトルネックのいずれかの兆候である場合があります

Application Gateway の合計時間は緑ライン。一部の時間においては乖離が見られるため、本事象に該当しているとも考えられる。
要は、バックエンドからは最終応答が返ってきているのに、それをクライアントに返すまでに2秒くらいの差分があることが分かる。

クライアント RTT に対応する急増も見られる場合、この低下はクライアントと Application Gateway 間のネットワークが原因であることを示します。

クライアント RTTは紫ライン。低い位置で安定推移のため問題なし。

→ バックエンドのリソース利用の急騰も見られないことから、SQLの実行に時間がかかっているのではないかと推測。もしくは、IOPSに特化したプランを選択するかどうか。

  • SQLのスロークエリ(duration)
    Application Insightsのアプリケーションマップから確認したところApp ServiceからPostgreSQLの呼び出しに平均140msかかっている。少し遅いかなと思ったため、追加の分析をする。
    ApplicationMap

パフォーマンスから確認すると、半分以上のクエリが120ms以上かかっている。画面下の詳細の表示…から選択すると1つ1つの結果が確認できる。
Performance

e2etranzaction

→ DBのリソース(CPU・メモリ・ディスクIOPS)には異常がないので、インデックス作成で様子を見る。

  • インデックス作成後
    → 特に変化が見られなかった。Azure Load Testingの結果を比較することができる。
    lthikaku

  • DB側でのクエリパフォーマンスの確認
    queryperformance
    長いクエリでも100ミリ秒くらい。
    ちなみに、Log Analyticsワークスペースから、時間のかかっているSQLを抽出できる。

AzureDiagnostics
| where Category contains 'PostgreSQLLogs'
| extend duration_ms = todouble(split(split(Message, 'duration: ')[-1], ' ')[0])
| where duration_ms >= 100 ★適宜変更する★
| project TimeGenerated, duration_ms, Message

クエリ結果としては、100ms超えが10存在したくらいで、決してスロークエリが頻発しているわけでは無かった。
時間帯としてはLoad Testingの仮想ユーザ数が50に到達した直後が1番頻発していた。
slowquery

→ クエリ自体は問題なく完了しているけれどIOPSでやっぱりボトルネックになっているのかと推測

  • フレキシブルサーバのスケールアップ
    • ストレージのIOPSを120から240に変更:変化なし。そもそもIOPSは特に上限来てなかった。
    • リソース増強:Standard_B1msからStandard_D2s_v3に変化。改善は見受けられず。

→ App Serviceのリソース変化にもう一度着目してみることに。

  • App Serviceのリソース増強
    App Serviceのメトリックをちゃんと見ると、CPUに100%の張り付きが見られていた。
    appsmetric

そこで、以下の考え方に従って適切なCPUコア数を求めることにした。括弧内は例である。

1秒間のリクエスト数。(50rps)
1リクエストにどのくらいのCPU時間がかかっているか(0.1/s)
 → 1秒間で(10)リクエストがさばける。
50rpsのリクエストを捌くには**CPU5コア**必要になる。
  • 1秒間のリクエスト数
    Log Analyticsワークスペースからクエリを実施。
    以下のクエリはApplication Insightsのパフォーマンスから飛ぶことができる。
// 名前別の要求のカウント
let start=datetime("2024-01-27T15:25:00.000Z");
let end=datetime("2024-01-27T15:32:00.000Z");
let timeGrain=1s;
let dataset=requests
    // ここに追加のフィルターを適用できます
    | where timestamp > start and timestamp < end
    | where client_Type != "Browser"
;// フィルター処理された要求のセットを選択して、名前別にカウントする
dataset
| where ((operation_Name == "GET /"))// 別のプロパティにより、下の行の 'operation_Name' をセグメントに変更します
| summarize count_=sum(itemCount) by ["要求"]=operation_Name, bin(timestamp, timeGrain)
// 結果をグラフにレンダーする
| render timechart

実行の結果として22リクエスト/sが最大値として確認できた。

  • 1リクエストにどのくらいのCPU時間がかかっているか
    Application Insightsのパフォーマンスマップから確認を実施した。
    すると、138msが期間平均として確認できた。
    そのため、1コアで1秒間に7.3リクエストさばけるという計算になる。

  • 必要なCPUコア数
    以上の計算結果より22/7.3≒CPU3コアが必要であることが分かった。
    安全率を1.2として3.6コア、切り上げでCPU4コアのプランを選択することにした。

再試験

App Serviceのスケールアウトによる対応

プランは変更せずに、インスタンス数を3つにした。結果としては以下の通りで、リソース利用率は依然として高騰してままであった。

scaleout

インスタンスによって利用状況に違いがあるのかと思い、分割基準にinstanceを設定して確認。すると、2台は途中でデータ欠損してしまっている。スケールアウトしたインスタンスでうまく処理できていない可能性がある。
(ログアナから処理しているインスタンスが見られるか確認したい。)
scaleoutperins

再実行すると3インスタンスで処理されるようになったが、それでもリソースとしては足りていない感じ。
retrybasicinstance3

正常の時は若干の改善がみられる。青いほうが正常実行で赤いほうがデータ欠損になったテスト。
ldtest_retrybasicinstance3

いずれにしろ、Basic B1だと足りないため、Standard S1プランとPremium v3 P0V3で実行してみた。どちらもCPUコア数としては1だが、ACUという値が変わっている。

プラン名 ACU/vCPU vCPU Memory(GB) Remote Storage(GB) Scale(instance) SLA
Standard S1 100 1 1.75 50 10 99.95%
Premium v3 P0V3 195 1 4 250 30 99.95%

Load Testingの結果としては以下の通り。赤がPremiumで青がStandardプランである。ともに、インスタンス数を4つにしている。
ltresultpremiumstandardhikaku

  • StandardのCPUリソース状況
    standardresource

  • PremiumのCPUリソース状況
    premiumresource

→ Premiumプランの方が大幅な性能改善ができていることが分かる。ちなみに、Standard S1のインスタンス数を3つでスケールアウトしたときには、若干の改善は見られた程度である。結果は割愛する。

App Serviceのスケールアップによる対応

Premium v3 P2V3(CPU 4コア・メモリ16GB)に変更した。インスタンス数は1つである。再テストの結果応答時間に改善が見られた。CPUの推移としても改善している。
scaleup

スループットとしても改善がみられている。(当たり前か…)
もしかしたらCPUがマルチスレッド対応のため、想定以上のパフォーマンスが出ている可能性がある。
scaleupretest

参考資料

  • 要求数(RPS)の考え方について

https://learn.microsoft.com/ja-jp/azure/load-testing/concept-load-testing-concepts

まとめと所感

CPUにボトルネックがあることが分かった。サイジングの結果、CPU4コアが必要であるという結論まで導くことができた。
matome

そこまでは今までの知見でも分かることだが、「では具体的なスペックは何にするればいいのか」まで踏み込んで調査できたのがネクストステップだった。
Azure Load Testingやそれ以外のApplication Insightsや各種メトリックを使った考察を実施できたのはよい学習であった。
Application Insightsは性能分析では重宝されることが分かったので必須で導入していきたい。

後日談

CPUについて調べる中で、「もしかしてApp ServiceのPremium v3ってマルチスレッド対応しているのでは?」
そうであれば、4コアじゃなくても2コア2スレッドでも処理として出るのはと推測。
P1mv3 (2 vCPU、16 GiB メモリ)で実施してみた。実際にマルチスレッド対応しているかはドキュメントから確認できなかったが、ACUが195と通常のBasicプランの100と比較すると2倍近くのため対応している可能性はある。

その時の結果が以下の通り。青ライン該当の試験だが、2コアでも十分改善している。スループットとしても期待した22が出ている。
今回のようにDBからselectして画面表示するような簡単なアプリであればマルチスレッド化が効いている気がした。
P1mv3

CPUの使用状況は以下の通り。100%張り付きだが、前述の通り元のテスト設定が結構な過負荷試験のためである。
scaleup2core1ins

TODO

  1. SQLとスレッド(CPU)の関係性。
  2. DBのサイジングの考え方。
    → これまでの流れと類似する部分が多いと多いと思う。
  3. マルチスレッドと処理時間の関連性。
    → スクラップ参照
  4. メモリのサイジング方法

Discussion