負荷試験ツールvegetaを使ってみた

はじめに

SREグループ・ヒロチカです。GO株式会社では、サービスのクラウドインフラの設計から構築・運用までを担当しています。 今回、高トラフィックが予想されるアプリケーションに対して負荷試験を実施するにあたり、軽量に負荷をかけられるツールを試してみた中で、Golangベースでかつお手軽に負荷をかけられるツール「vegeta」が便利だったため、こちらの記事で紹介したいと思います。

負荷試験ツール vegeta

今回利用した「vegeta」は、Golangで書かれているCLIで実行可能なシンプルな負荷試験ツールです。

公式リポジトリ: vegeta

説明文を読むとHTTPサービスに対して一定のリクエストレートで負荷リクエストを投げることを目的として開発され、CLIでの利用だけでなくライブラリとしても利用できるとのことでした。

他にも数多ある負荷試験ツールの中で、自分がvegetaを採用したのは主に下記の理由からです。

現在、弊社サービスの多くはGolangで開発されており他言語でできた負荷試験ツールよりもとっつきやすかった点と、仮にバイナリ以外のインストールで環境を準備する必要が出てきた場合にも対応しやすいかなと考えた点。 また、万が一ライブラリとして利用する可能性も残したいと思いました。

  • CLI + パラメータでサクッと

負荷試験ツールの中にはテストケースを細かくコードやらyamlやらで書いてビルドして・・・と準備に時間がかかるものなどがありますが、今回の負荷試験でそれらを使うには少し工数がかかりすぎてしまうと感じ、インストールした後によくある負荷パラメータや想定リクエストのAPIリストなどを準備するだけで負荷を掛けられそうだった点。

今回、負荷をかけたいサービスの想定リクエスト数がかなりのトラフィック量であったため、負荷試験ツールを動かすリソースも相当数必要だと考えていました。 この負荷をかける視点でみた時にいくつかのツールを試したところ、vegetaはGolangベースで書かれているためか挙動がかなり安定的で、相当数のリクエストを投げてもリソースの消費のされ方が許容範囲で働いてくれそうでした。

負荷試験にあたり過去利用したツールもいくつか試してみましたが、軽量に動かすために事前準備に時間がかかりそうなものや、逆にお手軽で軽量故にリクエストが安定しないものなどがある中、個人的にvegetaはそのバランスが非常に良いと思いました。 今回、負荷試験ツールをインストールしたk8sのpodに対して、ガッツリCPUやメモリのリソースのスペックを与えて、何十何百podを並列で・・・といった事をせずに済んだのは、vegetaのおかげだったと思います。

導入

インストール手順も公式ドキュメントの記載通りです。

今回、負荷試験ツールを動かす環境は、適当なk8s環境にpodを立てそこにvegetaをインストールし、リクエスト先APIのデータを渡すなどして利用しました。 また動作確認などするためのローカルのMacでの環境については、homebrewでinstallしました。

基本動作・パラメータ

基本的なリクエスト送信方法は下記になります。 vegetaというツール名の割にパラメータの内容は一般的な負荷試験ツールのものと似ていて直感的にもわかりやすいです。

よく使うであろうパラメータは下記になります。

rate ... 秒間のリクエスト数。いわゆるRPS(request/second)相当。default:50

duration ... リクエストを投げ続ける時間。default:0(投げ続ける)

targets ... リクエスト先URLのリストを入れたファイルを指定する

100r/sを60秒間targets.txtファイルにあるリクエスト先に投げ続けるサンプル

vegeta attack -rate=100 -duration=60s -targets=targets.txt > result.bin | vegeta report

リクエスト先URLについてはコマンドライン上で直接指定も可能です。

echo "GET https://example.com/" | vegeta attack -rate=10 -duration=20s | tee result.bin | vegeta report

target.txtの例

  • GET or POST のリクエストメソッドの後にURLを記載するのが基本形
  • header情報などを複数行にかけて記載することも可能
  • 同一ファイル内にGETやPOSTが混ざっていても、異なるURLでも、記載可能
  • POST時のRequest Bodyはファイルに記載して指定できる(先頭行@で指定)

GETの例

GET https://example.com/test
GET https://example.com/foo/ver?key=value
・・・

POSTの例

POST https://example.com/foo/ver
@path/to/sample.json
POST https://example.com/mee/moo
Content-Type: application/json
Authorization: Bearer xxxxxx
@path/to/test.json
・・・

