負荷テストツールK6を試す

はじめに

負荷テストのツールを何かしら勉強したいなと思って、K6というツールがあるというのを知って良さそうに感じたのでとりあえず動かしてみるところまでやってみようと思います。

K6とは

K6Load Impactという負荷テストのサービスを作っていた会社が、その経験を活かして作ったOSSみたいです。その機能に以下のようなものがあります。

Virtual User

K6にはVirtual Users(VUs)という概念があります。
Virtual Usersはそれぞれが分けられた環境で、並行でテストスクリプトを実行してくれます。
また、Virtual Userはリアルユーザの真似をするようなリクエストを送ることも可能みたいです。

使ってみる

早速使ってみたいと思います。
K6を実行するためにはそのツールセットをインストールする必要がありますが今回はDockerを利用してK6を実行したいと思います。

環境

実行環境は以下の通りです。

$ uname -srvmpio
Linux 5.4.0-58-generic #64-Ubuntu SMP Wed Dec 9 08:16:25 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

$ lsb_release -a
LSB Version:    core-11.1.0ubuntu2-noarch:security-11.1.0ubuntu2-noarch
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.1 LTS
Release:    20.04
Codename:   focal

$ docker version
Client: Docker Engine - Community
 Version:           19.03.12
 API version:       1.40
 Go version:        go1.13.10
 Git commit:        48a66213fe
 Built:             Mon Jun 22 15:45:36 2020
 OS/Arch:           linux/amd64
 Experimental:      false

Server: Docker Engine - Community
 Engine:
  Version:          19.03.12
  API version:      1.40 (minimum version 1.12)
  Go version:       go1.13.10
  Git commit:       48a66213fe
  Built:            Mon Jun 22 15:44:07 2020
  OS/Arch:          linux/amd64
  Experimental:     true
 containerd:
  Version:          1.2.13
  GitCommit:        7ad184331fa3e55e52b890ea95e65ba581ae3429
 runc:
  Version:          1.0.0-rc10
  GitCommit:        dc9208a3303feef5b3839f4323d9beb36df0a9dd
 docker-init:
  Version:          0.18.0
  GitCommit:        fec3683

事前準備

インストール

前述の通り、K6を実行するにはそのツールセットをインストールする必要があります。様々な環境でインストール、動作させることが可能です。

Linux(Ubuntu/Debian)

DabianベースのLinuxの場合はプライベートのdebリポジトリからインストールすることができます。

sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 379CE192D401AB61
echo "deb https://dl.bintray.com/loadimpact/deb stable main" | sudo tee -a /etc/apt/sources.list
sudo apt-get update
sudo apt-get install k6

Docker

Dockerイメージloadimpact/k6が公開されており、そのイメージを用いたテストの実行も可能です。

docker pull loadimpact/k6

Mac

Macではbrewを使ってインストールすることができるみたいです。

brew install k6

バイナリでのインストール

GitHubのページからバイナリを取得することも可能です。
リンクからバイナリをダウンロードしてPATHを通して通してください。

テストするアプリを用意する

インストールが終わったところで、テスト対象のアプリを準備しておきます。
今回は環境が用意できればなんでもよいので個人的に1番使い慣れてるSpringを使ってアプリを作ろうと思います。
Spring Initializrを使ってアプリを作成します。
設定は以下のように

f:id:yuya_hirooka:20201215083536p:plain

出来上がったプロジェクトをエディタ等で開き作成されているApplicationクラスを以下のように修正します。

@SpringBootApplication
@RestController
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @GetMapping("/hello")
    public String hello(){
        return "hello";
    }
}

アプリを起動してcURLでエンドポイントへアクセスするとHelloという文字列が返ってきます。

$ curl localhost:8080/hello
hello

テストコードを書く

それでは準備ができたところで実際にテストスクリプトを書いて行きたいと思います。 K6スクリプトを記述する場合基本的なスクリプトの構成は以下のようになります。

// init code

export default function() {
  // vu code
}

K6ではdefalt functionを定義してやる必要があり、これが一般的に言うメイン関数のようなテストコードのエントリーポイントとなります。
defalt functionの中のコードのことをVU codeと呼び、外側のコードのことをinit codeと呼びます。
ここで、Virtual Usersを利用して、並行化されるのはVU codeであり、init codeは一回のみ実行されます。VU codeの中ではHTTPなどのリクエストを送信しそのメトリクスを計測することは可能ですが、ローカルのファイルシステムを読み込んだり、他のモジュールを呼んだりすることはできません。それらはinit codeで実行することが必要です。

