
負荷テストをカジュアルに実施したい② ~k6でシナリオ作ってみたよ~
7月以降いろいろ忙しくて燃え尽きてnoteを書く気力を少し失っていた「ぎだじゅん」です。
ライフイズテックという会社でサービス開発部 インフラ/SREグループに所属しています。
いろんなお仕事を広く浅くしてきたおじさんですが、新しいことは常に吸収しつづけなければいけない業界(?)にいるので、勉強の一環で役に立ちそうなセミナーやワークショップがあれば参加しています。
ただ、参加するセミナーでは自分よりも一回り以上も若い方々が多くなり、そんな中で、見た目は役員、雰囲気は中間管理職、頭脳と実際のポジションはペーペーの逆コナン(?)的な自分が参加するのに、少し恥ずかしさを感じることもたまにあります。

昔話はしないように心がけていますが、ついしちゃいます。
昔は自分にもあった新しいことへの興味に対する行動力や実践力は、歳をとるにつれ減っていることは確かです。
しかし、おじさんでも勉強していかないと、抽象的でありきたりなことしか発言できなかったり、自身の過去の経験と比較して否定しかできないでいたりで、今の時代に必要な具体的なことが何もできないダメなおじさんになりそうなので、恥ずかしいとか面倒くさいとかと感じる時もありますが、自分にムチを打ってセミナーに参加しています。

今の職場ではセミナーに参加しなくとも否が応でもいろいろ経験させてくれるのですが、現場のメンバーがみんなキラキラしていて活気あふれる若い人が多い中で、みんなから刺激を受けつつ、自分も個性あふれるメンバーの中で存在感を出していけるように、少しでも成長し続けようと心がけています。

そんな活気あふれる素敵な職場で一緒にポジティプに働ける人を超募集してます!!
これを見て興味を持ってくれた方は、応募してくれると嬉しいです!!

俺はキレンジャー、カレーじゃなくてラーメンが好きだぜ!!
本題です。
k6で負荷テストを行なっています
(前回の振り返り)
6月24日に書きましたGrafana k6に関してのnoteから、約3ヶ月が経過しようとしています。
続きを書くと宣言しておきながら時間が経過してしまい、少しづつ思い出しながら書いてます・・・。
前回は、いろんな負荷テストツールからGrafana k6を選択した経緯や、初期セットアップと、デフォルトで生成されるシナリオファイルの内容の説明、コマンドでの負荷テスト実施してコンソールに出力されるテスト結果の内容について紹介しました。
セットアップも簡単で、単一のURLに対しての負荷テストであれば、デフォルトのシナリオのファイルに、対象のURLと同時接続させる数(vus/仮装ユーザ数)と、継続して実行する時間(duration)またはシナリオを繰り返す回数(iterations)を指定してコマンド一発で実行するだけでできます。

ただ、実際に負荷テストをする際には、対象のシステムによっては最初にログインをしたり、前のページでのレスポンスの値から、その値をパラメータとして付与して次のページへリクエストを投げたり、目的のページまで複数のページを遷移させるケースが多いと思いますので、その遷移にあわせたシナリオを作成しないといけません。

k6のシナリオはJavaScriptで定義されている
k6のシナリオのファイルでは、JavaScriptで書かれており、負荷テストでリクエストを投げる対象のURLやHTTPリクエストメソッド、リクエストパラメータなどをリクエスト毎にJavaScriptで定義する必要があります。
今回の負荷テスト対象も目的の処理ではログインしてから複数のページでデータをPOSTして完了するようなページ遷移でテストが必要であり、JavaScriptに精通していない私にとっては、負荷テスト実施の準備段階で燃え尽きてしまいそうです。

そんな自分が燃え尽きないように、Chromeの拡張機能を使って負荷テストのアクセス遷移をレコーディングしてシナリオ化しました。
今回は、そのシナリオ化の方法と、生成されたシナリオを実際の複数の利用者のアクセスを想定してテストできるように以下の修正をした内容について、紹介します。
仮想ユーザ毎に異なるアカウント情報でログインさせる
レスポンスの値から次ページのリクエストのURI(パス)やパラメータをに埋め込む

