💭

k6で始める負荷テスト

2022/09/15に公開

はじめに

APIに対する負荷試験を実施している際、Jmeterとk6で実装し負荷試験を実施していたのですが、k6が非常に便利だったので紹介してみようと思います。

k6について

K6はオープンソースの負荷生成、測定ツールです。
https://k6.io/docs/

公式サイト参照ですが、以下のような特徴があります。

  • CLIツールであること
  • 負荷スクリプトはJavascript ES2015/ES6で記述でき、モジュールをサポートしている
  • ChecksThresholdsを備えている
    • Checks: 成功失敗のチェックを記述することができる
    • Thresholds: 閾値を設定できる

またJmeterと比較して軽量で、ローカル環境でRPS(Request per second)を増してもレスポンスの処理や結果の正しさは良好な印象でした。

ただし、注意点としてスクリプトはJavascriptで記載可能ですがNode.jsではないので、npm moduleNodeJS APIを使用するにはモジュールを作成して、ファイルからインポートしてやる必要があります。
これは後ほど作成方法など紹介しようと思います。
今回使用したコードは次のレポジトリにあります。
https://github.com/shohta-tera/k6-testing

環境

  • WSL2: Ubuntu 20

k6のインストール

Brew経由でインストールをします。Macならhomebrew, Linux, WSLならlinuxbrewで以下のコマンドを実行します。
brew install k6

実際に負荷をかけてみる

import { check } from "k6"
import http from "k6/http"

export const options = {
    thresholds: {
        http_req_failed: ["rate<0.01"],
        http_req_duration: ["p(90)<2000"]
    }
}

export default function () {
    const headers = {
        "Content-Type": "application/json"
    }

    const res = http.post(
        "https://test.k6.io",
        { headers: headers }
    )
    check(res, {
        'is_status_200': (r) => r.status === 200
    })
}

上記だけで、一回のAPIリクエストについて記述できました。非常に簡単ですね。例では、単純なHTTPリクエストですが、GraphQLへのリクエストや、特定のシナリオに沿った負荷測定実行も可能です。

export const options = {
    thresholds: {
        http_req_failed: ["rate<0.01"],
        http_req_duration: ["p(90)<2000"]
    }
}

thresholdsにて、リクエストに対するレスポンスの閾値を設定することができます。

  • http_req_failed: リクエストに失敗する割合。上記の例だと失敗リクエストが1%以下。
  • http_req_duration: レスポンスタイムの閾値。上記の例だと90パーセンタイルで2000ms以内であること。この条件としては複数設定が可能です。
    • http_req_duraion: ['p(90) < 400', 'p(95) < 800', 'p(99.9) < 2000']
      上記の例だと90パーセンタイルで400ms、95パーセンタイルで800msそして99.9パーセンタイルで2000msを条件にしています。
check(res, {
        'is_status_200': (r) => r.status === 200
    })

k6にあらかじめ用意されているcheckモジュールをインポートすることで、レスポンスに対するチェックを実施することが可能です。
上記例だと、レスポンスのステータスが200であることをチェックしており、こちらも複数条件設定することが可能です。

それでは、実際に負荷をかけてみようと思います。実行には次のようなコマンドを実行します。
k6 run simple_http.js -u 100 --rps 20 -d 1h --out json=result.json

  • -u: ユーザー数
  • --rps: Rquest per second
  • -d: 継続時間で、1h30m55sのような形式で入力する
  • --out: 結果の出力先。主につかうのは、csv, jsonなど。上記の例では、jsonで出力しています。出力形式のあとにでつないで、ファイル名を指定することで、結果を保存できます。

実行結果としては以下のようなものがCLIに表示されます。


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

  execution: local
     script: simple_http.js
     output: json (test.json)

  scenarios: (100.00%) 1 scenario, 10 max VUs, 1m0s max duration (incl. graceful stop):
           * default: 10 looping VUs for 30s (gracefulStop: 30s)


running (0m32.0s), 00/10 VUs, 160 complete and 0 interrupted iterations
default ✓ [======================================] 10 VUs  30s

     ✓ is_status_200

     checks.........................: 100.00% ✓ 160      ✗ 0   
     data_received..................: 1.9 MB  59 kB/s
     data_sent......................: 38 kB   1.2 kB/s
     http_req_blocked...............: avg=22.31ms  min=6.9µs    med=9.04µs   max=388.91ms p(90)=26.05µs  p(95)=348.49ms
     http_req_connecting............: avg=11ms     min=0s       med=0s       max=183.83ms p(90)=0s       p(95)=173.45ms
   ✓ http_req_duration..............: avg=209.15ms min=171.78ms med=179.73ms max=781.71ms p(90)=348.89ms p(95)=355.6ms 
       { expected_response:true }...: avg=209.15ms min=171.78ms med=179.73ms max=781.71ms p(90)=348.89ms p(95)=355.6ms 
   ✓ http_req_failed................: 0.00%   ✓ 0        ✗ 160 
     http_req_receiving.............: avg=17.34ms  min=70.3µs   med=163.14µs max=606.11ms p(90)=485.47µs p(95)=172.05ms
     http_req_sending...............: avg=84.98µs  min=30.2µs   med=78.7µs   max=217.3µs  p(90)=131.74µs p(95)=137.63µs
     http_req_tls_handshaking.......: avg=11.15ms  min=0s       med=0s       max=191.88ms p(90)=0s       p(95)=174.66ms
     http_req_waiting...............: avg=191.72ms min=171.53ms med=178.78ms max=443.99ms p(90)=209.34ms p(95)=257.88ms
     http_reqs......................: 160     5.001738/s
     iteration_duration.............: avg=1.94s    min=571.01ms med=1.99s    max=2.6s     p(90)=2.05s    p(95)=2.17s   
     iterations.....................: 160     5.001738/s
     vus............................: 6       min=6      max=10
     vus_max........................: 10      min=10     max=10

