はじめに
現在、プログラミング学習コミュニティでISUCONの勉強会に参加しており、『達人が教えるWebパフォーマンスチューニング〜ISUCONから学ぶ高速化の実践』を読みながら学習を進めています。この記事はその学習記録として作成しました。
過去の記事はこちらです。
『ISUCON本で初めてのパフォーマンスチューニングに挑戦してみた』
動作環境
- mac OS Sonoma 14.2.1
- Homebrew 4.2.4
ハンズオンの概要
4章ではk6というテストツールを使って、iscogramというパフォーマンスチューニング練習用のWebアプリに対してシナリオを持った負荷試験を実行する方法が書かれています。
しかし、そのままハンズオンをしてもつまらないので、今回は自分がポートフォリオとして作成中のSNSアプリに対してシナリオを持った負荷試験を実行してみました!
1. k6を使った負荷試験を試してみる
2. シナリオを作成する
3. シナリオを使った負荷試験を実行する
k6を使った負荷試験を試してみる
k6とは
- 負荷試験を実行することができるオープンソースのテストツール
- 負荷試験のシナリオはJavaScriptで作成することができる
- 開発者が使いやすいAPIを持ったCLIが提供されている
API
- Application Programming Interfaceの略称
- アプリケーション同士で相互に情報をやり取りするためのインターフェースのこと
CLI
- Command Line Interfaceの略称
- コンピューターやソフトウェアとの対話を行うためのインターフェースのこと
- テキストベースのコマンドを入力して操作することができる
k6をインストールする
私はmac環境なのでHomebrewを使ってインストールしました。
他の環境の方は下記を参照してください。
k6公式ドキュメント
$ brew install k6
バージョンを確認して表示されればOK
$ k6 --version
=> k6 v0.50.0 (go1.22.1, darwin/arm64)
シンプルなシナリオを作成する
ログインページにGETリクエストを送信するだけのシナリオを作成してみます。
// HTTPリクエストを送信するための関数をインポート
import http from "k6/http";
// 負荷試験に使用するURLを定義
const BASE_URL = "http://123.456.78.90/login";
// GETリクエストを送信する関数を定義
export default function(){
http.get(`${BASE_URL}/`);
}
test.js
があるディレクトリで以下のコマンドを実行します。
—vus
オプションで並列度を、--duration
オプションで実行時間[s]を指定してしています。
$ k6 run --vus 1 --duration 30s test.js
以下のような実行結果が表示されればOK!
実行結果
/\ |‾‾| /‾‾/ /‾‾/
/\ / \ | |/ / / /
/ \/ \ | ( / ‾‾\
/ \ | |\ \ | (‾) |
/ __________ \ |__| \__\ \_____/ .io
execution: local
script: test.js
output: -
scenarios: (100.00%) 1 scenario, 1 max VUs, 1m0s max duration (incl. graceful stop):
* default: 1 looping VUs for 30s (gracefulStop: 30s)
data_received..................: 5.6 MB 185 kB/s
data_sent......................: 75 kB 2.5 kB/s
http_req_blocked...............: avg=40.93µs min=1µs med=4µs max=32.15ms p(90)=8µs p(95)=10µs
http_req_connecting............: avg=35.61µs min=0s med=0s max=31.66ms p(90)=0s p(95)=0s
http_req_duration..............: avg=33.53ms min=31.21ms med=33.39ms max=44.39ms p(90)=34.71ms p(95)=35.2ms
{ expected_response:true }...: avg=33.53ms min=31.21ms med=33.39ms max=44.39ms p(90)=34.71ms p(95)=35.2ms
http_req_failed................: 0.00% ✓ 0 ✗ 889
http_req_receiving.............: avg=198.91µs min=51µs med=149µs max=7.03ms p(90)=227µs p(95)=268.59µs
http_req_sending...............: avg=24.21µs min=7µs med=23µs max=117µs p(90)=36µs p(95)=41µs
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_waiting...............: avg=33.31ms min=30.17ms med=33.19ms max=44.26ms p(90)=34.51ms p(95)=34.96ms
http_reqs......................: 889 29.622859/s
iteration_duration.............: avg=33.73ms min=31.34ms med=33.57ms max=67.11ms p(90)=34.9ms p(95)=35.39ms
iterations.....................: 889 29.622859/s
vus............................: 1 min=1 max=1
vus_max........................: 1 min=1 max=1
各メトリクスの意味を調べてみたのでトグルにまとめておきます。
とりあえず、スループットに相当するhttp_req
と、レイテンシに相当するiteration_duration
を覚えておけば良さそうです。
k6のメトリクス一覧
メトリクス | 説明 |
---|---|
data_received | サーバーから受信したデータの合計量 |
data_sent | クライアントから送信したデータの合計量 |
http_req_blocked | リクエストがブロックされた時間の |
http_req_connecting | リクエストが接続を確立するのに要した時間 |
http_req_duration | リクエストの完了に要した時間(レイテンシ) |
http_req_failed | リクエストが失敗した割合 |
http_req_receiving | レスポンスを受信するのに要した時間 |
http_req_sending | リクエストを送信するのに要した時間 |
http_req_tls_handshaking | TLSハンドシェイクが行われた時間 |
http_req_waiting | リクエストがサーバーの応答を待機していた時間 |
http_reqs | リクエストの総数およびリクエストの実行速度(スループット) |
iteration_duration | イテレーション(1回のテストサイクル)の完了に要した時間 |
iterations | イテレーションの総数およびイテレーションの実行速度 |
vus | 同時に実行されている仮想ユーザー数 |
vus_max | テスト実行中に最大で同時に実行されていた仮想ユーザー数 |
シナリオを作成する
<共通で使用する関数を定義する>
負荷試験対象のURLを生成するための関数を定義してconfig.jsとして保存する。
// 負荷試験に使用するベースURLを定義
const BASE_URL = "http://123.456.90";
// 引数としてpathを受け取り、完全なURLを生成して返す関数を定義
export function url(path) {
return `${BASE_URL}${path}`;
}
<シナリオで使用するアカウント情報を定義する>
今回のシナリオではログイン処理を入れようと思います。
並列度を上げてシナリオを実行する際、複数のアカウントでリクエストを送信できるように複数のアカウント情報をJSON形式のファイルで用意します。
[
{
"email": "test_01@example.com",
"password": "password_01"
},
{
"email": "test_02@example.com",
"password": "password_02"
},
{
"email": "test_03@example.com",
"password": "password_03"
},
{
"email": "test_04@example.com",
"password": "password_04"
}
]
<アカウント情報を読み込むモジュールを定義する>
JSON形式のアカウント情報をSharedArray型として読み込むモジュールを作成します。
SharedArray型とは
- k6で用意されている配列的のような使い方のできるオブジェクト
- k6公式ドキュメント:SharedArray
// SharedArrayをimport
import { SharedArray } from "k6/data";
// accounts.jsonを読み込んでSharedArray型で保持する
const accounts = new SharedArray('accounts', function () {
return JSON.parse(open('./accounts.json'));
});
// SharedArrayからアカウント情報をランダムに1件取り出して返す
export function getAccount() {
return accounts[Math.floor(Math.random() * accounts.length)]
}
<ログインして投稿一覧を表示するシナリオを作成する>
以下のシナリオを持つ関数を定義する。
- ログインページにGETリクエストを送信する
- ログインフォームのCSRF対策用トークンを取得する
- ログインのPOSTリクエストを送信する
- 投稿一覧ページにGETリクエストを送信する
CSRFとは
- Cross-Site Request Forgeryの略称
- Webアプリケーションの脆弱性を利用した攻撃の一種
- Webサイトとのログイン状態(セッション)が維持されている状態で、攻撃者が用意した他のサイトのリンクを踏むとユーザーが意図しないリクエストが送信される攻撃
- Railsガイドの解説
// k6からhttp処理のmuduleをimport
import http from "k6/http";
// k6からHTMLをパースする関数をimport
import { parseHTML } from 'k6/html';
// k6からcheck関数をimport
import { check } from "k6";
// config.jsで定義したurl関数をimport
import { url } from "./config.js";
// get_account.jsで定義したgetAccount関数をimport
import { getAccount } from "./get_account.js";
// ログインしてから投稿一覧ページを表示するシナリオ関数
export default function (){
// ログインページをGETリクエストで取得
const loginPage = http.get(url("/login"));
// HTMLをパース
const doc = parseHTML(loginPage.body);
// action="/login"のフォーム要素を見つける
const form = doc.find('form[action="/login"]');
// CSRFトークンの値を取得
const loginCsrfToken = form.find('input[name="authenticity_token"]').attr('value');
// アカウント情報を取得
const account = getAccount();
// /loginに対してemailとパスワードを送信
const loginResponse = http.post(url("/login"), {
email: account.email,
password: account.password,
authenticity_token: loginCsrfToken
});
// レスポンスのステータスコードが200であることを確認
check(loginResponse, {
"is status 200": (r) => r.status === 200,
});
// 投稿一覧ページ"/tweets"にGETリクエストを送信
const tweetsResponse = http.get(url("/tweets"));
// レスポンスのステータスコードが200であることを確認
check(tweetsResponse, {
"is status 200": (r) => r.status === 200,
});
}
シナリオを使った負荷試験を実行する
並列度を変えてシナリオを実行してみる
作成したlogin_and_view_tweets.js
を使って負荷試験を30[s]実行してみます。
並列度--vus
を1から4まで変更して結果を比較してみました。
k6 run --vus 1 --duration 30s login_and_view_tweets.js
並列度1の実行結果
/\ |‾‾| /‾‾/ /‾‾/
/\ / \ | |/ / / /
/ \/ \ | ( / ‾‾\
/ \ | |\ \ | (‾) |
/ __________ \ |__| \__\ \_____/ .io
execution: local
script: login_and_view_tweets.js
output: -
scenarios: (100.00%) 1 scenario, 1 max VUs, 1m0s max duration (incl. graceful stop):
* default: 1 looping VUs for 30s (gracefulStop: 30s)
✓ is status 200
checks.........................: 100.00% ✓ 118 ✗ 0
data_received..................: 1.6 MB 52 kB/s
data_sent......................: 114 kB 3.8 kB/s
http_req_blocked...............: avg=139.66µs min=1µs med=6µs max=31.29ms p(90)=12µs p(95)=13µs
http_req_connecting............: avg=130.3µs min=0s med=0s max=30.75ms p(90)=0s p(95)=0s
http_req_duration..............: avg=126.73ms min=30.91ms med=41.73ms max=457.16ms p(90)=395.92ms p(95)=397.76ms
{ expected_response:true }...: avg=126.73ms min=30.91ms med=41.73ms max=457.16ms p(90)=395.92ms p(95)=397.76ms
http_req_failed................: 0.00% ✓ 0 ✗ 236
http_req_receiving.............: avg=226.18µs min=69µs med=216.5µs max=1.55ms p(90)=309µs p(95)=327.25µs
http_req_sending...............: avg=39.3µs min=7µs med=37.5µs max=247µs p(90)=56µs p(95)=61.24µs
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_waiting...............: avg=126.46ms min=30.74ms med=41.47ms max=456.85ms p(90)=395.59ms p(95)=397.42ms
http_reqs......................: 236 7.856294/s
iteration_duration.............: avg=509.12ms min=449.32ms med=511.73ms max=577.97ms p(90)=555.07ms p(95)=568.57ms
iterations.....................: 59 1.964073/s
vus............................: 1 min=1 max=1
vus_max........................: 1 min=1 max=1
並列度2の実行結果
/\ |‾‾| /‾‾/ /‾‾/
/\ / \ | |/ / / /
/ \/ \ | ( / ‾‾\
/ \ | |\ \ | (‾) |
/ __________ \ |__| \__\ \_____/ .io
execution: local
script: login_and_view_tweets.js
output: -
scenarios: (100.00%) 1 scenario, 2 max VUs, 1m0s max duration (incl. graceful stop):
* default: 2 looping VUs for 30s (gracefulStop: 30s)
✓ is status 200
checks.........................: 100.00% ✓ 148 ✗ 0
data_received..................: 2.0 MB 65 kB/s
data_sent......................: 143 kB 4.7 kB/s
http_req_blocked...............: avg=182.95µs min=1µs med=8µs max=25.98ms p(90)=11µs p(95)=12µs
http_req_connecting............: avg=167.15µs min=0s med=0s max=24.76ms p(90)=0s p(95)=0s
http_req_duration..............: avg=203.71ms min=30.98ms med=52.19ms max=759.74ms p(90)=676.97ms p(95)=681.69ms
{ expected_response:true }...: avg=203.71ms min=30.98ms med=52.19ms max=759.74ms p(90)=676.97ms p(95)=681.69ms
http_req_failed................: 0.00% ✓ 0 ✗ 296
http_req_receiving.............: avg=889.63µs min=62µs med=206.5µs max=8.43ms p(90)=3.54ms p(95)=4.84ms
http_req_sending...............: avg=38.79µs min=7µs med=36µs max=355µs p(90)=55µs p(95)=66µs
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_waiting...............: avg=202.79ms min=30.79ms med=50.41ms max=759.46ms p(90)=676.76ms p(95)=681.45ms
http_reqs......................: 296 9.783835/s
iteration_duration.............: avg=817.04ms min=711.12ms med=819.68ms max=910.95ms p(90)=840.35ms p(95)=858.03ms
iterations.....................: 74 2.445959/s
vus............................: 2 min=2 max=2
vus_max........................: 2 min=2 max=2
並列度3の実行結果
/\ |‾‾| /‾‾/ /‾‾/
/\ / \ | |/ / / /
/ \/ \ | ( / ‾‾\
/ \ | |\ \ | (‾) |
/ __________ \ |__| \__\ \_____/ .io
execution: local
script: login_and_view_tweets.js
output: -
scenarios: (100.00%) 1 scenario, 3 max VUs, 1m0s max duration (incl. graceful stop):
* default: 3 looping VUs for 30s (gracefulStop: 30s)
✓ is status 200
checks.........................: 100.00% ✓ 172 ✗ 0
data_received..................: 2.3 MB 74 kB/s
data_sent......................: 166 kB 5.4 kB/s
http_req_blocked...............: avg=277.36µs min=0s med=5µs max=31.17ms p(90)=11µs p(95)=12µs
http_req_connecting............: avg=263.14µs min=0s med=0s max=30.23ms p(90)=0s p(95)=0s
http_req_duration..............: avg=264.32ms min=31.56ms med=55.82ms max=1.48s p(90)=722.24ms p(95)=891.29ms
{ expected_response:true }...: avg=264.32ms min=31.56ms med=55.82ms max=1.48s p(90)=722.24ms p(95)=891.29ms
http_req_failed................: 0.00% ✓ 0 ✗ 344
http_req_receiving.............: avg=1.25ms min=54µs med=216.5µs max=9.51ms p(90)=5.15ms p(95)=6.31ms
http_req_sending...............: avg=38.16µs min=7µs med=33µs max=554µs p(90)=53µs p(95)=57µs
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_waiting...............: avg=263.03ms min=31.41ms med=54.49ms max=1.48s p(90)=721.93ms p(95)=890.95ms
http_reqs......................: 344 11.230755/s
iteration_duration.............: avg=1.05s min=786.46ms med=872.17ms max=1.69s p(90)=1.57s p(95)=1.59s
iterations.....................: 86 2.807689/s
vus............................: 3 min=3 max=3
vus_max........................: 3 min=3 max=3
並列度4の実行結果
/\ |‾‾| /‾‾/ /‾‾/
/\ / \ | |/ / / /
/ \/ \ | ( / ‾‾\
/ \ | |\ \ | (‾) |
/ __________ \ |__| \__\ \_____/ .io
execution: local
script: login_and_view_tweets.js
output: -
scenarios: (100.00%) 1 scenario, 4 max VUs, 1m0s max duration (incl. graceful stop):
* default: 4 looping VUs for 30s (gracefulStop: 30s)
✓ is status 200
checks.........................: 100.00% ✓ 172 ✗ 0
data_received..................: 2.3 MB 73 kB/s
data_sent......................: 166 kB 5.3 kB/s
http_req_blocked...............: avg=381.42µs min=1µs med=4µs max=32.4ms p(90)=10µs p(95)=12µs
http_req_connecting............: avg=367.85µs min=0s med=0s max=31.66ms p(90)=0s p(95)=0s
http_req_duration..............: avg=361.05ms min=30.35ms med=65.57ms max=1.32s p(90)=1.25s p(95)=1.29s
{ expected_response:true }...: avg=361.05ms min=30.35ms med=65.57ms max=1.32s p(90)=1.25s p(95)=1.29s
http_req_failed................: 0.00% ✓ 0 ✗ 344
http_req_receiving.............: avg=1.03ms min=41µs med=176µs max=13.44ms p(90)=4ms p(95)=5.2ms
http_req_sending...............: avg=33.35µs min=8µs med=27µs max=453µs p(90)=46µs p(95)=50.84µs
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_waiting...............: avg=359.98ms min=30.23ms med=64.13ms max=1.32s p(90)=1.25s p(95)=1.29s
http_reqs......................: 344 11.053335/s
iteration_duration.............: avg=1.44s min=815.44ms med=1.43s max=2.13s p(90)=1.54s p(95)=1.6s
iterations.....................: 86 2.763334/s
vus............................: 4 min=4 max=4
vus_max........................: 4 min=4 max=4
結果
並列度を変えた結果は以下のようになりました。
ログインして投稿一覧を表示するだけの簡単なシナリオですが、並列度3で頭打ちとなっているようです。
詳しく調べていませんが、nginxのworker_processesを2に設定しているのがボトルネックになっているのかなーと予想しています。
(EC2のインスタンスがt2.microなのでworker_processesを増やしても変わらないかも...)
クライアント数 | レイテンシ[ms] | スループット[request/sec] |
---|---|---|
1 | 126.7 | 7.85 |
2 | 203.7 | 9.78 |
3 | 264.3 | 11.23 |
4 | 361.1 | 11.05 |
まとめ
今回のハンズオンを通して、k6を使ったシナリオを持った負荷試験について学ぶことができました。
実際に自分でシナリオを書くことで、Railsではログイン時にCSRF対策用トークンが必要であることなど新たな学びも得られてよかったです。
最後までお読みいただきありがとうございます!