PLAY DEVELOPERS BLOG

HuluやTVerなどの日本最大級の動画配信を支える株式会社PLAYが運営するテックブログです。

HuluやTVerなどの日本最大級の動画配信を支える株式会社PLAYが運営するテックブログです。

負荷試験ツール Artillery を使って秒間 1 万リクエスト以上の負荷をかける

こんにちは、メディアプラットフォーム事業部の今雪です。

前回のPLAY DEVELOPERS BLOGでは負荷試験ツールLocustを紹介しました。 今回は当社で併用している負荷試験ツールArtilleryについての紹介と環境構築、その使い方についてLocustと同じような流れで簡単にご紹介します。

最後に「Artillery」と「Locust」両ツールの使い分けについて考察します。本記事が負荷試験ツールの導入の参考になれば嬉しいです。

本記事の内容はMacでの操作を想定しています。

負荷テストツールArtilleryについて

Artilleryはyamlファイルで宣言的にシナリオを作成し、負荷をかけることができるNode.js製の負荷テストツールです。 Artilleryは「砲兵団」や「大砲」という意味持つそうです。かっこいいですね。 特徴としては下記のことがあげられます。

  • yamlでテストシナリオが書けるので学習コストが低い
  • テストの進行状況をリアルタイムで表示するCUI
  • 手間いらずのクラウドを使った負荷テスト

特に「手間いらずのクラウドを使った負荷テスト」という点ですが、コマンドひとつでAWS Lambdaを使ってある程度の負荷をかけられるので重宝しています。

Artilleryのインストール

下記の手順に沿ってローカルの環境を構築してみましょう。

本記事ではNode.js v17.6.0で行います。

  1. Node.jsのバージョン確認

     % node -v
     v17.6.0
    
  2. Artilleryをインストール

     % npm install -g artillery@latest
    
  3. Artilleryのバージョン確認

     % artillery -v
    
             ___         __  _ ____
       _____/   |  _____/ /_(_) / /__  _______  __ ___
      /____/ /| | / ___/ __/ / / / _ \/ ___/ / / /____/
     /____/ ___ |/ /  / /_/ / / /  __/ /  / /_/ /____/
         /_/  |_/_/   \__/_/_/_/\___/_/   \__  /
                                         /____/
    
    
     VERSION INFO:
    
     Artillery: 2.0.0-29
     Node.js:   v17.6.0
     OS:        darwin
    

これでインストールは完了です。

ローカルでArtilleryを負荷試験を実行してみよう

下記のようにartilleryというフォルダ配下にscript.yamlというファイルを作成しましょう。

./artillery
└── script.yaml

script.yamlには下記を記述しておいてください。 targetの部分とurlの部分にはdummyAPIの関数URLを入れてください。 今回はtargetに前回の記事で作成したAPIと同じものを作って設定します。

script.yaml

# dummyAPI
config:
  target: dummyAPIの関数URL
  phases:
    - duration: 120
      arrivalRate: 5
      rampTo: 100
      name: Ramp up load

scenarios:
  - name: "dummyAPI"
    flow:
      - get:
          url: dummyAPIの関数URL

上記のyamlは以下のシナリオになっています。

  • シナリオの期間は120秒。
  • 5人のユーザーからスタートし、最大100人のユーザーになるまで1秒ごとにユーザーを増やす。
  • シナリオの期間が終わるまで指定したURLにGETリクエストを行い続ける。

./artilleryの配下でArtilleryを実行してみましょう。

% artillery run script.yaml
--------------------------------------
Metrics for period to: 19:39:40(+0900) (width: 0.298s)
--------------------------------------

http.codes.200: ................................ 5
http.request_rate: ............................. 5/sec
http.requests: ................................. 5
http.response_time:
  min: ......................................... 22
  max: ......................................... 134
  median: ...................................... 58.6
  p95: ......................................... 98.5
  p99: ......................................... 98.5
http.responses: ................................ 5
vusers.completed: .............................. 5
vusers.created: ................................ 5
vusers.created_by_name.dummyAPI: ............... 5
vusers.failed: ................................. 0
vusers.session_length:
  min: ......................................... 55.4
  max: ......................................... 238.4
  median: ...................................... 94.6
  p95: ......................................... 153
  p99: ......................................... 153