path/to/sample.jsonの例

{
  "key": "value"
}

実行結果(vegeta report)の例

vegetaでは負荷リクエストの実行完了後の結果をサマリしてresult.bin(ファイル名は任意)に入れるのが作法となっています。 その出力フォーマットは-typeのパラメータで指定できjsonhist(ヒストグラム)などがあり、デフォルトではtextとなります。 そのresult.binの内容をvegeta reportすることでcli上で結果表示ができます。

text形式での実行結果

echo "GET https://sample-site/test/" | vegeta attack -rate=5 -duration=3s | tee result.bin | vegeta report
Requests      [total, rate, throughput]         15, 5.36, 5.18
Duration      [total, attack, wait]             2.898s, 2.799s, 98.833ms
Latencies     [min, mean, 50, 90, 95, 99, max]  97.862ms, 375.282ms, 100.312ms, 1.138s, 1.144s, 1.146s, 1.146s
Bytes In      [total, mean]                     4987890, 332526.00
Bytes Out     [total, mean]                     0, 0.00
Success       [ratio]                           100.00%
Status Codes  [code:count]                      200:15
Error Set:

上記、お試しでリクエストした5r/sで3秒間のリクエストに対して、Requeststotalがちょうど5*3=15、Durationtotalで約3秒程度と比較的正確に出ており、レイテンシ、送受信のbyte数なども確認できます。

なおこのresult.binは、htmlに変換しグラフ表示もできたりします。

cat result.bin | vegeta plot > result_plot.html

負荷試験

試しに既存GKEに立てた1つのpodから、test-target.txtに指定したURL群に2500r/sを30秒間流してみます。

vegeta attack -targets=data/test-target.txt -rate=2500 -duration=30s -header="accept: application/json" -header="Authorization:Basic XXX" > result.bin

test-target.txt(全50行程度)

GET https://sample-site.hoge.com/foo/api1?key=AAA&key=BBB
GET https://sample-site.hoge.com/foo/api2?key=1111
GET https://sample-site.hoge.com/foo/api3?key=XXX&key=YYY
POST https://sample-site.hoge.com/foo/api4
@/path/to/test.json
GET https://sample-site.hoge.com/foo/api5?voo=doo
・・・

実行結果はこちらになります。

$ vegeta report result.bin
Requests      [total, rate, throughput]         75000, 2500.08, 2499.90
Duration      [total, attack, wait]             30.001s, 29.999s, 2.185ms
Latencies     [min, mean, 50, 90, 95, 99, max]  1.187ms, 3.741ms, 2.478ms, 3.886ms, 5.004ms, 36.891ms, 234.614ms
Bytes In      [total, mean]                     3965625, 52.88
Bytes Out     [total, mean]                     0, 0.00
Success       [ratio]                           100.00%
Status Codes  [code:count]                      200:75000

APIとしては全体的に軽めのものを選んだものの、さらっと2500r/sを30秒間、指定した通りのrateを維持し続けなながら綺麗にリクエストしてくれるのはかなり優秀なツールだと感じます。

負荷試験ツールの環境はGCPのコンソールから適当に起動しただけのpodだったのですが、上記のような負荷試験リクエストを何度か試していた期間のリソースをみても余裕そうで多少のリソース調整する程度で対応できそうです。 (cpu単位はmCPU,mem単位はMiB)

負荷試験の安定度

実際に行った高リクエスト想定のアプリケーションに対する負荷試験の実行内容と結果を少し紹介できればと思います。

試験内容としては、GET,POSTが入り混じった全20前後のAPIに対して、実際に想定されるAPIごとのリクエスト数の比率を考慮した形で複数台podから投げています。 各target_url_0x.txtに記載されているアクセス先APIのエンドポイントは、r/sの量でジャンル分けしそれぞれ約1~10万通りほどのパターン行が入っています。 前項のサンプルで投げた負荷試験では2500r/sも余裕で投げられましたが、リクエスト比率の調整をtargetファイルの中で編集して行うのが大変なため、リクエストするAPIをr/s量ごとにジャンル分けしリクエスト数を増やしたいAPIがあればpodをスケールする形で負荷リクエスト量を調整していました。 加えて、result.binの結果だけではどのAPIがどの程度重たかったのかがわかりづらいため、APIはある程度分けた形でリクエストを流すほうが性能が出ていないAPIの確認が容易になります。 ただし、result.binを結合するなどはできないのでそれぞれの結果を確認する必要があります。

