k6でシナリオ作成する時にハマったこと

この記事は HRBrain Advent Calendar 第7日目の記事です。

https://qiita.com/advent-calendar/2020/hrbrain

k6

https://k6.io/docs/

k6とは負荷試験のシナリオを書くためのツールです。JavaScriptやTypeScriptで負荷シナリオを書くことができます。k6を使って少し複雑なシナリオを書く時に色々とハマったので備忘録としてまとめました。

実行シナリオを環境変数で変更する

シナリオを変更する際、コードの変更や複雑なオプションの追加をせずに変更したかったので、環境変数で制御できるようにしました。

こちらを参考に実装しました。

https://community.k6.io/t/execution-of-new-scenarios-from-cli/796

// script.ts
import { Options, Scenario } from 'k6/options';

const possibleScenarios = JSON.parse(open(`./scenarios.json`))
let scenarios: Record<string, Scenario> = {}
if (__ENV.SCENARIOS === 'all') {
  scenarios = possibleScenarios
} else {
  (__ENV.SCENARIOS || 'default').split(',').forEach((scenario) => {
    if (possibleScenarios[scenario]) {
      scenarios[scenario] = possibleScenarios[scenario]
    }
  })
}
export const options: Options = { scenarios }

options をexportしておくと実行時にそのオプションを使用してくれます。 k6で環境変数を使う場合、 __ENV.HOGE のように参照します。

例えば以下のような設定を書いた場合、

// scenarios.json
{
    "default": {
        "executor": "constant-vus",
        "vus": 30,
        "duration": "5m",
    },
    "rapid": {
        "executor": "ramping-vus",
        "stages": [
            { "duration": "10s", "target": 100 },
            { "duration": "5m", "target": 100 },
        ]
    }
}

以下のように実行シナリオを変更できます。

  • k6 run script.js -> default を実行
  • SCENARIOS=rapid k6 run script.js -> rapid を実行
  • SCENARIOS=default,rapid k6 run script.js -> default,rapid を実行
  • SCENARIOS=all k6 run script.js -> 全て(default,rapid)を実行

レスポンスのcookieを取得する

ログインから通してシナリオ実行したかったので、シナリオ内でログインし、レスポンスからアクセストークンを取得するような関数を書きました。

// script.ts
import { check } from 'k6';
import * as http from 'k6/http';

const login = (id: string, password: string): string => {
  const response = http.post(
    'http://localhost/login',
    JSON.stringify({
      id,
      password,
    }),
    {
      headers: {
        'Content-Type': 'application/json',
      },
    },
  )

  const checkRes = check(response, {
    'status is 200': (r) => r.status === 200,
  })

  // トークンを返す
  if (!checkRes) {
    return ''
  }
  const cookieName = 'cookie_name'
  if (!response.cookies[cookieName]) {
    return ''
  }
  if (response.cookies[cookieName].length === 0) {
    return ''
  }
  return response.cookies[cookieName][0].value
}

以後のシナリオでは、このトークンをAuthorizationヘッダにセットします。

GraphQLを叩く

k6でGraphQLを叩く方法は公式で紹介されています。

https://k6.io/blog/load-testing-graphql-with-k6

が、queryを全て直書きするのはちょっと大変です。コピペするにしても、プロダクトコードが変わるたびに合わせて修正するのも面倒です。

自分のプロダクトでは以下のようなコードをschemaから自動生成しているので、これを使用して良い感じに叩きたいところです。

// graphqlTypes.ts
import gql from 'graphql-tag';

export const GetMemberDocument = gql`
  query getMember {
    member {
      id
      name
    }
  }
`

この GetMemberDocument を使ってリクエストするためには以下のようにします。

// script.ts
import { print } from 'graphql';
import * as http from 'k6/http';

export default function () {
  const token = 'hoge'
  const response = http.post(
    'http://localhost/query',
    JSON.stringify({
      query: print(GetMemberDocument),
      variables: {},
    }),
    {
      headers: {
        Authorization: `Bearer ${token}`, 
        'Content-Type': 'application/json',
      },
    },
  )

  check(response, {
    'status is 200': (r) => r.status === 200,
  })

  console.log(JSON.parse(String(response.body || '')).data)
}

graphql パッケージの print 関数を使うと、 GetMembersDocument(DocumentNode オブジェクト) をquery文字列に変換できます。 これを使ってpostリクエストすればOKです。

--compatibility-mode=base をつける

実行時に --compatibility-mode=base オプションをつけるとメモリ消費を抑えることができるので、より大きな負荷をかけることができます。

https://k6.io/docs/testing-guides/running-large-tests#k6-options

k6は実行時に内部でES6からES5に変換していますが、このオプションをつけるとその処理が省かれるので、省コストで実行されるようになります。よって、このオプションをつけるためには予めES5にコンパイルしておく必要があります。

公式にwebpackを使った変換の例がありますが、JavaScript (ES6) からの変換しかなかったので、TypeScriptからの変換は自分で設定する必要があり、webpack初心者なので結構ハマりました。

// webpack.config.js
const path = require('path');
module.exports = {
    mode: 'development',
    entry: './src/script.ts',
    output: {
        path: path.resolve(__dirname, 'src'),
        libraryTarget: 'commonjs',
        filename: 'script.js',
    },
    module: {
        rules: [
            {
                test: /\.ts$/,
                use: 'ts-loader',
            }
        ],
    },
    stats: {
        colors: true
    },
    target: ['web', 'es5'],
    externals: /k6(\/.*)?/,
    resolve: {
        modules: ["node_modules"],
        extensions: [
            '.ts', '.js',
        ],
    },
}
// tsconfig.json
{
    "compilerOptions": {
        "target": "es5",
        ...
    }
}

これで yarn webpack すると k6 run script.js --compatibility-mode=base で実行できます。

おわり

k6もそうですが、そもそも負荷試験に関わるのが初めてで中々大変でした。 今後書きたいことができれば追記します。