上記のようなレポートが出力できていれば、Artilleryを起動できています。 Artilleryは、テストの実行中に10秒ごとにコンソールにレポートを出力します。 120秒間のレポートを見ていきましょう。

負荷テストの結果を見てみよう

--------------------------------
Summary report @ 19:41:41(+0900)
--------------------------------

errors.ENOTFOUND: .............................. 2
http.codes.200: ................................ 6298
http.request_rate: ............................. 52/sec
http.requests: ................................. 6300
http.response_time:
  min: ......................................... 9
  max: ......................................... 298
  median: ...................................... 19.1
  p95: ......................................... 29.1
  p99: ......................................... 58.6
http.responses: ................................ 6298
vusers.completed: .............................. 6298
vusers.created: ................................ 6300
vusers.created_by_name.dummyAPI: ............... 6300
vusers.failed: ................................. 2
vusers.session_length:
  min: ......................................... 28.2
  max: ......................................... 1299.7
  median: ...................................... 44.3
  p95: ......................................... 77.5
  p99: ......................................... 262.5

上記が120秒後に表示された最終的なレポートになります。
120秒間で6,300リクエストを行い、そのうちの2リクエストが失敗しました。
また http.request_rate の値を見ると平均で秒間52リクエストしていることがわかります。
失敗している2件のリクエストですが、errors.ENOTFOUNDが2件出ている事からlocalとLambdaとの通信で失敗したものが2件いたのではないかと思われます。

テストシナリオを構築してみよう

もう少し複雑なシナリオを構築してみます。script.yamlを下記のように書き換えましょう。

script.yaml

config:
  target: dummyAPIの関数URL
  phases:
    - duration: 180
      arrivalRate: 1
      rampTo: 10
      name: Ramp up load

# dummyAPIをユーザーはSpawnのタイミングで叩く
before:
    flow:
      - log: "dummyAPI"
      - post:
          url: dummyAPIの関数URL

# 継続的にdummyAPI2を叩く
scenarios:
  - name: "dummyAPI2"
    flow:
      - get:
          url: dummyAPI2の関数URL

上記のyamlは以下のシナリオになっています。

  • シナリオの期間は180秒。
  • 1人のユーザーからスタートし、最大10人のユーザになるまで1秒ごとに徐々にユーザーを増やす。
  • 初回はPOSTリクエストを行い、その後はシナリオの期間が終わるまでGETリクエストを行い続ける。

AWSでArtilleryを起動して、負荷をかけてみよう

では実行してみましょう。

# awsの環境が意図したところに向いてるか確認
% aws sts get-caller-identity
{
    "UserId": "xxx",
    "Account": "yyy",
    "Arn": "zzz"
}

% artillery run \
  --platform aws:lambda \
  --platform-opt region=ap-northeast-1 \
  --count 1 \
  script.yaml

コマンドを実行すると以下のようなログが出力されます。

λ Creating AWS Lambda function...
- Bundling test data
  - script.yaml
- Installing dependencies
- Creating zip package
Preparing AWS environment...
 - Lambda function: artilleryio-9625d23b-d456-4f47-a450-a40e5223e97e
 - Region: ap-northeast-1
 - AWS account: xxxxxxx
Phase started: Ramp up load (index: 0, duration: 180s) 21:00:33(+0900)

AWSのコンソールからLambdaを確認すると作成されていることがわかります。

AWSで秒間1万リクエスト以上の負荷をかけてみよう

以下のスクリプトを作成します。

script.yaml

config:
  target: dummyAPIの関数URL
  phases:
    - duration: 60
      arrivalRate: 100
      name: Ramp up load

before:
    flow:
      - log: "dummyAPI"
      - post:
          url: dummyAPIの関数URL

scenarios:
  - name: "dummyAPI2"
    flow:
      - get:
          url: dummyAPI2の関数URL

上記のyamlは以下のシナリオになっています。

  • シナリオの期間は60秒。
  • 100ユーザーがリクエストし続ける。
  • 初回はPOSTリクエストを行い、その後はシナリオの期間が終わるまでGETリクエストを行い続ける。

次に以下のコマンドを実行します。