以上のことを踏まえて、リクエストを一回実行して1秒だけまつスクリプトを記述します。

k6script.js

import http from 'k6/http';
import { sleep } from 'k6';

export default function () {
  
  http.get('http://${HOST_IP}:8080/hello');
  sleep(1);

}

テストコードが記述できたので早速実行してみます。 実行は以下のようにコマンドを叩きます。

$ docker run -i loadimpact/k6 run - < ${SCRIPT_NAME}.js

今回の場合は次のようになります。

$ docker run -i loadimpact/k6 run - < k6script.js

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

  execution: local
     script: -
     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.0s), 1/1 VUs, 0 complete and 0 interrupted iterations
default   [   0% ] 1 VUs  00m01.0s/10m0s  0/1 iters, 1 per VU

running (00m01.0s), 0/1 VUs, 1 complete and 0 interrupted iterations
default ✓ [ 100% ] 1 VUs  00m01.0s/10m0s  1/1 iters, 1 per VU

    data_received..............: 118 B 114 B/s
    data_sent..................: 89 B  86 B/s
    http_req_blocked...........: avg=1.67ms  min=1.67ms  med=1.67ms  max=1.67ms  p(90)=1.67ms  p(95)=1.67ms 
    http_req_connecting........: avg=1.61ms  min=1.61ms  med=1.61ms  max=1.61ms  p(90)=1.61ms  p(95)=1.61ms 
    http_req_duration..........: avg=2ms     min=2ms     med=2ms     max=2ms     p(90)=2ms     p(95)=2ms    
    http_req_receiving.........: avg=83.92µs min=83.92µs med=83.92µs max=83.92µs p(90)=83.92µs p(95)=83.92µs
    http_req_sending...........: avg=72.25µs min=72.25µs med=72.25µs max=72.25µs p(90)=72.25µs p(95)=72.25µs
    http_req_tls_handshaking...: avg=0s      min=0s      med=0s      max=0s      p(90)=0s      p(95)=0s     
    http_req_waiting...........: avg=1.84ms  min=1.84ms  med=1.84ms  max=1.84ms  p(90)=1.84ms  p(95)=1.84ms 
    http_reqs..................: 1     0.96781/s
    iteration_duration.........: avg=1s      min=1s      med=1s      max=1s      p(90)=1s      p(95)=1s     
    iterations.................: 1     0.96781/s
    vus........................: 1     min=1 max=1
    vus_max....................: 1     min=1 max=1

コンソールに実行結果が出力されているのがわかります。
ちょっとだけ細かく見てみると、送信したデータ量(B/s)、受信したデータ量(B/s)、リクエストでブロックされた時間、接続にかかった時間、等々の平均値やパーセントタイルの値などが表示されていますね。

VirtualUsersを追加して、テストの実行時間を変更する

次に、Virtual Usersを追加してリクエストを並行で送ってみるようにしてみたいと思います。 方法は以下の4つほどあります

  • テスト実行時オプション(--vus and -duration)として渡す
  • 環境変数として定義しておく
  • .jsonの設定ファイルを作成し、実行時に読み込ませる
  • init calloptionsJsonオブジェクトを定義する

今回は4つ目のJsonオブジェクトを定義する方でやろうと思います。
具体的には、「10 users で10秒間リクエストを送る」ように、先程のコードを以下のように書き換えます。

import http from 'k6/http';
import { sleep } from 'k6';

export let options = {
  vus: 10,
  duration: '10s',
};

export default function () {
  
  http.get('http://${HOST_IP}:8080/hello');
  sleep(1);

}

option.vusoption.durationをそれぞれinit codeの領域で設定しています。
テストを実行すると先ほどと違って、10並行で、10秒間リクエストが送られていることがわかります。

$ docker run -i loadimpact/k6 run - < k6script.js

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

  execution: local
     script: -
     output: -

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


running (01.0s), 10/10 VUs, 0 complete and 0 interrupted iterations
default   [   9% ] 10 VUs  00.9s/10s

running (02.0s), 10/10 VUs, 10 complete and 0 interrupted iterations
default   [  19% ] 10 VUs  01.9s/10s


(略)

default   [  99% ] 10 VUs  09.9s/10s

