k6での負荷試験

k6 + firebase + graphqlで負荷試験を行いました。


以前とあるプロジェクトで負荷試験を行ったのでその内容を共有します。
利用した技術は以下です(かなり前にやった話なので記憶を辿りながら記載しています)

  • 負荷試験ツール: k6
  • API方式: GraphQL
  • 認証部分: Firebase SDK (Identity platform)
  • インフラ: GCE, CloudRun, CloudSQL, Pub/Sub, GCS

目的

GraphQLのクエリは複数のリソースを一度に取得できることからdepth制限等を設けていないと、 クエリのネストが深くなり、DBへの負荷が大きくなることがあります。 そのため、負荷試験を行い、どの程度の負荷がかかるかを確認することが目的です。

負荷試験ツール

負荷試験ツールはk6を利用しました。 k6はGoで書かれた負荷試験ツールで、シンプルな記述で負荷試験を行うことができます。

インストール

k6はバイナリをダウンロードして利用することができます。 また、Dockerイメージも提供されているので、Dockerを利用することもできます。
https://k6.io/docs/getting-started/installation

負荷試験用マシン

負荷試験用マシンはCloudBuildを利用しました。 GCEで利用している例も多いかと思いますが、CloudBuildを利用することで インスタンスの起動から負荷試験の実行、試験終了までを手軽に自動化することができます。

Seedデータについて

負荷試験にDBに大量データを流し込む必要がある場合はそれだけで時間がかかります。
高スペックで高価な負荷試験用のCloudBuildのインスタンスとは別に、低スペックで低価格なCloudBuildインスタンスを利用してデータの流し込みを行うことでコスト節約しました。

認証

負荷試験を行うにあたり、実際の利用ケースと同じくGraphQLサーバに対して認証ヘッダ付きでリクエストの送信が必要でした。 今回の例ではFirebaseのIdentity platformを利用していたのですが、公式資料等を参考に一部コードを調整したため以下に記載します。

JWTの生成

負荷テスト用のJWTを生成するために、以下のようなコードを利用しました。
createCustomToken(uid)を利用しているのがポイントで、これとIDPのsignInWithCustomTokenのエンドポイントを組み合わせることで実際のJWTが手に入ります。

なお、実際の利用シーンではクライアント側で行うJWTの取得について負荷テストに含める必要がなかったため、 以下の処理はCloudBuildのStepとしてはk6を実行する前の別Stepで行っています。 (複数ユーザのJWTをひとつのjsonにまとめてk6の実行時に読み込むようにしています)

コードサンプル

SEED作成時にIDPにテナントとユーザを作成するためのコード

import { auth } from 'firebase-admin'

// DBにSeedを流し込む
// snip...

await auth()
  .tenantManager()
  .authForTenant(createdTenant.tenantId)
  .createUser({ uid, email: `${uid}@email.com` }) // Seedのuidを利用する(実際はPromise.allで複数アカウントを作成)

JWTの入手コード

import fetch from 'cross-fetch'
import { auth } from 'firebase-admin'
import {
  getApps,
  applicationDefault,
  cert,
  initializeApp,
} from 'firebase-admin/app'

// E2Eテストで本物のJWT生成に利用します
const publicFirebaseApiKey = 'xxxxx'

// Firebaseのセットアップ
if (getApps().length === 0) {
  initializeApp({
      credential: !process.env.BUILD_ID
        ? cert('firebase-adminsdk.json')
        : applicationDefault(), // CloudBuildではADCから認証情報取得
  })
}


const getAuthToken = async (
  tenantId: string,
  uid: string
): Promise<string> => {
  const customToken = await auth()
      .tenantManager()
      .authForTenant(tenantId)
      .createCustomToken(uid)

  const res = await fetch(
      `https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=${publicFirebaseApiKey}`,
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          token: customToken,
          tenantId,
          returnSecureToken: true,
        }),
      }
  )

  const result = await res.json()
  return result.idToken
}

参考資料

  • https://k6.io/docs/examples/oauth-authentication
  • https://tech-blog.optim.co.jp/entry/2021/04/01/103000

負荷試験の実行

負荷試験の実行は以下のようなコマンドで実行します。

gcloud builds submit --region=asia-northeast1 --config path/to/load-test.yaml

シナリオ作成

負荷試験のシナリオ作成と簡易的な動作確認はローカルマシンで行いました。 シナリオ開発の序盤では以下のようなコマンドで直接実行していました。(後半ではDocker化して実行していました)

# TS --> JSへの変換は別途行なった上で以下を実行(docker runで実行する手もある)
k6 run path/to/load-test.js