Masayan tech blog.

  1. ブログ記事一覧>
  2. k6で始める負荷テスト

k6で始める負荷テスト

公開日

負荷テストとは何か

  • 測定対象のリソースに対して、機械的にHttpリクエストやWebSocket通信などを行い負荷をかけ、パフォーマンスを計測するテスト
  • 仮想ユーザー数やリクエスト数、実行時間などの要素を増減させ、その時のシステムの応答性と安定性を計測する(システムの限界点と許容限度がわかる)

負荷テストのポイント

目的を明確化する

同時接続〇〇万人で95パーセンタイルが300ms未満をクリアできるかどうかや、どれくらいの負荷をかけるとサーバーリソースがエラーを返す(限界点に達する)のか等

パフォーマンスに関する重要な指標

レイテンシ

リクエストを送ってからレスポンスが帰ってくるまでの時間。ネットワーク経由でデータが送信されるのにかかる時間。一般的には数百ミリ秒未満であることが好ましい

スループット

特定の時間にネットワークを実際に通過できるデータの平均量=リクエスト量。ビット/秒(bps)、バイト/秒(Bps)で表現される

重要なリソース指標

  • コンピューティングリソースの使用率(CPU、メモリ)
  • データベースへのクエリ数、実行時間、ロック数、待機時間
  • ネットワークI/O
  • ディスクI/O(ハードディスクなどの外部記憶装置に対するデータの読み書き操作)
  • エラーが発生した場合、その発生頻度、エラーの割合

計測値

平均値、最大値、中央値などよりもパーセンタイルで計測することが好ましい(計測値の上限/下限の数%には異常値が含まれる場合があるが、これを無視できるメリットがある)

負荷テストの分類と役割

負荷テストは一般的にそのシュチュエーションにより以下のように分類できる

  • 予備テスト
    • 再小規模で十分なパフォーマンスが得られるか
  • パフォーマンステスト
    • 通常時に十分なパフォーマンスが得られるか
  • ロードテスト
    • ピーク時に十分なパフォーマンスが得られるか
  • 限界値テスト
    • システム限界点を把握する
    • 限界を超えた場合にどのような現象が発生するか

シナリオ作成時のポイント

ユーザーの現実的な利用シーン、操作に則したシナリオを作成することが重要。ユースケースに沿ってシナリオを作成し、その中でデータの受信と送信、次のページに移るまでのユーザーの思考遅延などをsleepなどで指定することにでより意味のあるテストに近づく