想定されるリクエスト比率を加味し合計で9000r/s以上を30分間流すことを目安に試験します。

urlリスト 想定r/s 1Pod当たりから流すr/s Pod数 合計秒間リクエスト数
target_url_01 非常に多い 1250 4 5000
target_url_02 多い 1500 2 3000
target_url_03 普通 350 3 1050
target_url_04 少ない 25 1 25
target_url_05 非常に少ない 1 1 1
  • 実際のリクエストコマンド
# 秒間リクエスト量が非常に多いAPIリスト x 4pod
vegeta attack -targets=data/target_url_01.txt -rate=1250 -duration=1800s -header="accept: application/json"  -header="Authorization:Basic XXX" > result.bin
# 秒間リクエスト量が多いAPIリスト x 2pod
vegeta attack -targets=data/target_url_02.txt -rate=1500 -duration=1800s -header="accept: application/json"  -header="Authorization:Basic XXX" > result.bin
# 秒間リクエスト量が普通程度のAPIリスト x 3pod
vegeta attack -targets=data/target_url_03.txt -rate=350 -duration=1800s -header="accept: application/json"  -header="Authorization:Basic XXX" > result.bin
# 秒間リクエスト量が少ないAPIリスト x 1pod
vegeta attack -targets=data/target_url_04.txt -rate=25 -duration=1800s -header="accept: application/json"  -header="Authorization:Basic XXX" > result.bin
# 秒間リクエスト量が非常に少ないAPIリスト x 1pod
vegeta attack -targets=data/target_url_05.txt -rate=1 -duration=1800s -header="accept: application/json"  -header="Authorization:Basic XXX" > result.bin
  • 弊社のモニタリング環境(Grafana)環境での実行結果

リクエストを投げ続けている30分間、安定して約9000r/s流れていることがわかります。 この後、負荷試験ではさらに、12000r/s,15000r/sと投げるなどしましたが負荷試験ツール側としては問題なく動いてくれました。

気になった点など

リクエスト先URLのリストが入っているtargetファイルの読み込みはランダムではなく上から順番に読み込んでいきます。 毎試験ごとに色々なURLに対しランダムに投げたい要件がある場合はtargetファイルの行を入れ替える等の対応が必要です。 その際、1行単位のシャッフルではなく、複数行に渡って記載があるPOSTリクエストなども考慮する必要があります。

またお手軽で軽量だと推してきましたが、軽量に動いている理由の一つはvegetaがメモリを上手に使って動いている点だと思われます。 負荷試験ツール側のリソースを観察すると、基本のレイテンシーが高めのAPIへの試験内容ではCPUよりもメモリの消費変動が大きく変わっているようでした。 加えてvegetaコマンド実行時、result.binに集計して吐き出すまでの情報についてもメモリ上に保存していくようでした。 そのため、負荷をかける際のAPIリクエストのRequest Bodyがあまりに大量であったり、あまりに長時間投げ続けるようなパラメータを指定した場合にはOutOfMemoryやpod自体が固まるケースもありました。 安定的なツールとはいえ、負荷試験のケースに合わせて無理させすぎないようなパラメータ指定は考慮する必要があります。

今回の負荷試験では、大量の単一リクエストを定量的に送り続けることに焦点を当てて選定しましたが、セッション管理やファイルのダウンロード・アップロードなどには対応していないため、ユーザのシナリオテストをするような負荷試験は難しいです。 こういった負荷試験を実施したい場合はk6やlocust、Apache JMeterなどの負荷シナリオに対応したツールを利用するのが良いかと思いますが、公式の紹介文にあるようにvegetaをCLIツールとしてではなくライブラリとしてGolangに組み込むことが可能なため、自前実装で負荷シナリオ機能を実現することも可能です。

念の為にはなりますが、負荷試験ツールからのリクエストを投げる際には通信経路の帯域、リクエスト先URLには十分気をつけしましょう。

終わりに

今回、負荷試験ツール「vegeta」を紹介しました。 cliで簡単に負荷試験ができてしまうので、ちょっとした通信確認以上で使いたいツールとして優秀だと思います。 REST APIに対する負荷試験ツールとして、まず最初に使いたいツールです。 比較的知られてきているツールだとは思いますが、負荷試験検討中の方でvegetaをまだ触ったことがない方などにこの記事が参考になれば幸いです。