テスト対象
- Raspberry Pi4上に構築したWordPress
- サンプルで作成される
Hello World
のページを使用
※環境構築手順は末尾です。
テスト実行
1. とりあえず実行
JavaScriptでテスト用のコードを書いて実行します。
import http from 'k6/http';
import { check, sleep, } from 'k6';
export default function () {
const BASE_URL = 'http://192.168.1.113/'
const res = http.get(BASE_URL, { tags: { name: 'base' } })
check(res, {
'status was 200': (r) => {
return r.status == 200
}
})
const responses = http.batch([
['GET', `${BASE_URL}/wp-includes/blocks/navigation/style.min.css?ver=6.0`, null, { tags: { name: 'png' } }],
['GET', `${BASE_URL}/wp-includes/js/wp-emoji-release.min.js?ver=6.0`, null, { tags: { name: 'png' } }],
['GET', `${BASE_URL}/wp-content/themes/twentytwentytwo/style.css?ver=1.2`, null, { tags: { name: 'png' } }],
['GET', `${BASE_URL}/wp-includes/blocks/navigation/view.min.js?ver=009e29110e016c14bac4ba0ecc809fcd`, null, { tags: { name: 'png' } }],
['GET', `${BASE_URL}/wp-content/themes/twentytwentytwo/assets/images/flight-path-on-transparent-d.png`, null, { tags: { name: 'png' } }],
['GET', `${BASE_URL}/wp-content/themes/twentytwentytwo/assets/fonts/source-serif-pro/SourceSerif4Variable-Roman.ttf.woff2`, null, { tags: { name: 'png' } }],
['GET', `${BASE_URL}/favicon.ico`, null, { tags: { name: 'png' } }],
['GET', `${BASE_URL}/wp-includes/images/w-logo-blue-white-bg.png`, null, { tags: { name: 'png' } }],
]);
responses.forEach(res => {
check(res, {
'status was 200': (r) => {
return r.status == 200
}
})
});
sleep(1);
}
ブラウザからのアクセスを想定し、htmlの取得とページ中のjsやcssなども続けて取得する形にしてみました。
これを実行してみます。
$ k6 run script_v1.js
/\ |‾‾| /‾‾/ /‾‾/
/\ / \ | |/ / / /
/ \/ \ | ( / ‾‾\
/ \ | |\ \ | (‾) |
/ __________ \ |__| \__\ \_____/ .io
execution: local
script: script_v1.js
output: -
scenarios: (100.00%) 1 scenario, 1 max VUs, 10m30s max duration (incl. graceful stop):
* default: 1 iterations for each of 1 VUs (maxDuration: 10m0s, gracefulStop: 30s)
running (00m01.5s), 0/1 VUs, 1 complete and 0 interrupted iterations
default ✓ [======================================] 1 VUs 00m01.5s/10m0s 1/1 iters, 1 per VU
✓ status was 200
checks.........................: 100.00% ✓ 9 ✗ 0
data_received..................: 646 kB 426 kB/s
data_sent......................: 1.3 kB 887 B/s
http_req_blocked...............: avg=5.93ms min=5.97µs med=8.02ms max=11.12ms p(90)=10.77ms p(95)=10.94ms
http_req_connecting............: avg=2.43ms min=0s med=0s max=10.62ms p(90)=8.1ms p(95)=9.36ms
http_req_duration..............: avg=74.13ms min=8.42ms med=18.62ms max=349.01ms p(90)=155.72ms p(95)=252.37ms
{ expected_response:true }...: avg=74.13ms min=8.42ms med=18.62ms max=349.01ms p(90)=155.72ms p(95)=252.37ms
http_req_failed................: 0.00% ✓ 0 ✗ 10
http_req_receiving.............: avg=18.1ms min=212.95µs med=1.23ms max=101.51ms p(90)=50.78ms p(95)=76.14ms
http_req_sending...............: avg=159.56µs min=28.54µs med=101.63µs max=415.48µs p(90)=383.11µs p(95)=399.29µs
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_waiting...............: avg=55.87ms min=7.83ms med=14.87ms max=321.85ms p(90)=152.4ms p(95)=237.12ms
http_reqs......................: 10 6.590794/s
iteration_duration.............: avg=1.51s min=1.51s med=1.51s max=1.51s p(90)=1.51s p(95)=1.51s
iterations.....................: 1 0.659079/s
vus............................: 1 min=1 max=1
vus_max........................: 1 min=1 max=1
1回のアクセスを行い、結果が出力されます。1回だけなので負荷はかかってないです。
2. 繰り返し実行させる
繰り返し実行させるにはoptions
を指定します。スモークテストを例にすると、vus
(仮想ユーザー)が1で、1m
(1分間)連続でアクセスし、99%のレスポンスが1.5秒以内だと成功となります。
import http from 'k6/http';
import { check, sleep, } from 'k6';
// Smoke testing
export const options = {
vus: 1, // 1 user looping for 1 minute
duration: '1m',
thresholds: {
http_req_duration: ['p(99)<1500'], // 99% of requests must complete below 1.5s
},
};
export default function () {
const BASE_URL = 'http://192.168.1.113/'
const res = http.get(BASE_URL, { tags: { name: 'base' } })
check(res, {
'status was 200': (r) => {
return r.status == 200
}
})
const responses = http.batch([
['GET', `${BASE_URL}/wp-includes/blocks/navigation/style.min.css?ver=6.0`, null, { tags: { name: 'png' } }],
['GET', `${BASE_URL}/wp-includes/js/wp-emoji-release.min.js?ver=6.0`, null, { tags: { name: 'png' } }],
['GET', `${BASE_URL}/wp-content/themes/twentytwentytwo/style.css?ver=1.2`, null, { tags: { name: 'png' } }],
['GET', `${BASE_URL}/wp-includes/blocks/navigation/view.min.js?ver=009e29110e016c14bac4ba0ecc809fcd`, null, { tags: { name: 'png' } }],
['GET', `${BASE_URL}/wp-content/themes/twentytwentytwo/assets/images/flight-path-on-transparent-d.png`, null, { tags: { name: 'png' } }],
['GET', `${BASE_URL}/wp-content/themes/twentytwentytwo/assets/fonts/source-serif-pro/SourceSerif4Variable-Roman.ttf.woff2`, null, { tags: { name: 'png' } }],
['GET', `${BASE_URL}/favicon.ico`, null, { tags: { name: 'png' } }],
['GET', `${BASE_URL}/wp-includes/images/w-logo-blue-white-bg.png`, null, { tags: { name: 'png' } }],
]);
responses.forEach(res => {
check(res, {
'status was 200': (r) => {
return r.status == 200
}
})
});
sleep(1);
}
k6 run script_v2.js
/\ |‾‾| /‾‾/ /‾‾/
/\ / \ | |/ / / /
/ \/ \ | ( / ‾‾\
/ \ | |\ \ | (‾) |
/ __________ \ |__| \__\ \_____/ .io
execution: local
script: script_v2.js
output: -
scenarios: (100.00%) 1 scenario, 1 max VUs, 1m30s max duration (incl. graceful stop):
* default: 1 looping VUs for 1m0s (gracefulStop: 30s)
running (1m00.2s), 0/1 VUs, 42 complete and 0 interrupted iterations
default ✓ [======================================] 1 VUs 1m0s
✓ status was 200
checks.........................: 100.00% ✓ 378 ✗ 0
data_received..................: 27 MB 451 kB/s
data_sent......................: 57 kB 938 B/s
http_req_blocked...............: avg=111.51µs min=1.6µs med=6.95µs max=10.75ms p(90)=15.2µs p(95)=17.09µs
http_req_connecting............: avg=74.84µs min=0s med=0s max=7.39ms p(90)=0s p(95)=0s
✓ http_req_duration..............: avg=71.68ms min=5.41ms med=25.2ms max=327.56ms p(90)=226.82ms p(95)=277.36ms
{ expected_response:true }...: avg=71.68ms min=5.41ms med=25.2ms max=327.56ms p(90)=226.82ms p(95)=277.36ms
http_req_failed................: 0.00% ✓ 0 ✗ 420
http_req_receiving.............: avg=21.83ms min=48.98µs med=3.04ms max=205.44ms p(90)=92.69ms p(95)=111.21ms
http_req_sending...............: avg=41.36µs min=8.52µs med=32.74µs max=526.69µs p(90)=59.85µs p(95)=95.62µs
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_waiting...............: avg=49.81ms min=4.13ms med=14.97ms max=303.21ms p(90)=197.59ms p(95)=258.42ms
http_reqs......................: 420 6.972959/s
iteration_duration.............: avg=1.43s min=1.4s med=1.42s max=1.5s p(90)=1.45s p(95)=1.48s
iterations.....................: 42 0.697296/s
vus............................: 1 min=1 max=1
vus_max........................: 1 min=1 max=1
3. Grafanaで可視化
コンソールに出力される結果だけだと分かりづらいので、InfluxDBにデータを投入し、Grafanaで可視化ができます。
実行時に--out
で宛先を指定します。InfluxDBはv1にしか対応していないようです。
k6 run --out influxdb=http://localhost:8086/myk6db script_v2.js
4. どれだけのユーザー数に耐えられるか検証
以下の条件で試験を実施しました。
- 15秒ごとに1ユーザーずつ増加
- p95のリクエストが1.5秒以内に返却できることをしきい値として設定
options
にthresholds
を追加しています。abortOnFail
などの指定により、条件を満たさなくなった際にテストが終了します。
import http from 'k6/http';
import { check, sleep, } from 'k6';
export const options = {
stages: [
{ duration: '30m', target: 120 }, // below normal load
{ duration: '10m', target: 0 }, // scale down. Recovery stage.
],
thresholds: {
http_req_duration: [{ threshold: 'p(95)<1500', abortOnFail: true, delayAbortEval: '1m' }],
},
};
export default function () {
const BASE_URL = 'http://192.168.1.113/'; // make sure this is not production
const res = http.get(BASE_URL, { tags: { name: 'base' } })
check(res, {
'status was 200': (r) => {
return r.status == 200
}
})
const responses = http.batch([
['GET', `${BASE_URL}/wp-includes/blocks/navigation/style.min.css?ver=6.0`, null, { tags: { name: 'png' } }],
['GET', `${BASE_URL}/wp-includes/js/wp-emoji-release.min.js?ver=6.0`, null, { tags: { name: 'png' } }],
['GET', `${BASE_URL}/wp-content/themes/twentytwentytwo/style.css?ver=1.2`, null, { tags: { name: 'png' } }],
['GET', `${BASE_URL}/wp-includes/blocks/navigation/view.min.js?ver=009e29110e016c14bac4ba0ecc809fcd`, null, { tags: { name: 'png' } }],
['GET', `${BASE_URL}/wp-content/themes/twentytwentytwo/assets/images/flight-path-on-transparent-d.png`, null, { tags: { name: 'png' } }],
['GET', `${BASE_URL}/wp-content/themes/twentytwentytwo/assets/fonts/source-serif-pro/SourceSerif4Variable-Roman.ttf.woff2`, null, { tags: { name: 'png' } }],
['GET', `${BASE_URL}/favicon.ico`, null, { tags: { name: 'png' } }],
['GET', `${BASE_URL}/wp-includes/images/w-logo-blue-white-bg.png`, null, { tags: { name: 'png' } }],
]);
responses.forEach(res => {
check(res, {
'status was 200': (r) => {
return r.status == 200
}
})
});
sleep(1);
}
結果は、36ユーザーでしきい値に達しました。(これが多いのか、少ないのか、はどちらなんでしょう)
5. ソークテストもやってみた。
10分でソークテストなのかわかりませんが、段階的にアクセスを増やしてみます。
import http from 'k6/http';
import { check, sleep, fail } from 'k6';
import { Counter } from 'k6/metrics';
export const options = {
stages: [
{ duration: '2m', target: 10 }, // below normal load
{ duration: '10m', target: 10 }, // below normal load
{ duration: '2m', target: 15 }, // below normal load
{ duration: '10m', target: 15 }, // below normal load
{ duration: '2m', target: 20 }, // below normal load
{ duration: '10m', target: 20 }, // below normal load
{ duration: '2m', target: 25 }, // below normal load
{ duration: '10m', target: 25 }, // below normal load
{ duration: '2m', target: 30 }, // below normal load
{ duration: '10m', target: 30 }, // below normal load
{ duration: '10m', target: 0 }, // scale down. Recovery stage.
],
thresholds: {
// fail and abort the test if 95th percentile response goes above 500ms
// delay the threshold evaluation for 1 min to gather enought data
http_req_duration: [{ threshold: 'p(95)<1500', abortOnFail: true, delayAbortEval: '1m' }],
},
};
http.setResponseCallback(http.expectedStatuses({ min: 200, max: 399 }));
const errors = new Counter('errors');
export default function () {
const BASE_URL = 'http://192.168.1.113/'; // make sure this is not production
const res = http.get(BASE_URL, { tags: { name: 'base' } })
check(res, {
'status was 200': (r) => {
return r.status == 200
}
})
check(res, {
'error code': (r) => {
if (r.error_code != 0) {
errors.add(1, { error_code: r.error_code })
fail('error code was *not* 0');
}
return r.error_code == 0
}
})
const responses = http.batch([
['GET', `${BASE_URL}/wp-includes/blocks/navigation/style.min.css?ver=6.0`, null, { tags: { name: 'png' } }],
['GET', `${BASE_URL}/wp-includes/js/wp-emoji-release.min.js?ver=6.0`, null, { tags: { name: 'png' } }],
['GET', `${BASE_URL}/wp-content/themes/twentytwentytwo/style.css?ver=1.2`, null, { tags: { name: 'png' } }],
['GET', `${BASE_URL}/wp-includes/blocks/navigation/view.min.js?ver=009e29110e016c14bac4ba0ecc809fcd`, null, { tags: { name: 'png' } }],
['GET', `${BASE_URL}/wp-content/themes/twentytwentytwo/assets/images/flight-path-on-transparent-d.png`, null, { tags: { name: 'png' } }],
['GET', `${BASE_URL}/wp-content/themes/twentytwentytwo/assets/fonts/source-serif-pro/SourceSerif4Variable-Roman.ttf.woff2`, null, { tags: { name: 'png' } }],
['GET', `${BASE_URL}/favicon.ico`, null, { tags: { name: 'png' } }],
['GET', `${BASE_URL}/wp-includes/images/w-logo-blue-white-bg.png`, null, { tags: { name: 'png' } }],
]);
responses.forEach(res => {
check(res, {
'status was 200': (r) => {
return r.status == 200
}
})
check(res, {
'error code': (r) => {
if (r.error_code != 0) {
errors.add(1, { error_code: r.error_code })
fail('error code was *not* 0');
}
return r.error_code == 0
}
})
});
sleep(1);
}
うまく行きそうでしたが最後の最後でしきい値を超えましたね、残念。
参考情報(レスポンスタイムについて)
k6のサイトにはNielsen Norman Groupからの引用として以下の内容が記載されていました。
- 0.1秒は、システムが瞬時に反応しているとユーザーに感じさせる限界です。つまり、結果を表示する以外に特別なフィードバックは必要ありません。
- 1.0秒は、ユーザーが遅延に気付いたとしても、ユーザーの思考の流れが途切れないようにするための限界です。通常、0.1秒以上1.0秒未満の遅延の間は特別なフィードバックは必要ありませんが、ユーザーはデータを直接操作している感覚を失います。
- 10秒は、ユーザーの注意を対話に集中させ続けるための制限についてです。より長い遅延の場合、ユーザーはコンピューターが終了するのを待っている間に他のタスクを実行したいので、コンピューターがいつ完了すると予想されるかを示すフィードバックをユーザーに提供する必要があります。応答時間が大きく変動する可能性がある場合、遅延中のフィードバックは特に重要です。これは、ユーザーが何を期待できるかわからないためです。
なるほどという思いと、結構シビアだなという思い。
環境構築
テストされる側(Raspberry Pi4)
version: '3.1'
services:
nginx:
image: nginx
restart: always
ports:
- 80:80
volumes:
- wordpress:/var/www/html:ro
- ./default.conf:/etc/nginx/conf.d/default.conf
depends_on:
- wordpress
- db
wordpress:
image: wordpress:6.0.0-fpm
restart: always
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_USER: exampleuser
WORDPRESS_DB_PASSWORD: examplepass
WORDPRESS_DB_NAME: exampledb
volumes:
- wordpress:/var/www/html
depends_on:
- db
db:
image: mariadb
restart: always
environment:
MARIADB_DATABASE: exampledb
MARIADB_USER: exampleuser
MARIADB_PASSWORD: examplepass
MARIADB_RANDOM_ROOT_PASSWORD: '1'
volumes:
- db:/var/lib/mysql
volumes:
wordpress:
db:
テストする側
k6のインストール
sudo mkdir -p /root/.gnupg/
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update
sudo apt-get install -y k6
InfluxDBとGrafanaのインストール
InfluxDBは1.x系しか対応していないようです。
version: '3'
services:
influxdb:
image: influxdb:1.2
ports:
- "8086:8086"
expose:
- 8086
volumes:
- ./influxdb:/var/lib/influxdb
networks:
- influx-grafana-network
grafana:
image: grafana/grafana-oss
ports:
- "3000:3000"
volumes:
- ./grafana:/var/lib/grafana
networks:
- influx-grafana-network
user: "0"
networks:
influx-grafana-network:
external:
name: influx-grafana-network
Grafanaのダッシュボードはこちら(2587)