ちなみに、JavaScriptはDynamic HTMLが盛り上がっていた頃にMacromediaのShockwaveばりにWebサイトを動かしてた以降は、ほぼ触っていないので、今のJavaScriptに詳しい方ならもっと便利な書き方などあるかもしれません。あくまで参考まで・・・。

Chrome拡張を使ったシナリオ作成
k6ではシナリオ作成の方法として、1リクエストごとにスクリプトを書いていくこともできますが、実際にブラウザでテスト対象のサイトへアクセスしたユーザセッションを記録してシナリオ化する方法もあります。
公式サイトでは以下の3つの方法が紹介されています。
Chromeの拡張機能「k6 DevTools recorder」をインストール
Chromeのデベロッパーツールにあるレコーディング機能を使ってレコーディングした結果を、拡張機能でk6のJavascript形式でエクスポートができる
このエクスポートされたものから少し編集をしてシナリオファイルを作成
各種ブラウザのデベロッパーツールで記録できるHTTP トラフィックをHAR形式 ファイルでエクスポートしたものをk6 スクリプトにコンバートする NodeJS ツールでシナリオファイルを生成
拡張機能「Grafana k6 Browser Recorder」を使ったシナリオの作成
私は、この中でもChrome拡張機能で「Browser Recorder」を使った以下の手順でシナリオを作成しました。
①ブラウザに拡張機能をインストール
拡張機能「Grafana k6 Browser Recorder」はChromeまたはFirefoxで提供しているので、実際にテスト対象のサイトにアクセスして記録する際に使用するブラウザで拡張機能を追加してください。


追加されたら拡張機能「Grafana k6 Browser Recorder」をブラウザの「ツールバーに固定しておくようにし、シークレットモードでの実行を許可するようにしておきます。
(ブラウザでのレコーディング時に、日常で使用しているユーザのセッション情報が残ってしまっていることによる影響を避けるために、シークレットモードで行ったほうがよいかなと思い、このようにしてます)



②Grafana Cloud のアカウント作成およびk6の有効化
拡張機能「Grafana k6 Browser Recorder」では、ローカルPC上にもレコーディングしたデータを出力できます。
ただし、出力されるファイルの形式はk6のスクリプトの形式ではなく、HAR形式のため、最終的にはk6 スクリプトにコンバートする必要があります。
Grafana Cloudのアカウントがあれば、拡張機能でレコードディングされたものをGrafana Cloud k6と連携してk6 のスクリプト形式に出力してくれます。
Grafana Cloudアカウントについては、拡張機能から出力したデータをk6 スクリプト形式に出力する機能については、無料アカウントでも利用が可能なようです。


(Deployment regionに「Japan」があったので、「Japan」を選択してみました)
Grafana Cloud上でStack環境が用意されるまで多少時間がかかりますので、ちょっと待っておきましょう。

「Grafana is loading…」から画面が切り替わらない場合、一旦ブラウザを閉じて、再度ブラウザを立ち上げて拡張機能のアイコンをクリックしてみてください
「Sign in」のボタン表示でなくなっていれば環境は立ち上がっていると思いますが、まだ「Sign in」のボタン表示のままなら、再度サインインを選択してみてください
作成されたStack URL「[指定した名称].grafana.net」の環境が表示されたら、「Testing & synthetics > Performance」を開き、「Start testing」を押してk6の負荷テストが利用できるようにしておきます。


これでGrafana Cloud側でk6のシナリオをスクリプト出力する準備ができました。
③拡張機能でレコーディングを開始する
次に、ブラウザをシークレットモードで開いて、拡張機能でレコーディング開始してテスト対象へアクセスしながらユーザセッションを記録していきます。
ブラウザのツールバーに表示されているk6のアイコンをクリックすると以下のように、アカウント作成時に作成した自身のスタック環境が「Grafana Stack」上で選択された状態で以下のようなメニューが表示されるので、「Start recording」を開始します。


