RareJob Tech Blog

レアジョブのエンジニア・デザイナーによる技術ブログです

Vegetaライブラリを使ってGoでちょっとこだわった負荷試験シナリオを作る

こんにちは!プラットフォームチームの池田です。2回目の投稿になります。

元々ハンバーガーが好きで社内でもそれで自分を売り出していたのですが、つい最近とんでもない対抗馬と出会ってしまいました。そう、バインミーです(今更?)。

時代と文化が生んだ奇跡。私はハマってしまいました。バインミータベタイ

閑話休題、直近で負荷試験を実施する機会があり、それに関するトピックを紹介します。

はじめに

私が所属するチーム(プラットフォームチーム)ではGo言語をメインに開発していることもあり、負荷試験においてもGoでカスタマイズできるツールを探していました。特に気になったのがGo言語で開発されているOSSVegetaのライブラリでの利用方法でして、今回試してみました。

Vegetaライブラリを使った動的なテストシナリオの方法を紹介した記事はあまり見受けられず、ベストな選択肢では無いと承知しながらも、この記事を書いてみようと思いました。

【目次】

Vegetaとは?

安定した性能を発揮する多機能なHTTP負荷テストツールです。

GitHubのREADMEでは名前の由来である"彼"の姿を見ることできます。

Vegetaツール自体は古き良きApache HTTP server benchmarking tool (ab)と同様のCLIベースのツールです。

abと比較して、HTTP/2対応、分散攻撃機能、結果プロッティング機能などの付加的な特徴があります。(本記事ではこれらの機能には言及しません。)

Vegetaライブラリを利用するメリット

Vegetaライブラリで負荷試験シナリオを作るメリットは以下としています。

  • Goをメインに開発しているチームにとってスムーズに作成と保守ができる
  • Goの特徴であるシンプルなクロスコンパイルにより、どの環境にもシングルバイナリとして簡単に乗せることができる

逆にデメリットは以下になります。

  • JMeterGatlingLocustなど他の負荷テストツールと比較するとコードが複雑になり可読性が良くない

今回のシナリオの想定

プラットフォームチームが保持するマイクロサービスの1つである会員基盤サービス(REST API) に対する負荷試験という前提で進めます。

本記事では、弊社サービスにおいてシンプルだけれどもアクセスピークがクリティカルなシナリオとして、以下のユーザーストーリーを想定します。(実際の弊社サービスに対して正確な表現ではありません。)

  • Step1: 英会話レッスンが始まる直前の時刻にユーザーがログインする
  • Step2: レッスン予定を閲覧できるページにアクセスする(レッスン開始ボタンがある)
  • Step3: 開始ボタンを押してレッスンルームに入る

このとき、会員基盤サービスに関して、以下のようなやり取りが行われます。(下記システム構成も実際の弊社システムに対して正確ではありません。)

ログイン処理

f:id:ochataro:20200901234249p:plain
login

スケジュール取得

f:id:ochataro:20200901234329p:plain
schedule

単一アカウントのみのテストの場合

1つの会員アカウントを使って負荷をかける場合、VegetaのCLIを利用すれば簡単にテストが可能です。

f:id:ochataro:20200901234534p:plain

以下のログインエンドポイント用のJSONペイロードファイル(login.json)と対象エンドポイントを記載したファイル(target.txt)を用意します。

予定取得エンドポイント(/schedule)にてJWTトークンは予め用意したものを利用します。

login.json

{
  "email": "test_user@example.com",
  "password": "Password"
}

target.txt (※ この記事ではあえてローカル環境をターゲットにしています。)

POST http://localhost:8090/login
Content-Type: application/json
@login.json

GET http://localhost:8090/schedule
Content-Type: application/json
Authorization: Bearer xxxxx.yyyyy.zzzzz

負荷を以下のようなオプションでかけます。このとき、対象アカウントにてログインとスケジュール取得リクエストを交互に繰り返します。

vegeta attack -targets=target.txt -rate=50/s -duration 600s | \ 
  vegeta report -type=json | \
  jq

複数アカウントのテストの場合

テストシナリオを実環境にできる限り近づけるべく、テスト環境でバラバラな設定を持つユーザーらを事前に準備し負荷試験に利用するという前提です。

f:id:ochataro:20200901234517p:plain
multi

ポイントとなるのは、負荷時にログイン処理から返るJWTトークンを動的に取得し、予定取得エンドポイントのリクエストにてAuthorizationヘッダーへ取得したトークンを入れる処理を実装する必要があるということです。

プログラム

1000アカウント分のクレデンシャル情報を持つCSVファイルを事前に攻撃サーバに用意します。

test1@example.com,Passw0rdDAZE!
test2@example.com,Passw0rdNANOKA?
.
.
.
test1000@example.com,Passw0rdKAMONE&

Goのソースコードは下記になります。Vegetaライブラリのドキュメントとツール側の実装を参考にしながら作成しました。ちょっと長めなので折りたたんでいます。

主にJWTトークンの受け渡し部分と1000アカウント終了時に攻撃を停止する制御の部分でコードが少々複雑になってしまいましたがこれで動きます。

負荷をかける!

【はじめに注意】本記事用にダミーな負荷対象HTTPサーバをローカルに立てて検証していますので、下記の結果は弊社サービスの実際のシステムとは全く関係の無い結果であることにご留意ください。

上記のプログラムを動かし負荷をかけると結果が得られます。今回は2つのエンドポイントを混ぜ合わせた1シナリオとしての結果が得られるようにしています。

$ go build -o attack
$ ./attack -rate=50 -duration=60 | jq
{
  "latencies": {
    "total": 808174235516,
    "mean": 404087117,
    "50th": 410460112,
    "95th": 457702034,
    "99th": 470219198,
    "max": 509059811
  },
  "bytes_in": {
    "total": 64000,
    "mean": 32
  },
  "bytes_out": {
    "total": 52893,
    "mean": 26.4465
  },
  "earliest": "2020-mm-ddTHH:MM:SS.097961559+09:00",
  "latest": "2020-mm-ddTHH:MM:SS.097961559+09:00",
  "end": "2020-mm-ddTHH:MM:SS.097961559+09:00",
  "duration": 39957268820,
  "wait": 458157781,
  "requests": 2000,
  "rate": 50.053471097076844,
  "throughput": 49.4860544154324,
  "success": 1,
  "status_codes": {
    "200": 2000
  },
  "errors": []
}

攻撃結果を得ることができました。上記の場合は出力をJSONフォーマットにしていますが、プロッティングとして可視化できる仕組みもあります。

おわりに

複雑になってしまったリクエスト間のやり取りと終了時の制御は上手く抽象化すればプラグイン的な物が提供できそうだなと感じました。また次の機会に取り組みたいです。

参考