running (10.1s), 00/10 VUs, 100 complete and 0 interrupted iterations
default ✓ [ 100% ] 10 VUs  10s

    data_received..............: 12 kB  1.2 kB/s
    data_sent..................: 8.9 kB 880 B/s
    http_req_blocked...........: avg=32.42µs min=2.11µs   med=11.88µs  max=855.09µs p(90)=49.84µs  p(95)=172.34µs
    http_req_connecting........: avg=19.37µs min=0s       med=0s       max=814.16µs p(90)=4.29µs   p(95)=144.09µs
    http_req_duration..........: avg=3.67ms  min=704.32µs med=3.94ms   max=8.48ms   p(90)=6.71ms   p(95)=7.35ms  
    http_req_receiving.........: avg=99.37µs min=16.55µs  med=103.15µs max=223.89µs p(90)=173.99µs p(95)=182.34µs
    http_req_sending...........: avg=49.94µs min=9.15µs   med=47.83µs  max=334.41µs p(90)=70.58µs  p(95)=87.42µs 
    http_req_tls_handshaking...: avg=0s      min=0s       med=0s       max=0s       p(90)=0s       p(95)=0s      
    http_req_waiting...........: avg=3.52ms  min=658.68µs med=3.75ms   max=8.3ms    p(90)=6.52ms   p(95)=6.92ms  
    http_reqs..................: 100    9.895886/s
    iteration_duration.........: avg=1s      min=1s       med=1s       max=1s       p(90)=1s       p(95)=1s      
    iterations.................: 100    9.895886/s
    vus........................: 10     min=10 max=10

ここまでで、ざっくりとK6の利用方法は把握できた気がします。

テスト結果の出力

テスト結果はデフォルトではコンソールに出力されますが、以下のような出力先を変更するプラグインがいくつか用意されています。

この他にもNew Relicなどもあります。

オプション

最後に、その他にどんなことができるのかざっくり把握するためオプションで気になったところをいくつかまとめたいと思います。
全量はここで確認することができます。

Duration

Durationでは、テストが実行される時間を設定することができます。
実行時間中はそれぞれのVUsでスクリプトがループして呼ばれ続けます。
例えば以下のような設定の場合3分間vu codeがループで実行され続けます。

export let options = {
  duration: '3m',
};

Iterations

IterationsオプションはVUsがそれぞれ何度スクリプトを実行するかを指定することができます。
この値は、VUsで単純に割り算され、例えば以下のような設定である場合10Vusがそれぞれ10回のリクエストを実行します。

export let options = {
  vus: 10,
  iterations: 100,
};

No VU Connection Reuse

No VU Connection ReuseではTCPをVUsの実行の中でTCPコネクションを再利用するかをbooleanで設定できます。 設定は以下のように行います。

export let options = {
  noVUConnectionReuse: true,
};

RPS

RPSではVUsをまたいだ秒間での最大リクエスト数を設定することができます。

export let options = {
  rps: 500,
};

Scenarios

Scenariosでは、1つ以上の実行パターンを定義することができます。シナリオは以下のような特徴があります。

  • 同じスクリプトに対して複数のシナリオを定義できる
  • それぞれのシナリオは個別のVUsやスケジュールパターンを持つことができる
  • それぞれのシナリオは直列でも並行でも実行することができる
  • シナリオごとに違った環境変数やメトリクスタグをセットすることができる

より詳細な情報はこちらを参考にしてください。
利用イメージは以下のようになります。

export let options = {
  scenarios: {
    my_api_scenario: {
      // arbitrary scenario name
      executor: 'ramping-vus',
      startVUs: 0,
      stages: [
        { duration: '5s', target: 100 },
        { duration: '5s', target: 0 },
      ],
      gracefulRampDown: '10s',
      env: { MYVAR: 'example' },
      tags: { my_tag: 'example' },
    },
  },
};

Stages

StagesではVUsの上昇や下降を制御することができます。
例えば以下のような設定の場合、最初の3分間でVUsを1から10まで上昇させ5分間そのままリクエストを続けその他とに10から35ユーザ10分間で増やしていき、最後の3分でVUsを0まで減らします。

export let options = {
  stages: [
    { duration: '3m', target: 10 },
    { duration: '5m', target: 10 },
    { duration: '10m', target: 35 },
    { duration: '3m', target: 0 },
  ],
};

参考資料