「Start recording」を押してレコーディング開始

ブラウザでのデバッグが開始した旨の表示が現れます(Chromeの場合のみ?)

レコーディング中の状態です
テスト対象のサイトを一通りアクセスできたら、レコーディングを終了します。

レコーディング終了すると、ブラウザの別タブで以下のようなGrafana Cloudのスタック環境が開きます。

こちらの画面でレコーディングした内容からスクリプトを生成する処理を行います。
生成するにあたって、以下のように設定をして「Save」ボタンを押します。
「Project」は「Default project」を選択
「Test name」は、任意の名称(シナリオ名や実行日などを)を記入してください。
今回はスクリプトを生成するので「Test builder」ではなく「Script editor」を選択します。
サーバーから返されたデータの変数を自動的に検出して設定するようにするため、「Correlate request and response data」はチェックします。
SPA(シングルページアプリケーション)の環境においてAPIの負荷テストを行う目的を想定しているので、「Include static assets」のチェックは外します。
画像ファイル、CSSファイル、JSファイル などのリクエストも負荷テストに含む場合は「Include static assets」のチェックを入れてください(ただ、負荷テストを実施する環境側に負荷がかかることが考えられます)。
実際のユーザーのアクセスと同じようなアクセス遷移を想定して、500 ミリ秒以上の間隔で行われたリクエストの間には、実際のリクエスト間隔の秒数でテストが行われるようにするため、「Generate sleep」にチェックを入れます。
レコーディング時にサードパーティ ドメインに対して行われたリクエストが見つ買った場合は、「Third-party domains filtering」が表示されるので、これらのサードパーティ ドメインへのリクエストもテストに含める場合は「Select domains」から選択してください。

「Save」を押すと以下の画面に切り替わり、Javascriptでk6スクリプト化されたシナリオが常時されます。
この表示されたスクリプトをコピーして、k6の実行環境であるサーバ上に「script.js」などのファイル名でスクリプトファイルとして保存しておきます。