主要な負荷テストツール

  • k6(GO製、JavaScriptでシナリオを書ける
  • locust(Pythonでシナリオを書ける
  • Apache JMeter(Javaでシナリオを書ける

k6

読み方はケーシックス。HTTP,WebSocketに対応。

インストール

mac

brew install k6

mac以外はこちらを参照

GETリクエスト

  • テストファイルにエントリーポイントを表すデフォルト関数を少なくとも1つ含める必要がある
  • VU(仮想ユーザー)は、すべての条件が満たされるまで、デフォルト関数内のコードを最初から最後まで順次実行し、最後に到達すると、最初から同じ処理を繰り返す

http/index.js

// 指定したulrにリクエストを送信する
import http from "k6/http";
import { check } from "k6";

export default function () {
  const res = http.get("https://httpbin.test.k6.io/");
  check(res, { "status was 200": (r) => r.status == 200 });
}

POSTリクエスト

postメソッドの第二引数にペイロードを、第三引数にリクエストヘッダーを指定できる

import http from 'k6/http';

export default function () {
  const url = 'http://example.com/login';
  const payload = JSON.stringify({
    email: 'hoge@gmail.com',
    password: 'xfeeajepjege',
  });

  const params = {
    headers: {
      'Content-Type': 'application/json',
    },
  };

  http.post(url, payload, params);
}

テスト実行

k6 run http/index.js

出力結果

k6 run http/index.js

          /\      |‾‾| /‾‾/   /‾‾/
     /\  /  \     |  |/  /   /  /
    /  \/    \    |     (   /   ‾‾\
   /          \   |  |\  \ |  (‾)  |
  / __________ \  |__| \__\ \_____/ .io

  execution: local
     script: http/index.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)


     ✓ status was 200

     checks.........................: 100.00% ✓ 10
     data_received..................: 15 kB   27 kB/s
     data_sent......................: 458 B   817 B/s
     http_req_blocked...............: avg=377ms    min=377ms    med=377ms    max=377ms    p(90)=377ms    p(95)=377ms
     http_req_connecting............: avg=184.85ms min=184.85ms med=184.85ms max=184.85ms p(90)=184.85ms p(95)=184.85ms
     http_req_duration..............: avg=182.52ms min=182.52ms med=182.52ms max=182.52ms p(90)=182.52ms p(95)=182.52ms
       { expected_response:true }...: avg=182.52ms min=182.52ms med=182.52ms max=182.52ms p(90)=182.52ms p(95)=182.52ms
     http_req_failed................: 0.00%   ✓ 01
     http_req_receiving.............: avg=331µs    min=331µs    med=331µs    max=331µs    p(90)=331µs    p(95)=331µs
     http_req_sending...............: avg=71µs     min=71µs     med=71µs     max=71µs     p(90)=71µs     p(95)=71µs
     http_req_tls_handshaking.......: avg=190.18ms min=190.18ms med=190.18ms max=190.18ms p(90)=190.18ms p(95)=190.18ms
     http_req_waiting...............: avg=182.12ms min=182.12ms med=182.12ms max=182.12ms p(90)=182.12ms p(95)=182.12ms
     http_reqs......................: 1       1.7848/s
     iteration_duration.............: avg=560.14ms min=560.14ms med=560.14ms max=560.14ms p(90)=560.14ms p(95)=560.14ms
     iterations.....................: 1       1.7848/s


running (00m00.6s), 0/1 VUs, 1 complete and 0 interrupted iterations
default ✓ [======================================] 1 VUs  00m00.6s/10m0s  1/1 iters, 1 per V

特に重要な指標

テスト実行後、コンソールに各実行結果が表示される。特に重要な指標としては以下。その他の指標についてはこちらを参考されたし

指標名

内容

checks

宣言した check() アサーションのうち、実際に完了した数

http_reqs

送信された HTTP リクエストの総数 (スループット)

http_req_duration

リクエストを送信してからレスポンスが返ってくるまでの合計時間(レイテンシ)

iteration_duration

デフォルト関数(シナリオ)の実行にかかった合計時間

iterations

デフォルト関数(シナリオ)が呼び出された回数の集計

vus

アクティブ仮想ユーザー数

iteration_durationはsleep分などで待機時間があると、その分増加する

コンソールに出力される数字

テストの実行中に何かしらの変数なりをconsole.logでログを出力すると以下のように、INFO[000X]のような内容が表示される。

INFO[0003] Connected                                     source=console
INFO[0003] Disconnected                                  source=console
INFO[0003] Response:                                     source=console

INFO[0003] Response:                                     source=console
INFO[0004] Connected                                     source=console
INFO[0004] Disconnected                                  source=console

INFO[0005] Connected                                     source=console
INFO[0005] Disconnected                                  source=console
INFO[0005] Response:                                     source=console

この数字は、シナリオとして作成したdurationとtargetの設定によって決定される

例えば、以下のようなステージ設定だと、最初の2秒経過後に同時実行数が1、その後次の2秒間経過までで2にする

stages: [
        { duration: "2s", target: 1 },
        { duration: "2s", target: 2 },
        { duration: "2s", target: 0 },

この場合、[0003]は2秒経過以降4秒経過までに計1人の仮想ユーザーが実行し、[0005]は4秒経過以降6秒経過までに計2人の仮想ユーザーが実行する。(0秒経過から2秒経過までは仮想ユーザーが0人なので、実行されていない)

INFO[0003] responseBody: 200 OK                          source=console
INFO[0005] responseBody: 200 OK                          source=console
INFO[0005] responseBody: 200 OK                          source=console

オプション

以下のように、実行時にvus(vitual users)の数などをパラメータとして指定できるし、ソースコード上でも定義できる

k6 run --vus 2 http/index.js

optionsという名称で変数を定義し、vusやdurationなどのパラメータを指定できる(要export)

並列度(vus)

optionsにvus(vitual users)の数を指定できる

import http from 'k6/http';

export let options = {
  vus: 2
};
export default function () {
  http.get('http://example.com/foo');
}

startVUs

テスト開始時点の仮想ユーザー数を指定できる

export const options = {
  scenarios: {
    sample_scenario: {
      startVUs: 0,

duration

テストの実行時間を指定できる

import http from 'k6/http';

export let options = {
  duration: '10s'
};
export default function () {
  http.get('http://example.com/foo');
}

しきい値(threshold)

メトリクスに基づいて、どのような条件下でテストが成功または失敗とみなされるかを設定できる。

export let options = {
  stages: [
    { duration: "10s", target: 20 },
    { duration: "10s", target: 10 },
    { duration: "10s", target: 0 },
  ],
  thresholds: {
    http_req_duration: ["p(95)<200"], // 全てのリクエストにおいて95パーセンタイルが200msec以下であるか
  },
};

Stages

  • 仮想ユーザー数をピーク値まで徐々に増やし(ランプアップ) その後、仮想ユーザー数を徐々に減らしていき、テストの実行を終了する(ランプダウン)やり方が一般的
  • stagesを使用することで段階的にパラメータを変動させることが可能
  • 挙動としてはステージごとに指定した実行時間、アクティブな仮想ユーザーの数を変えながら可能な限り多くのリクエストを実行する
  • 詳細はこちらを参照
// 最初の20秒間は1VUから20VUへ。次の60秒間でゆっくりと40VUに移行。最後の10秒間で40VUから0VUに移行
export let options = {
  stages: [
    { duration: '20s', target: 20 },
    { duration: '1m', target: 40 },
    { duration: '10s', target: 0 },
  ],
};

Scenarios

scenariosを使用することで、上記で紹介したような設定値の内容を1つのシナリオとしてまとめることができる。

https://k6.io/docs/using-k6/k6-options/reference/#scenarios

Executor

k6 Executorが指定することで比較的簡単にはシナリオを定義することが可能。例えばk6 Executorの1つであるramping-vusは、可変数の仮想ユーザーが、指定された時間内にできるだけ多くの反復を実行するというシナリオであり、他にもシナリオ作成をサポートするExecutorが複数存在する

https://k6.io/docs/using-k6/scenarios/executors/

env

Scenariosに環境変数を指定できる

export const options = {
  scenarios: {
    sample_scenario: {
      executor: "ramping-vus",
      stages: [
        { duration: "3s", target: 1 },
        { duration: "3s", target: 2 },
        { duration: "3s", target: 0 },
      ],
      env: {
        HOSTNAME: "https://sample.com",
      },
    },
  },
  summaryTrendStats,
};

定義した環境変数は以下のようにテスト内で使用できる

export default function () {
  const endpoint = `${__ENV.HOSTNAME}/foo/bar?hoge=yaa`
  ...
}

noVUConnectionReuse

テスト実行時にVUの反復間でTCPコネクションを再利用するかどうかを指定することが可能。trueの場合再利用しない

export const options = {
  noVUConnectionReuse: true

summaryTrendStats

テスト結果として表示するメトリクスを指定できる

export const options = {
  summaryTrendStats: [
    "avg",
    "min",
    "med",
    "max",
    "p(95)",
    "p(99)",
    "p(99.99)",
    "count",
  ],

アサート

  • checkメソッドにより、テスト実行結果のアサートが可能
  • Httpレスポンスには、bodyやstatusなどが含まれているので、これに対して期待する結果が返却されているかをアサートする
  • 一般的なアサートとは異なり、失敗しても負荷テストの実行を中断することはなく、そのまま継続する
export default function () {
  const res = http.get("https://hoge.com/foo");

  check(res, {
    "status was 200": (r) => r.status == 200,
  });
}

WebSocket

k6/wsモジュールにより、可能。また、connectしてからdisconnectするまでの一連の処理を実行するようにしてあげないと、no iterationsということで、1つもテストが完了していないと判定されてしまうので注意

export default function () {
  const url = `${__ENV.WS_HOSTNAME}/ws/foo/bar/`;

  const res = ws.connect(url, {}, function (socket) {
    socket.on("open", () => {
      console.log("Connected");

      socket.send(
        JSON.stringify({
          token: `${__ENV.TOKEN}`,
          action: "something_action",
        })
      );
    });

    socket.on("close", () => console.log("Disconnected"));

    socket.on("message", (data) => {
      console.log("Message received", data);
    });

    socket.on("error", function (e) {
      console.error("An unexpected error occured: ", e.error());
    });
  });

  console.log(`Response: ${res.body}`);

  check(res, { "Connected successfully": (r) => r && r.status === 101 });
  
  socket.setTimeout(function () {
    console.log("Closing the socket");
    socket.close();
  }, 5000);
}

分散テスト

k6-operatorにより、Kubernetesを利用した分散テストが実現できる

手順

dockerデスクトップでEnable Kubernetesにチェックをつけ、Kubernetesを有効化する

ソースコードを取得、ルートディレクトリに移動

git clone https://github.com/grafana/k6-operator && cd k6-operator

カスタムリソース定義をクラスターに適用

make install

利用可能なカスタムリソース定義にk6s.k6.ioが追加されているか確認

kubectl get crd

NAME                     CREATED AT
k6s.k6.io                2023-11-04T11:22:01Z

以下でデプロイすると、クラスター内にコントローラーのサービスアカウントやロールなどに関するいくつかのリソースが作成される

make deploy

configmapの作成。(作成しなくても、config/samples/k6_v1alpha1_configmap.yamlを使用できるが、自前で用意したい場合は以下で作成できる)

kubectl create configmap --from-file

configmapのデプロイ

kubectl apply -f config/samples/k6_v1alpha1_configmap.yaml

利用できるconfigmapを確認し、config/samples/k6_v1alpha1_configmap.yamlのmetadata.nameで指定した名称の項目が追加されていればOK

kubectl get configmap

NAME               DATA   AGE
k6-test            1      13h

k6-testのconfigmapの詳細を確認。config/samples/k6_v1alpha1_configmap.yamlで定義した内容が表示されていればOK

kubectl describe configmap k6-test

Name:         k6-test
Namespace:    default
Labels:       <none>
Annotations:  <none>

Data
====
test.js:
----
import http from 'k6/http';
import { Rate } from 'k6/metrics';

テストの実行

kubectl apply -f config/samples/k6_v1alpha1_k6.yaml

ポッドの一覧で処理の進捗を確認。テストが完了したら、statusがcompletedとなる

kubectl get pods

NAME                          READY   STATUS      RESTARTS   AGE
k6-sample-1-6qvb7             0/1     Running     0          17s
k6-sample-10-smqcd            0/1     Running     0          16s
k6-sample-2-mwsrf             0/1     Running     0          17s
k6-sample-3-fzm5p             0/1     Running     0          17s


↓

NAME                          READY   STATUS      RESTARTS   AGE
k6-sample-1-trrjr             0/1     Completed   0          60s
k6-sample-10-tc7dl            0/1     Completed   0          60s
k6-sample-2-szqkg             0/1     Completed   0          60s
k6-sample-3-tc2j6             0/1     Completed   0          60s

・・・

特定のポッドでのテスト実行結果を確認

kubectl logs k6-sample-2-szqkg

✓ http response status code is 200

     checks.........................: 100.00% ✓ 5200   
     data_received..................: 543 kB  9.0 kB/s
     data_sent......................: 69 kB   1.1 kB/s
   ✓ failed_requests................: 0.00%   ✓ 0520 
     http_req_blocked...............: avg=14.55ms  min=8.91µs   med=30.66µs  max=407.39ms p(90)=92.05µs  p(95)=186.69µs

・・・

podのクリーンアップ。テストを再実行したい場合はこのコマンドで一度削除してから再度kubectl applyする

kubectl delete -f config/samples/k6_v1alpha1_k6.yaml

configmapの削除

kubectl delete configmap <namespace-name> 

podの一覧を表示

kubectl get pods

オペレータ関連のリソースを全て削除

make delete

まとめ

いかがでしたでしょうか。本記事では負荷テストの概要とk6を用いて負荷テストを実装するための方法やポイントについて紹介しました。大規模なアプリケーションや高パフォーマンスが求められるシステムでは負荷テストは必須のテストになりますのでぜひ参考にしてみてください