この中で重要なのは次が挙げられます。
checks.........................: 100.00% ✓ 160 ✗ 0
-> checkで定義した項目が成功しているか
http_req_duration..............: avg=209.15ms min=171.78ms med=179.73ms max=781.71ms p(90)=348.89ms p(95)=355.6ms
-> レスポンスに関するメトリクス
✓ http_req_failed................: 0.00% ✓ 0 ✗ 160
-> thresholdで定義したエラー率
http_reqs......................: 160 5.001738/s
-> 結果の左はトータルに投げられたリクエストで、右がRPSの実測値です。

k6からAWSモジュールを使用する

k6の負荷スクリプトは、Javascriptで記述することは可能ですが、実行環境としてNode.jsではありません。そのため、npmモジュールなどをインストールしてそれを使うなどができません。例えば、AWSのAPI GWへのリクエストを投げようとすると、署名バージョン4の署名プロセスを利用して、認証情報を付与してやる必要がありますが、aws4使えば、比較的簡単に実装できますが、自作で署名を付与するとなると些か面倒です。
そこで、browserifyを利用してnode_moduleにインストールした、AWS4をk6でも実行可能な形式に変換します。

  1. 必要モジュールのインストール
    npm install aws4 browserify
  2. AWS4の変換
    ./node_modules/browserify/bin/cmd.js ./node_modules/aws4/index.js > ./aws4.js
    これで、aws4.jsというファイルがカレントディレクトリに生成されます。
  3. 実際にk6のスクリプトでインポートする
    import aws4 from "./aws4.js"
    インポートするのは非常に簡単です。

外部npmモジュールを用いた負荷測定スクリプト

import { check } from "k6"
import http from "k6/http"
import aws4 from "./aws4.js"

const awsKey = ""
const awsSecretKey = ""
const awsSessionToken = ""

export default function () {
    const serviceName = "execute-api"
    const baseUrl = "some_domain.com"
    const path = "/api/v1/sampleEndpoint"
    const options = {
        headers: {
            "Content-Type": "application/json"
        },
        service: serviceName
    }
    const parts = path.split("?")
    options.host = baseUrl
    options.path = path
    options.region = "ap-northeast-1"
    options.method = "GET"

    aws4.sign(options, {
        accessKeyId: awsKey,
        secretAccessKey: awsSecretKey,
        sessionToken: awsSessionToken
    })

    const res = http.get(`https://${baseUrl}${path}`, {headers: options.headers})
    check(res, {
        'is_status_200': (r) => r.status === 200
    })
}

モジュールのインポート方法が通常とは異なりローカルのファイルを使うという違いを除けば、使用感などは一緒です。

ダッシュボードに結果を表示する

k6は非常に簡単で、軽量な負荷測定ツールですが、一点だけ欠点があります。デフォルトの結果表示では、レスポンス等の時系列変化がみれない。という点です。これは、似たような負荷測定ツールで、JmeterをベースとしたTaurusと比べると劣っている点であるとは思います。
そこで、k6のextensionである、xk6-dashboardをインストールしたk6をビルドして使用しようと思います。

https://github.com/szkiba/xk6-dashboard

xk6-jdashboardのビルド

事前にGoがインストールされていることが前提となります。

  1. xk6のダウンロード
    go install go.k6.io/xk6/cmd/xk6@latest
  2. バイナリのビルド
    xk6 build --with github.com/szkiba/xk6-dashboard@latest

実行方法としてはほぼほぼ一緒です。ただ、k6についてはローカルで生成されたものを利用することには注意が必要です。
./k6 run simple_http.js -u 10 --rps 3 -d 30s --out dashboard

ダッシュボード自体は、http://localhost:5665にアクセスすることで確認可能です。

終わりに

k6は軽量かつ、実装するのが非常に簡単でちょっと面倒な点もありますが、ダッシュボードによる可視化や外部npmモジュールを使用することが可能なため、強力なツールです。
また、CLIで実行するオプションが豊富であったり、結果自体をPrometheusに保存することが可能なため、CICDパイプラインで負荷テストを実装して、結果をGrafanaで確認するなども可能になり、CICDとも非常に親和性が高いツールとなっているので、GitHub Actionsなどとも組み合わせてみてもよいかもと思いました。

GitHubで編集を提案

Discussion