k6の実行環境で「script.js」などのファイル名でスクリプトファイルとして保存できたら、一度、 仮想ユーザ(vus)を1で実際にシナリオでテストを実施して問題なくシナリオが実行できるかを確認します。
以下のようにオプションを指定して「k6 run」コマンドを実行します。
k6 run --vus 1 --iterations 1 -v script.js --out json=output.json
仮想ユーザを「--vus 1」で1ユーザによるリクエストを指定(あわせて「--iterations 1」で1回分の実行を指定)
「-v」でデバッグモードで実行
作成したシナリオファイル「script.js」を指定
「--out json=output.json」で実行結果をjson形式で「output.json」のファイル名でログ出力
実行したコンソール上にエラー出力がないかや「http_req_failed」がないかを確認します。
もし、以下のように「✓」で件数が1件以上ある場合は、出力した実行結果のファイル「output.json」から、どのリクエストで問題が起きているかを確認します。
(後述の 各リクエストでの問題を特定する を参考にして
http_req_failed................: 2.56% ✓ 2 ✗ 76
以下のようなgrepコマンドで「http_req_failed」のログ記録のうち、HTTPステータスコードが2xx系以外のログをgrepすることで、問題のリクエストをログから確認できます。
$ grep "http_req_failed" output.json |grep -v '"status":"2'
リクエストを特定できたら、シナリオファイルから該当のリクエストの内容に問題がないかを確認して、必要に応じてシナリオを修正します。
後述の 各リクエストでの問題を特定する にもありますが、シナリオの中の問題があるリクエストの箇所にレスポンスを出力するように追加すると、レスポンス内容を確認できます。
# こちらをリクエストの後に追加してレスポンスを出力する
console.log(JSON.stringify(response))
こんな感じです。
response = http.get(
'https://example.jp/contents/page01',
{
headers: {
'Authorization': `Bearer ${authToken}`,
},
}
)
console.log(JSON.stringify(response))
このような形でシナリオのベースとなるk6スクリプトファイルを作成していきました。
以上が実際に行った拡張機能「Grafana k6 Browser Recorder」を使ったシナリオ出力の手順です。
今回は負荷テストを実施するサーバ環境は自前で用意したEC2インスタンンス環境でk6 CLIをセットアップして実施しました。
EC2インスタンス上でのセットアップ方法は前回のnoteを参考にしてください。
ちなみに、Grafana Cloudの有償版であれば、実施環境を別途用意する必要もなく、Grafana Cloudの環境から直接負荷テストしたり、テスト結果をわかりやすいダッシュボードで見ることも可能です。
頻繁に負荷テストされる場合は、有償版の導入を検討してみても良いかもしれません。
スクリプトでのリクエストのフォーマットについて
出来上がったシナリオのスクリプトファイルを見てみましょう。
前でもお伝えしましたが、私もあまりJavaScriptに詳しくなくて、拡張機能でシナリオが簡単にできても、JavaScriptで出力されたら訳わかんないなと思いつつ見てみましたが、なんとなく何をしているかはわかるような内容でしたので安心しました。
HTTP Requestsに関しては以下のような形式で、リクエスト毎にリクエスト先のURLやパラメータ、ヘッダ情報などが書かれています。
▼GETの場合
http.get(URL, Params);
▼POSTの場合
http.post(URL, Body, Params);
URL
リクエストURLBody (http.postやhttp.put、http.optionsなどの場合)
HTTPリクエストボディ(リクエストパラメータ)認証ページの時のUser/PasswordなどのPOSTデータ
任意のデータ形式(JSON、XML、バイナリ等)で記載
何もない時は null を指定
Params (Optional)
HTTPリクエストヘッダーなどリクエストヘッダー(headers)では、ユーサーエージェント、authorization、origin、content-type、CORS関連など
キーと値の形式で記述
ヘッダー情報で処理の結果が変わるようであれば、こちらで必要な情報を定義する必要がある
たとえば、サイトへのログインのPOSTリクエストでは以下のような感じでシナリオが書き出されていました。
response = http.post(
'https://example.jp/login',
'{"username":"taro.suzuki", "password":"WsdHa23h!3s9"}',
{
headers: {
'content-type': 'application/json',
},
}
)
const authToken = response.json('accessToken')
response = http.get(
'https://example.jp/content',
{
headers: {
'Authorization': `Bearer ${authToken}`,
},
}
)
Bodyの部分にPOSTするログイン情報を記載し、Paramsのheadersでcontent-typeに「application/json」を指定することでPOSTデータをjson形式で送信する旨を明示しています。
そして、ログインのPOSTリクエストが成功するとレスポンスでアクセストークンを返してくれます。
そのトークンを使って以降のリクエストを処理できるように、レスポンス情報にあるaccessTokenの値を変数「authToken」に格納し、次のリクエストでは、リクエストのParamsのheadersでauthorizationの値として返すようにすることで、認証を経てリクエストが処理されるようになります。
このような感じで実際のアクセス遷移に合わせてhttp.get や http.post のリクエストが順に記述されています。
詳しくは以下を参考にしてください。
実際のアクセス状態を想定したシナリオに修正する
ただ、拡張機能で生成されたシナリオのスクリプトファイルをそのまま使っても、実際のアクセス状態を想定したリクエストで負荷検証できない場合があります。
例えは前述のサイトへのログインのPOSTリクエストのスクリプトでは、ユーザー名とパスワードが直接スクリプトに書かれています。
そのシナリオで同時接続数が「vus: 1000」で指定してテストを実施すると、1,000リクエスト全てが同じアカウントでログインすることになりますが、実際に同一アカウントで大量アクセスするのは外部からの攻撃(?)以外には考えにくいです。
また、アクセスの遷移の中で前のリクエストで返ってきたレスポンスの値が次のリクエストのURI(パス)部分に含まれるような動的なURLリクエストの場合、レスポンスに合わせて次のURLを変える必要があります。
これらに対応すべく、生成されたスクリプトから修正したり、試験結果をもう少しわかりやすくするためにチェックポイントを入れるなどをしたので、それらについて紹介します。
①VU(仮装ユーザ)毎に異なるアカウントでログインさせる
Webのサービスで認証が必要なサイトでの負荷テストでは、ログイン認証をしたうえでリクエストするようにしないといけません。
前述の通り、拡張機能で生成されたシナリオのスクリプトファイルでは、シナリオ作成時のログイン情報がスクリプトに直接書かれているため、これを負荷テストの同時リクエスト時にリクエスト毎に異なるログイン情報で認証させる必要があります。
そのため、vusで指定する分(vusが1000なら1,000アカウント分)のアカウント情報を事前に用意し、それをjson形式で用意してリクエスト毎に1アカウント使用して認証するようにシナリオを修正します。
まず、アカウント情報をjson形式で用意します。
以下のような形式で users の中に必要なユーザ情報(username と password)が記載されたファイルを「users.json」という名称で用意します。
(以下はサンプルです)
{
"users": [
{ "username": "XXXX.0001", "password": "UtyA4hMRSEb8" },
{ "username": "XXXX.0002", "password": "zxWik6dhg5de" },
{ "username": "XXXX.0003", "password": "bziwjvS9gJg2" },
{ "username": "XXXX.0004", "password": "HOVEgSQVi8OS" },
~ (省略) ~
{ "username": "XXXX.1000", "password": "xbs9tDLkW8jv" }
]
}
username にはログインユーザ名を記載
password にはログインユーザのパスワードを記載
自分はCSV形式で用意されたアカウント情報を、うまいことExcel関数で「{ "username": "ユーザ名", "password": "パスワード" },」の形式に書き出したものをテキストに移して、末尾のアカウント情報だけ「,(カンマ)」を取って、先頭2行と末尾2行で括弧で囲みました。
今回は前述で紹介したログイン認証のファイルを、このアカウント情報のファイル「users.json」を使って、認証するように修正します。
以下は修正前のものです。
response = http.post(
'https://example.jp/login',
'{"username":"taro.suzuki", "password":"WsdHa23h!3s9"}',
{
headers: {
'content-type': 'application/json',
},
}
)
const authToken = response.json('accessToken')
response = http.get(
'https://example.jp/content',
{
headers: {
'Authorization': `Bearer ${authToken}`,
},
}
)
まずシナリオのスクリプトの先頭部分にある「import」の行に以下を追加します。
import { SharedArray } from 'k6/data'
SharedArray はk6が提供しているモジュールで、これを使ってアカウント情報を配列形式で保持するように設定します。
これを使い、以下のような形で配列にアカウント情報を記録しています。
記載する箇所は先ほど追加した「SharedArray」のimportの次あたりです。
const data = new SharedArray('some name', function () {
return JSON.parse(open('./users.json')).users;
});
open('./users.json') の箇所で、用意したアカウント情報のファイル「users.json」の場所を指定し、そのファイルのusersの情報をSharedArrayで配列として「data」に格納していきます。
続いてリクエストユーザ毎に使用するアカウント情報をdataから呼び出して利用できるようにします。
k6ではvusで指定した数の仮想ユーザ毎に1から始まる値(VU番号)が割り当てされます。その仮想ユーザに指定されたVU番号を使って使用するアカウント情報をdataにある配列から割り当てることで、アカウント情報が重複しないようにしていきます。
そのVU番号は「__VU」と呼ばれる事前に用意されている特殊変数で呼びだすことができます。
(k6 v0.34.0 では、k6/executionモジュールが導入されたため、そのモジュールを使って呼び出しすることを推奨しているようですが、自分が実施した環境はそれより前のバージョンだったので、この方法で行っています)
この「__VU」を使ってdataからアカウント情報を一つずつ割り当てていきます。
「data」に格納されているアカウントの配列は data[0] からスタートとなりますが、「__VU」のVU番号は1からのスタートとなるため、VU番号から1を引いてdata[VU番号−1]のような形で仮想ユーザ毎に一つずつ異なるアカウント情報を「user」に割り当てていきます。
const usernum = `${__VU}`-1
const user = data[usernum]
こちらを記述する場所ですが、Chrome拡張機能で生成したシナリオで「response = http.get〜」などのリクエストのコードが以下のようにgroupでグルーピングされているので、そのグループの先頭に書くようにしておきます。
group('page_1 - https://example.jp/', function () {
const usernum = `${__VU}`-1
const user = data[usernum]
(省略)
}
最後にログインのリクエストでアカウント情報を直書きで指定していた「'{"username":"taro.suzuki", "password":"WsdHa23h!3s9"}',」の箇所を以下のように書き換えます。
「user.username」や「user.password」で指定することで、先ほど格納したアカウント情報「user」から「username」と「password」をそれぞれ呼び出し、この認証情報を「JSON.stringify」でJSON 形式の文字列として変換しています。
JSON.stringify({
username: user.username,
password: user.password,
}),
これらを踏まえて最終的に以下のような感じになりました。
前でも書きましたが、ログインのPOSTリクエストが成功するとレスポンスでアクセストークンを返してくれます。
そのアクセストークンを続くリクエストで使って処理できるように、レスポンス情報にあるaccessTokenの値を変数「authToken」に格納し、続くリクエストのParamsのheadersでauthorizationの値として「`Bearer ${authToken}`」で変数「authToken」を指定してリクエストをさせることで、後続でもリクエストが処理されるようになります。
import { sleep, group } from 'k6'
import http from 'k6/http'
import { SharedArray } from 'k6/data'
const data = new SharedArray('some name', function () {
return JSON.parse(open('./users.json')).users;
});
(省略)
export default function main() {
let response
group('page_1 - https://example.jp/', function () {
const usernum = `${__VU}`-1
const user = data[usernum]
(省略)
response = http.post(
'https://example.jp/login',
JSON.stringify({
username: user.username,
password: user.password,
}),
{
headers: {
'content-type': 'application/json',
},
}
)
const authToken = response.json('accessToken')
response = http.get(
'https://example.jp/content',
{
headers: {
'Authorization': `Bearer ${authToken}`,
},
}
)
(省略 ~他のリクエストが続く~)
}
}
②レスポンスの値から次のリクエストのリクエストURIを指定する
サービスによっては、以下のような感じでレスポンスの値を元に次に遷移するリクエストのリクエスト先のURL(そのパス部分など)が変わるケースがあるかと思います。
POST https://example.jp/issueid
イシューのID発行リクエスト
レスポンスでコンテンツID(id)の「2093427」を返す
GET https://example.jp/contents/2093427/page01
コンテンツID(id)「2093427」のコンテンツを返す
その場合、以下のようにidを取得するリクエストの後に「response.json('id')」でレスポンス(response)の中にあるid(コンテンツID)の値を変数「contetsId」に入れます。
response = http.post(
'https://example.jp/issueid',
'{"page":01,"contents":true}',
{
headers: {
'Authorization': `Bearer ${authToken}`,
'content-type': 'application/json',
},
}
)
const contetsId = response.json('id')
そのコンテンツIDをパスやPOSTデータで「${contetsId}」で指定することで使用できます。
その場合、URLやPOSTデータはこれまで「' (シングルクオート)」で囲っていましたが、「${contetsId}」を入れる場合は「`(バッククオート)」で囲うように変更する必要があります。
response = http.get(
`https://example.jp/contents/${contetsId}/page01`,
{
headers: {
'Authorization': `Bearer ${authToken}`,
},
}
)
response = http.post(
'https://example.jp/check',
`{"contentsId":${contetsId},"page":"01"}`,
{
headers: {
'Authorization': `Bearer ${authToken}`,
'content-type': 'application/json',
},
}
)
③groupを一つにまとめる
Chrome拡張機能で生成されるシナリオのスクリプトでは、ページの遷移にあわせて自動で各ポイント毎に「group」でリクエストをグルーピングされて生成されます。
export default function () {
let response
const vars = {}
group('page_1 - https://example.jp/', function () {
(トップページ表示の各リクエストのスクリプト)
});
group('page_2 - https://example.jp/login', function () {
(ログイン時の各リクエストのスクリプト)
vars['accessToken1'] = jsonpath.query(response.json(), '$.accessToken')[0]
});
group('page_3 - https://example.jp/contents', function () {
(コンテンツ表示の各リクエストのスクリプト)
});
group('page_4 - https://example.jp/checkout process', function () {
(ログアウトの各リクエストのスクリプト)
});
}
ただ、この方法を取るとログイン認証して発行されたトークン情報をgroupを跨いで利用できるようにする必要があり、デフォルトでは「SharedArray」を使わずに「const vars = {}」を定義してトークン情報を格納するような形になっていましたが、これだとvusの数が多い場合に、負荷をかける側のリソースを結構消費するようだったりするようでした。
ただ、「SharedArray」をうまく使ってスクリプトを書き換えれば問題はなかったかもしれませんが、自分のJavaScriptの知識が乏しかったのと今回の負荷テストでは「group」を分けている必要性がなさそうだったので、以下のように「group」を一つにしました。
export default function () {
let response
group('page_1 - https://example.jp/', function () {
(トップページ表示の各リクエストのスクリプト)
(ログイン時の各リクエストのスクリプト)
const authToken = response.json('accessToken')
(コンテンツ表示の各リクエストのスクリプト)
(ログアウトの各リクエストのスクリプト)
});
}
これは、少し私のk6の理解が足りないだけで、「group」を一つに統一しなくても大丈夫な方法があるかもしれません。
各リクエストでの問題を特定する
デフォルトで診断を実施すると以下のような実施結果を出力してくれます。
$ k6 run script.js
/\ |‾‾| /‾‾/ /‾‾/
/\ / \ | |/ / / /
/ \/ \ | ( / ‾‾\
/ \ | |\ \ | (‾) |
/ __________ \ |__| \__\ \_____/ .io
execution: local
script: script.js
output: -
scenarios: (100.00%) 1 scenario, 10 max VUs, 1m0s max duration (incl. graceful stop):
* default: 10 looping VUs for 30s (gracefulStop: 30s)
data_received..................: 12 MB 403 kB/s
data_sent......................: 46 kB 1.5 kB/s
http_req_blocked...............: avg=1.06ms min=262ns med=515ns max=37.54ms p(90)=823ns p(95)=1.06µs
http_req_connecting............: avg=64.92µs min=0s med=0s max=2.51ms p(90)=0s p(95)=0s
http_req_duration..............: avg=13.38ms min=6.91ms med=10.74ms max=107.41ms p(90)=20.89ms p(95)=23.03ms
{ expected_response:true }...: avg=13.38ms min=6.91ms med=10.74ms max=107.41ms p(90)=20.89ms p(95)=23.03ms
http_req_failed................: 0.00% ✓ 0 ✗ 300
http_req_receiving.............: avg=872.78µs min=140.01µs med=494.63µs max=49.96ms p(90)=1.51ms p(95)=1.93ms
http_req_sending...............: avg=69µs min=21.9µs med=59.26µs max=1.01ms p(90)=86.94µs p(95)=143.47µs
http_req_tls_handshaking.......: avg=456.52µs min=0s med=0s max=19.28ms p(90)=0s p(95)=0s
http_req_waiting...............: avg=12.44ms min=6.39ms med=9.74ms max=107.15ms p(90)=20.16ms p(95)=22.09ms
http_reqs......................: 300 9.810602/s
iteration_duration.............: avg=1.01s min=1s med=1.01s max=1.1s p(90)=1.02s p(95)=1.02s
iterations.....................: 300 9.810602/s
vus............................: 10 min=10 max=10
vus_max........................: 10 min=10 max=10
ただ、この内容の場合、どこのページがエラーになっているかなどがこの画面からだけでは特定ができません。
その場合は、確認したいリクエストの箇所にチェックポイントを埋め込んでおくことで、そのリクエストでエラーが何件起きたかを確認できます。
なお、この機能を使う場合はモジュールを追加する必要があるため、「import」でk6が提供している check モジュールを指定します。
import { check } from 'k6';
れぞれのリクエストの後に以下のような形で「check」を使ってレスポンスの内容からステータスコード(r.status)が指定したもの(http.get なら200、http.postなら201)なら成功、それ以外の場合は失敗として記録するように設定しておきます。
response = http.get(
'https://example.jp/contents/page01',
{
headers: {
'Authorization': `Bearer ${authToken}`,
},
}
)
check(response, { 'Contents-Page01': (r) => r.status === 200 })
response = http.post(
'https://example.jp/contents',
'{"contents":"test01","page":"02"}',
{
headers: {
'Authorization': `Bearer ${authToken}`,
'content-type': 'application/json',
},
}
)
check(response, { 'Contents-Page02': (r) => r.status === 201 })
これらの設定を入れておくことで、それぞれのリクエストのチェックポイント毎に成功とエラーの数を結果として出力してくれます。
/\ |‾‾| /‾‾/ /‾‾/
/\ / \ | |/ / / /
/ \/ \ | ( / ‾‾\
/ \ | |\ \ | (‾) |
/ __________ \ |__| \__\ \_____/ .io
execution: local
script: script.js
output: -
scenarios: (100.00%) 1 scenario, 1000 max VUs, 10m30s max duration (incl. graceful stop):
* default: 1000 iterations shared among 1000 VUs (maxDuration: 10m0s, gracefulStop: 30s)
✗ Contents-Page01 in successfully
↳ 97% — ✓ 975 / ✗ 25
✗ Contents-Page02 in successfully
↳ 99% — ✓ 999 / ✗ 1
(以下省略)
これにより、どのようなリクエストの処理においてボトルネックになっているかなどを特定しやすくなりました。
また、エラーが出ているレスポンスに対してどのようなレスポンスの内容でエラーになっているかを確認したい場合は、「check」と同様に該当のリクエストの後に以下の「console.log」を埋め込むことでレスポンスの結果を出力してくれます。
(エラー数が多いと出力が多くて見にくくはなりますが)
console.log(JSON.stringify(response))
ログを大量に出力させることで、k6の負荷テスト自体の負荷などにも影響することも考えられるため、どうしてもエラーが残るリクエストがある時などで原因特定したい場合などにポイントで使用することをお勧めします。
最後に
今回はk6で実際にサービスへのアクセス導線を想定したシナリオを作成するにあたって、使用したChrome拡張機能の使い方や、拡張機能で出来上がったスクリプトを実際の利用状況に合わせて修正した内容について説明しました。
k6はセットアップも簡単で、シナリオもJavaScriptでありながら私のようなコードが苦手なおじさん(って言っていいのかわかりませんが)でも、扱いやすい内容で、とても便利です。
突発的に負荷テストが必要になった場合などでもパッと使えるカジュアルさがとてもいいと思いました。
ただ、もっと複雑なフローなどをシナリオで作る必要がある場合は、さらにスクリプトでいろいろ手を加えていく必要もあり、JavaScriptの知識が必要になりそうとも感じています。

そうなってくるとローカル環境でJMeterを使ってこつこつとGUIでシナリオを作成し、そのシナリオを使い、AWS のDLTソリューション(Distributed Load Testing)で提供されているCloudFormationのテンプレートでサクッと環境をデプロイして実施する方が私には向いてそうな気がしているので、同じお気持ちの方はこちらのワークショップを試してみても良いかと思います。
AWSはこのような運用面で必要な環境を簡単に構築できるコードなどGithub上でたくさん提供していますので、負荷テスト以外にもこれらを有効に使っていきたいと思う今日この頃です。

いろんなツールを使って改善を続けていく今の会社はとてもいい会社なので、興味のある方はこちらも是非見てみてください。