% artillery run \
  --platform aws:lambda \
  --platform-opt region=ap-northeast-1 \
  --count 200 \
  script.yaml

--count 200のオプションをつけることで、上記のシナリオを実行するLambdaが200個同時に実行されます。
つまり、100ユーザー * 200 で秒間20,000リクエストできる想定です。

実行した結果は以下のようになりました。

All VUs finished. Total time: 1 minute, 13 seconds

--------------------------------
Summary report @ 18:51:14(+0900)
--------------------------------

http.codes.200: ................................ 553217
http.codes.429: ................................ 646783
http.request_rate: ............................. 19997/sec
http.requests: ................................. 1200000
http.response_time:
  min: ......................................... 0
  max: ......................................... 731
  median: ...................................... 7.9
  p95: ......................................... 22.9
  p99: ......................................... 27.9
http.responses: ................................ 1200000
vusers.completed: .............................. 1200000
vusers.created: ................................ 1200000
vusers.created_by_name.dummyAPI2: .............. 1200000
vusers.failed: ................................. 0
vusers.session_length:
  min: ......................................... 7.9
  max: ......................................... 1065.9
  median: ...................................... 22.4
  p95: ......................................... 38.5
  p99: ......................................... 47

Estimated AWS Lambda cost for this test: $1.344

60秒間で1,200,000リクエストを行い、1,200,000のレスポンスを受け取ることができています。
また http.request_rate の値を見ると平均で秒間19,997リクエストしていることがわかります。
受け取ったレスポンスのステータスコードを見ると、ステータス200で帰ってきているのが553,217件、ステータス429で帰ってきているのが646,783件でした。 これはターゲットとなっている負荷をかけられる側のLambdaで同時実行数の上限に達してスロットリングエラーが発生しているためでした。
シナリオでは期間を60秒と設定しておりましたが、テスト終了に1分13秒かかったのは全てのユーザーがシナリオを完了させるのに時間がかかったためです。
リクエストしてからレスポンスを受け取るまでの時間も含めるため、リクエスト数が多いとテスト終了までに時間がかかるようです(公式ドキュメントでも説明されております) 。

結果の最後にArtilleryが負荷をかけるために実行したAWS Lambdaの使用料金の予測が出ているので、AWSの難しい料金計算をしなくても良いので助かりますね。(※今回負荷をかけるターゲットにしているLambdaの使用料金は計算されていません。)

Estimated AWS Lambda cost for this test: $1.344

「Artillery」と「Locust」両ツールの使い分けを比較

- 負荷テストのモニタリングをCUIベースで行うか、GUIベースで行うか
    Artillery => CUIだけで完結したい
    Locust    => GUIでグラフでもモニタリングしたい

- 負荷テストの結果をどのように保存しておくか
    Artillery => CUIのレポートログだけで良い
    Locust    => 整形されたファイル形式で残しておきたい

- エラーした原因を負荷テストツール側でみれるかどうか
    Artillery => 見れない
    Locust    => 見れる

- 複雑なテストシナリオを書く際の向き不向き
    Artillery => 不向き。特にレスポンスを利用する際は煩雑になる
    Locust    => 向いている。Pythonで書くためプログラミングと同じ感覚で組める

- 数万リクエストの負荷試験をすぐに行いたい時
    Artillery => AWS上で実行する環境構築はコマンド一発でできるのでAWSの恩恵を受けて数万リクストはすぐに出せる
    Locust    => AWS上で実行する環境構築には別の技術のある程度の理解が必要

本記事では触れることはありませんでしたが、Artilleryは、継続的インテグレーション(CI)と自動化をサポートしているようです。 CI/CDパイプラインの一部としてステージング/機能環境に対してテストを実行して、パフォーマンスの低下を早期に発見し、SLOを検証することができるようです。

まとめ

今回はNode.js製の負荷試験ツール「Artillery」を使って
「ローカルでの負荷試験の実行方法」
「AWS上での負荷試験の実行方法」
「AWS上で秒間1万リクエスト以上の負荷試験をする」
を見てきました。

PLAY DEVELOPERS BLOG 2回に渡って「Locust」と「Artillery」、2つの負荷試験ツールの紹介と比較を行いました。 月並みではありますが、使用用途に合わせた負荷試験ツールの選定が重要になります。