Go製CLIツールGatling Commanderによる負荷試験実施の自動化

OGP画像

はじめに

こんにちは、ML・データ部MLOpsブロックの岡本です。

MLOpsブロックでは機械学習モデルの実験基盤の作成、機械学習モデルを組み込んだAPI・Batchの開発・運用・保守を行なっています。APIを開発する際には負荷試験を実施し、本番環境で運用する際に求められるスループット・レイテンシを達成できるか確認します。

MLOpsブロックでの従来の負荷試験実施には人手を要する定型的な作業が複数ありました。また頻繁に行う作業でもありトイルとなっていました。

本記事ではMLOpsブロックで抱えていた負荷試験実施の課題と、解決のために開発したOSSのCLIツール、Gatling Commanderについて紹介します。Gatling Commanderが負荷試験の実施におけるトイル削減の一助になれば幸いです。

github.com

目次

背景・課題

従来の負荷試験実施の方法

本節ではMLOpsブロックで行なっていた従来の負荷試験実施の方法について、利用するツールと実施方法を説明します。

分散負荷試験ツール(Gatling Operator)の説明

MLOpsブロックでは負荷試験の実施に、Gatlingで分散負荷試験を行うKubernetes OperatorGatling Operatorを利用します。

GatlingはWebアプリケーション向けのOSS負荷試験フレームワークです。負荷試験シナリオに基づき負荷試験を行い、HTML形式の結果レポートを自動で生成します。

Kubernetes Operatorは、Kubernetes APIの拡張であるCustom Resourceと、その状態管理を担うCustom ControllerによりKubernetesを拡張するための仕組みです。Workloadsの目的の状態を定義したCustom ResourceをKubernetesクラスタにデプロイすると、Custom Controllerは制御ループを通じてその目的の状態に近づくように制御します。WorkloadsはKubernetes上で稼動するアプリケーションであり、制御ループはあるシステムの状態を制御する終了状態のないループです。

Kubernetes Operatorを利用するとKubernetes上で稼動するアプリケーションであるWorkloadsのライフサイクル管理を自動化できます。

Gatling Operatorは分散負荷試験の内容を定義したCustom ResourceであるGatling Resourceを扱うKubernetes Operatorです。Gatling ResourceがKubernetesクラスタにデプロイされると、Gatling Resourceを監視するCustom ControllerであるGatling Controllerが目的の状態に近づくように制御します。Gatling Operatorの仕組みにより、一連の分散負荷試験のタスクを自動化できます。

Gatling Operatorの詳細は、TECH BLOGの「Gatlingによる分散負荷試験を自動化するKubernetesオペレーターGatling Operatorの紹介」をご参照ください。

techblog.zozo.com

従来の実施方法

MLOpsブロックでは従来、次の流れでGatling Operatorを使った負荷試験を実施していました。

  1. Gatlingの負荷試験シナリオを作成
  2. 負荷試験シナリオを含むコンテナイメージのBuild&Push(アップロード)
  3. Gatling ResourceのKubernetesマニフェストを作成
  4. Gatling ResourceをKubernetesクラスタへデプロイし、負荷試験を開始
  5. 負荷試験対象APIが稼働するコンテナのメトリクスを取得
  6. 負荷試験の実行状況を確認
  7. 負荷試験結果(Gatling Report)をGoogle Sheetsへ記録

これらの作業は作業者のローカル環境で行っていました。

Gatling Operatorを使った従来の負荷試験では、各作業で利用できるシェルスクリプトはいくつかあるものの、作業者の手作業が多く必要でした。

シェルスクリプトを使ったKubernetesクラスタの操作

Gatling ResourceのデプロイなどKubernetesクラスタへの操作は、kubectlコマンドを利用します。kubectlコマンドはKubernetes APIを使用してKubernetesクラスタと通信するためのCLIツールです。

負荷試験の作業では、kubectlコマンドを使ってKubernetesクラスタを操作し、Gatling Resourceのオブジェクト作成と負荷試験対象コンテナのメトリクスを取得します。kubectlコマンドでの操作では操作対象クラスタの切り替えなどの作業も行います。

負荷試験でGatlingが自動生成するHTML形式の結果レポートは、Gatling ResourceのKubernetesマニフェストで指定したクラウドストレージにアップロードされます。一方で負荷試験結果は負荷試験を行うGatlingコンテナのログにも出力されます。このログをkubectl logsコマンドで取得し、ログから抽出することで、クラウドストレージを確認せずとも簡易的な形式で負荷試験結果を確認できます。

負荷試験対象コンテナのメトリクスはkubectl topコマンドで取得します。kubectl topコマンドはKubernetesクラスタ内にMetrics Serverをインストールすると利用できます。kubectl topコマンドによりPod・Node・コンテナのCPU・メモリの使用量を取得できます。

このように負荷試験用のシェルスクリプトではkubectlコマンドを使って、Gatling Resourceのオブジェクトを作成していました。またGatlingコンテナのログから結果の抽出と、負荷試験対象コンテナのメトリクスを取得し、これらの値をまとめた負荷試験結果を出力していました。

負荷試験時には作業者がこれらのシェルスクリプトを実行していました。

手作業での負荷試験結果の記録

MLOpsブロックでは負荷試験結果の記録先としてGoogle Sheetsを利用しています。Google Sheetsは複数の人が同時に閲覧・編集できるため、チームでの作業に便利です。

Gatlingは負荷試験ごとに結果レポートを自動生成します。このレポートは負荷試験ごとに作成されるため、実施したすべての負荷試験結果を一覧し、比較するには不便でした。

Google Sheetsであれば負荷試験結果を表形式でまとめることができ、Googleアカウントへの権限付与で共有できるためチーム内への展開も簡単です。そのため負荷試験結果の最終的な記録先としてGoogle Sheetsを利用しています。

作業者はシェルスクリプトが出力する負荷試験結果をGoogle Sheetsにコピー&ペーストして記録していました。

負荷試験実施の課題

従来の負荷試験実施の方法は作業者の手間が多く、負荷試験全体でかかる作業工数が課題でした。作業者は各作業を負荷試験シナリオごとに行います。個々は小さな手間でも積み重なると大きくなり、負荷試験全体で1〜2日程度の作業工数を要していました。

負荷試験でAPIのパフォーマンスを計測する際、様々な条件を試すために設定パラメータの値が異なる複数の負荷試験シナリオを用意します。負荷試験シナリオごとに、前節(従来の実施方法)で説明した3.Gatling ResourceのKubernetesマニフェストを作成から7.負荷試験結果(Gatling Report)をGoogle Sheetsへ記録までの作業を繰り返します。これらは定型作業ですが作業数が多く、負荷試験シナリオの数に比例して作業者の手間も増えます。

このように従来の負荷試験実施では、手作業の項目が多いことで作業者の工数を要していました。本節ではこれらの項目の中でも、特に作業者の大きな手間になっていた項目について説明します。

設定値の変更の手間

従来の負荷試験実施の方法では、負荷試験シナリオごとに作業者が設定パラメータの値を変更していました。

負荷試験シナリオの設定パラメータはGatlingのテストシナリオを記述したファイルであるSimulationで定義します。SimulationはGatlingのコンテナイメージに含めます。設定パラメータの値はGatling ResourceのKubernetesマニフェストで指定します。Gatlingはこの値をGatlingコンテナの環境変数から読み込みます。

負荷試験シナリオ間でSimulationが共通する場合、同一のコンテナイメージを利用できるため、シナリオごとにコンテナイメージのBuild&Push作業は不要です。一方で設定パラメータの値については負荷試験シナリオ間で異なるため、シナリオごとにGatling ResourceのKubernetesマニフェストで値の変更が必要です。

例えば負荷試験で頻繁にある、APIの最大スループットを計測する場合を考えます。この場合秒間リクエスト数の設定パラメータ値を変更した、複数のシナリオで負荷試験を実施します。

作業者は秒間リクエスト数を変更するために、負荷試験シナリオごとにGatling ResourceのKubernetesマニフェストを変更する必要がありました。負荷試験シナリオごとに実行結果を確認し、レイテンシに問題がなければ秒間リクエスト数の値を増やして次のシナリオを実行していました。

リクエスト数の変更を繰り返してAPIの最大スループットを測るため、作業者が負荷試験の設定パラメータの値を変更する作業回数は多くなります。特に新規開発したAPIでは、どの程度のスループットを捌けるのか事前にわからず、作業工数がかかっていました。

このように負荷試験において設定パラメータの値の変更は頻繁に行われるにもかかわらず、スクリプトでの自動化ができていなかったため作業者の手間になっていました。

実行状況の確認の手間

同一のAPIを対象に負荷試験を行う場合、前のシナリオで負荷試験が完了してから、次のシナリオでの負荷試験を実行します。従来の方法では、前の負荷試験の実行状況をシナリオごとに、作業者が確認していました。

Gatling Operatorによって分散負荷試験の実行は自動化されているため、シナリオごとの負荷試験の実行中に作業者による作業は必要ありません。この間に作業者は他のタスクを進められます。しかしシナリオあたりの負荷試験の実行時間が短いと、実行状況の確認のために他のタスクを頻繁に中断するため、作業者は効率的に作業できていませんでした。

このように実行状況の確認は、作業者にとって頻繁に行う必要のある割り込み作業であり、手間になっていました。

結果の記録の手間

従来の負荷試験実施の方法では、シナリオごとに作業者が結果を記録していました。

前節(手作業での負荷試験結果の記録)の通り、作業者はシェルスクリプトが出力した、簡易的な負荷試験結果の値をGoogle Sheetsへ記録します。作業者はGoogle Sheetsへ手作業でコピー&ペーストして結果を記録していました。

また前節(設定値の変更の手間)で例として挙げたAPIの最大スループットを測る場合では、結果の記録に加えて結果の値の確認も必要です。作業者は負荷試験結果を確認し、目標レイテンシを超えていないか、エラーが発生していないかなど、事前に決められた閾値を元に次のシナリオを実行するかどうかを判断していました。

このように負荷試験結果の確認およびGoogle Sheetsへの結果の記録も、シナリオごとに作業者が手作業で行い、作業者の手間となっていました。

Go製CLIツール(Gatling Commander)による負荷試験実施の自動化

MLOpsブロックでは負荷試験実施にかかる作業者の工数削減のために、CLIツールGatling Commanderを開発し、OSSとして公開しました。

Gatling Commanderは、Gatling Operatorを利用した負荷試験実施における一連の作業を自動化します。

前節(従来の実施方法)で説明した従来の負荷試験の流れのうち、3.Gatling ResourceのKubernetesマニフェストを作成から7.負荷試験結果(Gatling Report)をGoogle Sheetsへ記録までの作業はGatling Commanderにより自動化されます。

本節ではGatling Commanderについて機能と実装の概要、そして実際にMLOpsブロックで利用して感じた効果を説明します。

Gatling Commanderの機能

Gatling Commanderは次の機能を持ちます。

  • 負荷試験用コンテナイメージをBuild&Push
  • 負荷試験シナリオごとにGatlingオブジェクトを作成
  • 過負荷時に負荷試験を自動停止
  • 負荷試験結果、コンテナメトリクスを記録
  • 負荷試験の実行状況を確認
  • 負荷試験の完了を通知

これらの機能に加えて、Gatling Commanderの設定ファイルには複数の負荷試験シナリオを記述可能です。この設定ファイルをYAML形式で記述し、コマンドを実行するとGatling Commanderによりすべての負荷試験シナリオが自動実行されます。

Gatling Commanderは、従来の負荷試験実施の方法でシナリオの数に比例して作業者の手間が増加していた課題を解消し、負荷試験実施にかかる全体の作業工数を削減しています。本節ではGatling Commanderが提供する機能について説明します。

負荷試験用コンテナイメージを自動作成

Gatling Commanderは負荷試験用コンテナイメージのBuild&Push作業を自動化します。

Gatling CommanderはGatlingのSimulation等のファイルを含めて負荷試験用のコンテナイメージをBuildします。またGatling Commanderの設定ファイルで指定したコンテナレジストリに、BuildしたコンテナイメージをPushします。

Gatling CommanderはBuild&Pushしたコンテナイメージを、複数の負荷試験シナリオ間で共通のコンテナイメージとして利用します。

事前にBuild&Pushしたコンテナイメージを使う場合は、コマンド実行時にオプションを指定してBuild&Pushを省略可能です。Build&Pushを省略する場合、Gatling Commanderは設定ファイルで指定したBuild&Push済みのコンテナイメージを利用します。

負荷試験用Gatlingオブジェクトを自動作成

コンテナイメージのBuild&Push後、Gatling CommanderはKubernetesクラスタ内にGatling Resourceのオブジェクトを作成します。Kubernetesクラスタ内にGatlingオブジェクトが作成されると、Gatling OperatorはGatlingコンテナを稼働させるPodを作成し分散負荷試験を実施します。

Gatling Commanderは、設定ファイルで指定したベースとなるGatling ResourceのKubernetesマニフェストを、GoのGatling構造体のオブジェクトに読み込みます。次に負荷試験シナリオごとに値が異なるフィールドの値を、設定ファイルに指定された値で上書きします。

ベースとなるGatling ResourceのKubernetesマニフェストの例は次のとおりです。

apiVersion: gatling-operator.tech.zozo.com/v1alpha1
kind: Gatling
metadata:
  name: <config.yaml overrides this field> # will be overrided by services[].name field value in config.yaml. ex: sample-service
  namespace: gatling
spec:
  generateReport: true
  generateLocalReport: true
  notifyReport: false
  cleanupAfterJobDone: false
  podSpec:
    gatlingImage: <config.yaml overrides this field> # will be overrided by built Gatling Image URL or imageURL field value in config.yaml. ex: asia-docker.pkg.dev/project_id/foo/bar/gatlinge-image-name-prefix-YYYYMMDD
    rcloneImage: rclone/rclone
    resources:
      requests:
        cpu: "7000m"
        memory: "4G"
      limits:
        cpu: "7000m"
        memory: "4G"
    serviceAccountName: "gatling-operator-worker-service-account"
    affinity:
      nodeAffinity:
        requiredDuringSchedulingIgnoredDuringExecution:
          nodeSelectorTerms:
            - matchExpressions:
                - key: cloud.google.com/gke-nodepool
                  operator: In
                  values:
                    - "gatling-operator-worker-pool"
    tolerations:
      - key: "dedicated"
        operator: "Equal"
        value: "gatling-operator-worker-pool"
        effect: "NoSchedule"
  cloudStorageSpec:
    provider: "gcp"
    bucket: "report-storage-bucket-name"
  notificationServiceSpec:
    provider: "slack"
    secretName: "gatling-notification-slack-secrets"
  testScenarioSpec:
    parallelism: <config.yaml overrides this field> # will be overrided by services[].scenarioSpecs[].testScenarioSpec.parallelism field value. ex: 1
    simulationClass: <config.yaml overrides this field> # will be overrided by services[].scenarioSpecs[].testScenarioSpec.simulationClass field value. ex: SampleSimulation
    env: # will be overrided by services[].scenarioSpecs[].testScenarioSpec.env[] field value. ex: `env: [{name: ENV, value: "dev"}, {name: CONCURRENCY, value: "20"}]`
      - name: <config.yaml overrides this field>
        value: <config.yaml overrides this field>

上記のGatling ResourceのKubernetesマニフェストの例で、<config.yaml overrides this field>と記述があるフィールドの値は、Gatling Commanderの設定ファイルの値で負荷試験シナリオごとに上書きされます。

例えば負荷試験コンテナイメージを指定するspec.podSpec.gatlingImageフィールド・GatlingのSimulationのクラス名や環境変数など、負荷試験の具体的な設定値を記述するフィールドを配下に持つspec.testScenarioSpecフィールドの値は上書き対象です。これらのフィールドの値はGatling Commanderの設定ファイルの値で負荷試験シナリオごとに上書きされます。

Gatling Commanderの設定ファイルの記述例は次のとおりです。

gatlingContextName: gatling-cluster-context-name
imageRepository: gatling-image-stored-repository-url
imagePrefix: gatlinge-image-name-prefix
imageURL: "" # (Optional) specify image url when using pre build gatling container image
baseManifest: config/base_manifest.yaml
gatlingDockerfileDir: gatling
startupTimeoutSec: 1800 # 30min
execTimeoutSec: 10800 # 3h
slackConfig:
  webhookURL: slack-webhook-url
  mentionText: <@targetMemberID>
services:
  - name: sample-service
    spreadsheetID: sample-sheets-id
    failFast: true
    targetPercentile:
    targetLatency:
    targetPodConfig:
      contextName: target-pod-context-name
      namespace: sample-namespace
      labelKey: run
      labelValue: sample-api
      containerName: sample-api
    scenarioSpecs:
      - name: case-1
        subName: 10req/sec
        testScenarioSpec:
          simulationClass: SampleSimulation
          parallelism: 1
          env:
            - name: CONCURRENCY
              value: "10"
            - name: ENV
              value: "dev"
            - name: DURATION
              value: "180"
      - name: case-1
        subName: 20req/sec
        testScenarioSpec:
          simulationClass: SampleSimulation
          parallelism: 1
          env:
            - name: CONCURRENCY
              value: "20"
            - name: ENV
              value: "dev"
            - name: DURATION
              value: "180"

Gatling Commanderの設定ファイルでは、負荷試験シナリオごとに上書きするフィールド値を1つの設定ファイル内に記述できます。複数の負荷試験シナリオごとの設定値を1つの設定ファイルに集約することで、実行するシナリオを一覧しやすくなります。

上記の設定ファイルの例でservices[].scenarioSpecs[]フィールドには2つの要素が記述されています。このフィールドの各要素は負荷試験シナリオごとに固有の設定値を持ちます。

Gatling Commanderはコマンド実行時に、設定ファイルからservices[].scenarioSpecs[].testScenarioSpecフィールドの値を読み取ります。こうして読み取った値で負荷試験シナリオごとに、ベースとなるGatling ResourceのKubernetesマニフェストの対象フィールド値を上書きします。この値の上書きは、ベースとなるGatling ResourceのKubernetesマニフェストを読み込んだGatling構造体のオブジェクトに対して行われます。

負荷試験シナリオごとに固有な設定値をGatling Commanderの設定ファイルに指定することで、Gatling ResourceのKubernetesマニフェストをシナリオごとに用意する必要がなくなります。Gatling ResourceのKubernetesマニフェストについて、複数の負荷試験シナリオ間で同一のフィールド値の指定が共通化されるため、作業者はシナリオごとに固有の設定値のみGatling Commanderの設定ファイルへ記述すれば良くなります。フィールドが共通化されることにより、値を変更するフィールドが絞られ、設定値の変更漏れを防ぐことができます。

このようにGatling Commanderは、ベースとなるGatling ResourceのKubernetesマニフェスト・Gatling Commanderの設定ファイルから、負荷試験シナリオごとにGoのGatling構造体のオブジェクトを作成します。こうして作成したGatling構造体のオブジェクトを元に、Kubernetesクラスタ内に負荷試験シナリオごとのGatlingオブジェクトを作成します。

Gatling Commanderはこれらの処理により、負荷試験用Gatlingオブジェクトを自動作成します。

過負荷時に負荷試験を自動停止

Gatling Commanderは設定ファイルに停止条件を指定し、ある負荷試験シナリオの結果が条件に一致した時、後続のシナリオの実行を自動停止できます。事前に停止条件を指定することで、負荷試験シナリオを余計に実行することを防ぎます。

前節(設定値の変更の手間)で挙げた、複数のシナリオを切り替えて秒間リクエスト数を増やしながら負荷試験を繰り返し、APIの最大スループットを測る場合を考えます。

この場合、ある負荷試験シナリオで目標レイテンシを達成できないと、そのシナリオよりもリクエスト数が多い後続のシナリオでは更なるレイテンシの悪化が予想でき、追加実行はあまり意味がないです。こうした場合にGatling Commanderによる負荷試験の自動停止が活用できます。

負荷試験の自動停止の条件には次の項目が利用可能です。

  • Failしたリクエストがある
  • 特定のパーセンタイル値で指定した目標レイテンシの閾値を超過する

設定ファイルに停止条件を指定すると、Gatling Commanderは各シナリオで負荷試験の実行が完了した際に、負荷試験結果の値を確認します。確認した値が指定したこの条件に一致する場合、Gatling Commanderは後続のシナリオの負荷試験を自動実行せずに停止します。

前節(Gatling Commanderの機能)の通り、Gatling Commanderの設定ファイルには複数の負荷試験シナリオを記述できます。これらの負荷試験シナリオはserviceという単位で記述します。

service単位で記述することで、同一の負荷試験対象APIに対する複数のシナリオの設定値をグループ化できます。同一のservice配下に複数の負荷試験シナリオの設定値を記述する場合、これらはGatling Commanderにより設定ファイル内での記載順で実行されます。

前節(負荷試験用Gatlingオブジェクトを自動作成)で説明したGatling Commanderの設定ファイルの例で、serviceごとの設定値は次の箇所です。

services:
  - name: sample-service
    spreadsheetID: sample-sheets-id
    failFast: true
    targetPercentile:
    targetLatency:
    targetPodConfig:
      contextName: target-pod-context-name
      namespace: sample-namespace
      labelKey: run
      labelValue: sample-api
      containerName: sample-api
    scenarioSpecs:
      - name: case-1
        subName: 10req/sec
        testScenarioSpec:
          simulationClass: SampleSimulation
          parallelism: 1
          env:
            - name: CONCURRENCY
              value: "10"
            - name: ENV
              value: "dev"
            - name: DURATION
              value: "180"
      - name: case-1
        subName: 20req/sec
        testScenarioSpec:
          simulationClass: SampleSimulation
          parallelism: 1
          env:
            - name: CONCURRENCY
              value: "20"
            - name: ENV
              value: "dev"
            - name: DURATION
              value: "180"

Galing Commanderの設定ファイルでは、services[]フィールドの配下の要素にserviceごとの設定値を指定します。負荷試験の自動停止の条件はservices[].failFastフィールドとservices[].targetPercentileservices[].targetLatencyフィールドで指定します。

この例ではservices[].nameフィールドの値に負荷試験対象のservice名としてsample-serviceを指定しています。このserviceのservices[].scenarioSpecs[]フィールドには2つの負荷試験シナリオの設定値を指定しています。同一service内の負荷試験シナリオは順次実行されます。この例だとGatling Commanderはname: case-1subName: 10req/secsubName: 20req/secの順番でシナリオごとの負荷試験を実行します。

この例でservices[].failFastフィールドの値はtrueです。そのためname: case-1subName: 10req/secのシナリオの結果でFailしたリクエストがあると、後続のname: case-1subName: 20req/secのシナリオの負荷試験は実行しません。

負荷試験の自動停止の例としてGatling Commanderを利用し、前節(設定値の変更の手間)で挙げたAPIの最大スループットを測る場合を考えます。次のYAMLはこの場合のGatling Commanderの設定ファイルの記述例です。

gatlingContextName: gatling-cluster-context-name
imageRepository: gatling-image-stored-repository-url
imagePrefix: gatlinge-image-name-prefix
imageURL: "" # (Optional) specify image url when using pre build gatling container image
baseManifest: config/base_manifest.yaml
gatlingDockerfileDir: gatling
startupTimeoutSec: 1800 # 30min
execTimeoutSec: 10800 # 3h
slackConfig:
  webhookURL: slack-webhook-url
  mentionText: <@targetMemberID>
services:
  - name: sample-service
    spreadsheetID: sample-sheets-id
    failFast: true
    targetPercentile: 99
    targetLatency: 500
    targetPodConfig:
      contextName: target-pod-context-name
      namespace: sample-namespace
      labelKey: run
      labelValue: sample-api
      containerName: sample-api
    scenarioSpecs:
      - name: check-max-throughput
        subName: 50req/sec
        testScenarioSpec:
          simulationClass: SampleSimulation
          parallelism: 1
          env:
            - name: CONCURRENCY
              value: "50"
            - name: ENV
              value: "dev"
            - name: DURATION
              value: "180"
      - name: check-max-throughput
        subName: 100req/sec
        testScenarioSpec:
          simulationClass: SampleSimulation
          parallelism: 1
          env:
            - name: CONCURRENCY
              value: "100"
            - name: ENV
              value: "dev"
            - name: DURATION
              value: "180"
      - name: check-max-throughput
        subName: 125req/sec
        testScenarioSpec:
          simulationClass: SampleSimulation
          parallelism: 1
          env:
            - name: CONCURRENCY
              value: "125"
            - name: ENV
              value: "dev"
            - name: DURATION
              value: "180"
      - name: check-max-throughput
        subName: 150req/sec
        testScenarioSpec:
          simulationClass: SampleSimulation
          parallelism: 1
          env:
            - name: CONCURRENCY
              value: "150"
            - name: ENV
              value: "dev"
            - name: DURATION
              value: "180"
      - name: check-max-throughput
        subName: 175req/sec
        testScenarioSpec:
          simulationClass: SampleSimulation
          parallelism: 1
          env:
            - name: CONCURRENCY
              value: "175"
            - name: ENV
              value: "dev"
            - name: DURATION
              value: "180"

この例ではGatling Commanderの設定ファイルで停止条件として、services[].targetPercentile99services[].targetLatency500を指定しています。これにより、あるシナリオの負荷試験結果で指定した目標レイテンシ(99%ile値で500ミリ秒)の閾値を超えると、後続のシナリオの負荷試験は実行されません。またGatling Commanderの設定ファイルのservices[].scenarioSpecs[]フィールドの要素にはリクエスト数が50・100・125・150・175req/secに対応する複数の負荷試験シナリオの設定値を記述しています。

Gatling Commanderはこれらの負荷試験シナリオを設定ファイルの記載順で実行するため、小さいリクエスト数のシナリオを先に記述します。このように設定値を記述すると、Gatling Commanderはあるシナリオの負荷試験で目標レイテンシの閾値を超えるまで、順にリクエスト数を増やして負荷試験を実行します。もし125req/secのシナリオで負荷試験結果が目標レイテンシの閾値を超えると、Gatling Commanderは150req/sec以降のシナリオを実行せずに負荷試験を終了します。

このようにGatling Commanderでは事前に設定ファイルで停止条件を指定し、負荷試験結果が指定した条件と一致した場合に後続の負荷試験を自動停止できます。

負荷試験結果・コンテナメトリクスを自動記録

Gatling Commanderは設定ファイルで指定したGoogle Sheetsに、負荷試験結果と負荷試験対象コンテナのメトリクスを自動で記録します。

Gatlingコンテナが稼働するPodの実行が終了すると、Gatling OperatorはGatlingの結果レポートを指定したクラウドストレージにアップロードします。Gatling Commanderはこのクラウドストレージにアップロードされた結果レポートを取得します。

またGatling Commanderは負荷試験の実行中に、負荷試験対象コンテナのメトリクスを一定間隔で取得し続けます。このメトリクスはKubernetesクラスタのMetrics APIから取得します。そして負荷試験の実行完了後に、Gatling Commanderは取得したコンテナメトリクスの平均値を計算します。

続いてGatling Commanderは取得した結果レポートと、計算したコンテナメトリクスの平均値から負荷試験結果を生成します。生成した負荷試験結果を、Gatling Commanderの設定ファイルで指定されたGoogle Sheetsへ記録します。

次に示す画像が、Google Sheetsへの負荷試験結果の記録例です。

負荷試験結果の記録例

負荷試験結果の記録先となるGoogle SheetsはGatling Commanderの設定ファイルでserviceごとに指定できます。また同一service内では、services[].scenarioSpecs[].nameフィールドの値と実行日を基にしたシート名で、記録先シートを自動作成します。

負荷試験の完了を通知

Gatling Commanderでは設定ファイルに通知先を指定して、すべての負荷試験シナリオが完了したことを通知できます。通知先はSlackを利用でき、メンバーIDを指定することで特定メンバーへのメンションも可能です。

次に示す画像はSlack通知の例です。

Slack通知の例

この画像の例は、Gatling Commanderで実行した負荷試験が正常終了した場合の通知です。

Gatling Commanderは複数の負荷試験をまとめて実行可能であり、1回のコマンド実行あたりの負荷試験全体の実行時間は長くなります。このように事前にSlack通知を設定することで、作業者はGatling Commanderの実行完了時にSlackのメンションですぐに気が付けます。

Gatling Commanderの実装

本節ではGatling Commanderについて、利用技術、処理の概要、主な機能の実装として、Kubernetesオブジェクトの操作の実装・Goroutinesによる並列処理の実装を説明します。

利用技術

Gatling CommanderはGoで実装しています。

従来の負荷試験実施の方法では、負荷試験の際に必要な一部の操作をシェルスクリプトとして実装していました。そのため開発時に元々あったシェルスクリプトの機能を拡張し、課題を解決するアプローチも検討しました。しかし機能の拡張のしやすさを考えると、別の言語で新たに実装した方が良いと判断し、元々シェルスクリプトで行っていた処理も含めて新たにGoでCLIツールを実装しました。

Goはclient-goapimachineryapiなどKubernetes周辺の開発で利用できるモジュールが充実しており、Kubernetes関連のツールの実装と相性が良いです。またGatling OperatorもGoで実装されており、Gatling Resourceに対応するGoの構造体はGatling Operatorのリポジトリで定義されています。これらの理由からGatling Commanderの開発言語にはGoを採用しました。

またGatling Commanderは、CLIを作成するためのインタフェースを提供するGoのモジュールであるCobraをベースに実装しています。Cobraを利用することでCLIインタフェースの実装に工数をかけずに開発ができました。

次にGoogle Sheetsへの結果の記録にはGoogle Sheets APIを利用しています。

Google Sheets APIへのリクエストには、GoのモジュールであるGoogle APIs Client Libraryを使用しています。このモジュールはGoogle Cloudサービスへアクセスする機能を提供します。Gatling Commanderはこのモジュールのsheetsパッケージを利用して、Google Sheets APIへリクエストを送り、Google Sheetsへ負荷試験結果を記録します。

処理の概要

Gatling Commanderは負荷試験実施の一連の作業を、次に示す各処理で実装しています。これらの一連の処理は次の流れで行います。

  1. Gatling Commanderの設定ファイルの読み込み
  2. 負荷試験用コンテナイメージをBuild&Push
  3. 負荷試験シナリオに対応するGatling構造体のオブジェクトを作成
  4. 負荷試験対象APIが稼働するコンテナのリソース割り当ての値を取得
  5. KubernetesクラスタにGatlingオブジェクトを作成
  6. 負荷試験の実行中に、負荷試験対象APIが稼働するコンテナのメトリクスを取得
  7. 負荷試験の実行後に、アップロードされた負荷試験の結果レポートをクラウドストレージから取得
  8. 負荷試験結果をGoogle Sheetsへ書き込み
  9. 次の負荷試験シナリオの負荷試験を実行

同一service内に複数のシナリオがある場合、3.負荷試験シナリオに対応するGatling構造体のオブジェクトを作成から9.次の負荷試験シナリオの負荷試験を実行までの処理をシナリオごとに繰り返します。またserviceごとの、3.負荷試験シナリオに対応するGatling構造体のオブジェクトを作成から9.次の負荷試験シナリオの負荷試験を実行までの処理は並列で実行します。

Kubernetesオブジェクトの操作の実装

Gatling CommanderはGoのモジュールを利用してKubernetesのオブジェクトを操作します。

具体的には次の操作をします。

  • Gatlingオブジェクトの取得・作成・削除
  • 負荷試験対象APIが稼働するコンテナのメトリクスを取得

Kubernetesのオブジェクトを操作するためにはKubernetes APIへリクエストを送ります。GoでKubernetes APIへリクエストを送るために、各Kubernetes Resourceに対応するClientパッケージを利用します。Clientパッケージを利用することで、Goのオブジェクトのメソッドを通してAPIへのHTTPリクエストを送れます。

前述したclient-goモジュールでは、PodやDeploymentなどKubernetesの標準Resourceを扱うClientを提供します。またKubernetesのControllerを開発する際に利用されるcontroller-runtimeモジュールでもClientを提供します。

Gatling Commanderではcontroller-runtimeclientパッケージでClientを初期化し、Kubernetesのオブジェクトを操作します。

clientパッケージを用いて、Kubernetes Resourceを操作するには、操作対象のResourceのSchemeを追加する必要があります。SchemeはKubernetes Resourceについて、Kubernetes APIで扱うKubernetesオブジェクトと対応するGoの構造体をマッピングします。操作対象のKubernetes ResourceのSchemeを追加すると、clientパッケージで初期化したClientから対象のResourceを操作できます。

Gatling Commanderでは次の実装でClientを初期化します。簡略化のため一部処理の実装を省略しています。

import (
    gatlingv1alpha1 "github.com/st-tech/gatling-operator/api/v1alpha1"
    "k8s.io/apimachinery/pkg/runtime"
    utilruntime "k8s.io/apimachinery/pkg/util/runtime"
    clientgoscheme "k8s.io/client-go/kubernetes/scheme"
    ctrlClient "sigs.k8s.io/controller-runtime/pkg/client"
    ctrlConfig "sigs.k8s.io/controller-runtime/pkg/client/config"
  ...省略
)

func InitClient(k8sCtxName string) (ctrlClient.Client, error) {
    scheme := runtime.NewScheme()

    utilruntime.Must(clientgoscheme.AddToScheme(scheme))
    utilruntime.Must(gatlingv1alpha1.AddToScheme(scheme))

    k8sConfig, err := ctrlConfig.GetConfigWithContext(k8sCtxName)
    if err != nil {
        return nil, err
    }
    cl, err := ctrlClient.New(k8sConfig, ctrlClient.Options{
        Scheme: scheme,
    })
  ...省略
}

各Kubernetes ResourceのSchemeは、各Resourceに対応する構造体を定義したGoのモジュールで提供されます。各モジュールが提供するAddToScheme()関数にSchemeオブジェクトを渡すことで、Kubernetes ResourceごとのSchemeがSchemeオブジェクトに追加されます。

Clientはcontroller-runtimeモジュールのclientパッケージが提供するNew()関数で初期化できます。上記の実装ではcl変数が初期化したClientです。SchemeオブジェクトをNew()関数の引数に渡すことで、ClientはSchemeを追加したKubernete Resourceについて、メソッドを通してリクエストを送れるようになります。

初期化したClientのCreate()メソッドを利用すると、次の実装でGatlingオブジェクトを作成できます。簡略化のため一部処理の実装を省略しています。

func CreateGatling(ctx context.Context, cl ctrlClient.Client, gatling *gatlingv1alpha1.Gatling) error {
  ...省略
    if err := cl.Create(ctx, gatling); err != nil {
        return err
    }
    return nil
}

この実装のCreateGatling()関数では引数clで、初期化したClientを受け取っています。引数clCreate()メソッドの引数には、Gatling構造体オブジェクトのポインタを渡して実行します。Create()メソッドの実行によりKubernetes APIにリクエストを送り、KubernetesクラスタにGatlingオブジェクトを作成します。

このようにGoのcontroller-runtime等のモジュールを活用して、簡単にKubernetes Resourceの操作を実装できました。

Goroutinesによる並列処理の実装

Gatling CommanderではGoが提供するGoroutinesを活用して、複数箇所で処理を並列実行しています。

GoroutinesはGoのランタイムにより管理される軽量なスレッドです。goキーワードを関数の前につけることで、その関数を他のコードと別のスレッドで実行できます。

Gatling CommanderでのGoroutinesの主な利用用途は次の2つです。

  • serviceごとの負荷試験の並列実行
  • 実行した負荷試験の終了の待機処理と、負荷試験対象コンテナのメトリクス取得処理の並列実行

Gatling Commanderでは次の実装でserviceごとの負荷試験を並列実行しています。簡略化のためここでは負荷試験の実行処理等の実装を省略しています。

   wg := new(sync.WaitGroup)
    for _, service := range config.Services {
        wg.Add(1)
        go func(ctx context.Context, s cfg.Service) {
            defer wg.Done()
            // 負荷試験の実行処理
            ...省略
        }(ctx, service)
    }
    wg.Wait()

上記の実装の中でconfig変数はGatling Commanderの設定ファイルに対応する構造体のオブジェクトです。configオブジェクトのServicesフィールドでは各serviceの全ての負荷試験シナリオを含む設定値を持ちます。

forブロックの中でgoキーワードを無名関数の前につけて実行しています。この関数にはgoキーワードがついているため、serviceごとに呼び出し元のスレッドとは別のスレッドで実行されます。この関数の中では、Gatling Resourceオブジェクトの作成など負荷試験の根幹的な処理をします。

上記の実装では、呼び出し側のスレッドで実行したGoroutinesの完了を待つためにwg変数を作成しています。wg変数はsyncパッケージのWaitGroupのポインタ型変数です。

WaitGroup型が提供するWait()メソッドを利用すると、実行したGoroutineの完了まで呼び出し側のスレッドで処理を待機できます。Wait()メソッドはWaitGroupオブジェクトの持つカウンタが0になるまで処理をブロックします。またAdd()メソッドはこのカウンタの値を増やし、Done()メソッドはこの値を減らします。

上記の実装では、Goroutinesの起動時に呼び出し側でAdd()メソッドを呼び、その後Wait()メソッドを呼んでいます。それぞれのGoroutines内でdeferキーワードをつけて、Done()メソッドを呼んでいます。関数にdeferキーワードをつけることで実行が遅延され、無名関数のブロックを抜ける前に必ずDone()メソッドが実行されます。Wait()メソッドではカウンタが0になるまで処理をブロックするため、起動したすべてのGoroutines内でDone()メソッドが呼ばれるまで処理を待機します。

このようにGatling CommanderではGoroutinesを利用して、serviceごとの負荷試験を並列実行しています。

またGatling Commanderでは、実行した負荷試験の終了の待機処理と、負荷試験対象コンテナのメトリクス取得処理を並列で実行します。serviceごとの負荷試験の並列実行の実装と異なる点は、起動したGoroutinesを関数の呼び出し元のスレッドから終了する点です。

このようなGoroutines間の連携ではGoのChannelsを利用できます。Channelsを利用すると、異なるGoroutinesで実行している関数同士で、特定の型の値を送受信してやり取りできます。

実行した負荷試験の終了を待機する関数では、負荷試験の実行状況を一定間隔で確認し続けます。そして負荷試験の実行終了を確認すると、並列実行していた負荷試験対象コンテナのメトリクスを取得する関数に、負荷試験の実行終了を知らせます。
それぞれの関数は異なるGoroutinesとして実行されているため、関数間でのやり取りにChannelsを利用しています。

Gatling Commanderでは次の実装で、負荷試験の実行終了の待機と、負荷試験対象コンテナのメトリクス取得を並列実行しています。

   wg := new(sync.WaitGroup)
    wg.Add(1)

    informJobFinishCh := make(chan bool, 1)
    metricsUsageCh := make(chan kubeapiTools.MetricsField, 1)

    go kubeapiTools.FetchContainerMetricsMean(ctx, wg, metricsCl, metricsUsageCh, informJobFinishCh, targetPodConfig)

    err = gatlingTools.WaitGatlingJobRunning(ctx, k8sGatlingClient, gatling, waitExecTimeout, informJobFinishCh)
    if err != nil {
        return nil, fmt.Errorf("failed to wait gatling job running, %v", err)
    }

    close(informJobFinishCh)

    wg.Wait()

    close(metricsUsageCh)

    metricsUsageMean, ok := <-metricsUsageCh
    if !ok {
        fmt.Fprintf(os.Stderr, "metricsUsageCh value is empty, so each metricsUsage field value is 0")
    }

この実装では、負荷試験対象コンテナのメトリクスを取得する関数がFetchContainerMetricsMean()、実行した負荷試験の終了を待機する関数がWaitGatlingJobRunning()です。FetchContainerMetricsMean()関数の前にはgoキーワードがあり、WaitGatlingJobRunning()関数を実行するスレッドとは別のスレッドで実行されます。

WaitGatlingJobRunning()関数を実行するスレッドからFetchContainerMetricsMean()関数を実行するGoroutinesを終了するために利用するChannelsがinformJobFinishCh変数です。FetchContainerMetricsMean()関数とWaitGatlingJobRunning()関数ではどちらも引数にinformJobFinishCh変数を受け取ります。

WaitGatlingJobRunning()関数では負荷試験が終了した際にこのinformJobFinishCh変数へ値を送信します。一方でFetchContainerMetricsMean()関数ではinformJobFinishCh変数に書き込まれた値を受信します。

Gatling Commanderでは次の実装で、WaitGatlingJobRunning()関数内でChannelsへ値を送信します。簡略化のためここでは負荷試験の実行状況の確認処理を省略しています。

func WaitGatlingJobRunning(
    ctx context.Context,
    cl ctrlClient.Client,
    gatling *gatlingv1alpha1.Gatling,
    timeout int32,
    jobFinishCh chan bool,
) error {
    defer func() { jobFinishCh <- true }()
    for {
        // 負荷試験の実行状況の確認処理
        ...省略
    }
}

WaitGatlingJobRunning()関数ではinformJobFinishCh変数の値を引数のjobFinishChで受け取ります。そしてChannelsであるjobFinishCh引数へ値を送信します。jobFinishCh引数への値の送信はdeferキーワードをつけた無名関数内で実行しています。deferキーワードによりWaitGatlingJobRunning()関数では、関数ブロックを抜ける前に必ずjobFinishCh引数への値の書き込みが実行されます。

Gatling Commanderでは次の実装で、FetchContainerMetricsMean()関数内でChannelsから値を受信します。簡略化のためここではコンテナメトリクスの取得処理等を省略しています。

func FetchContainerMetricsMean(
    ctx context.Context,
    wg *sync.WaitGroup,
    cl metricsClientset.Interface,
    resultCh chan MetricsField,
    receiveGatlingFinishedCh chan bool,
    podConfig cfg.TargetPodConfig,
) {
    defer wg.Done()
    ...省略
    for {
        select {
        case <-ctx.Done():
            return
        case <-receiveGatlingFinishedCh:
            // 取得したコンテナメトリクスの平均値を計算
            // 計算したコンテナメトリクスの値を呼び出し側に送信
            ...省略
            return
        default:
            // コンテナメトリクスの取得の処理
            ...省略
        }
    }
}

FetchContainerMetricsMean()関数内でinformJobFinishCh変数の値を、引数のreceiveGatlingFinishedChで受け取ります。そしてChannelsであるreceiveGatlingFinishedCh引数から値を受信します。

FetchContainerMetricsMean()関数では、無限ループとなるforブロックの中でselectブロックを使用して処理を条件分岐しています。WaitGatlingJobRunning()関数でChannelsに値が送信されると、case <-receiveGatlingFinishedChの条件に一致します。この条件に一致した場合、呼び出し側へ取得したコンテナメトリクスの値を渡してforブロックを抜けます。

上記の実装では省略していますが、取得したコンテナメトリクスの値を呼び出し側に渡す際には、Channelsである引数のresultChへ値を送信しています。

このようにGatling Commanderでは開発言語としてGoを採用したことで、GoroutinesとChannelsを活用し簡単に並列処理を実装できました。

Gatling Commanderの導入効果

Gatling Commanderの導入により、Gatling Operatorのみで行っていた従来の負荷試験において、作業者が手作業で実施していたほとんどの作業を自動化できました。

次に示すのが自動化できた作業の一覧です。

  • 負荷試験シナリオを含むコンテナイメージのBuild&Push
  • Gatling ResourceのKubernetesマニフェストを作成
  • 負荷試験の開始(Gatling ResourceをKubernetesクラスタへデプロイ)
  • 負荷試験対象APIが稼働するコンテナのメトリクスを取得
  • 負荷試験の実行状況を確認
  • 負荷試験結果(Gatling Report)をGoogle Sheetsへ記録

前節(従来の実施方法)で説明した作業の一覧と比較すると、2.負荷試験シナリオを含むコンテナイメージのBuild&Push(アップロード)以降の全ての作業が自動化できています。

またこれらの作業の中には作業者が負荷試験シナリオごとに設定を変えて繰り返す作業もありました。Gatling Commanderでは設定ファイルに複数の負荷試験シナリオに対応する値を指定できます。事前に複数のシナリオの値を指定することで、従来の負荷試験実施の方法で作業者が繰り返していた設定変更の作業は不要になりました。

このようにGatling Commanderを利用することで、負荷試験全体で作業者が要する工数を大幅に削減できました。作業者は一度Gatling Commanderのコマンドを実行すれば、全てのシナリオで負荷試験の実行が完了し、終了通知が来るまで負荷試験の実施を放置できるようになりました。

本節では、中でも特にGatling Commanderによる自動化の恩恵が大きかった部分について説明します。

負荷試験シナリオ切り替えの手間が不要になった

前節(設定値の変更の手間)の通り、従来の負荷試験実施ではシナリオごとの設定値の変更作業が作業者の大きな手間でした。

Gatling Commanderでは複数のシナリオごとの設定値を、1つの設定ファイルで記述できます。また負荷試験シナリオはservice単位でグループ化し、シナリオ間で共通する値の指定は共通化しています。serviceごとの負荷試験は並列で実行し、同一service内のシナリオごとの負荷試験は順次実行します。

事前に全ての負荷試験シナリオの設定値をGatling Commanderの設定ファイルに記述すると、Gatling Commanderは自動でシナリオごとの設定値を切り替えます。そのため従来発生していた作業者による設定値の変更作業は不要です。

負荷試験シナリオの切り替えの自動化により、作業者の工数を大きく削減できました。

負荷試験実行の状況確認の手間が不要になった

前節(実行状況の確認の手間)の通り、従来の負荷試験実施ではシナリオごとに作業者が実行状況を確認して、次のシナリオを実行していました。

Gatling Commanderは、作業者が確認していた次の点を自動で確認します。

  • 前に実行した負荷試験が実行中か
  • 実行した負荷試験の結果が閾値(目標レイテンシ、リクエストのFail数が0)を超えていないか

前節(Goroutinesによる並列処理の実装)の通り、Gatling Commanderは実行中の負荷試験の終了を待機します。また前節(過負荷時に負荷試験を自動停止)の通り、事前に停止条件を指定することで過負荷時に負荷試験の順次実行を自動停止できます。Gatling Commanderが提供するこれらの機能により、従来の負荷試験で作業者が行っていた上記の点の確認作業は不要です。

例えばAPIの最大スループットを測る場合、従来の方法では作業者はAPIのレイテンシを見ながらその度にシナリオを変更して、負荷試験結果が閾値を超えないか確認していました。すべてのシナリオが完了するまでには最小でも負荷試験の試行回数 x 負荷試験の実施時間の時間が必要でした。試行回数が10回、実施時間が3分の場合、全ての負荷試験を合わせて最小でも30分かかります。実行状況の確認はこの間に頻繁に行う必要のある割り込み作業であり、作業者は全体で30分かかる待ち時間を効率的に使えていませんでした。

Gatling Commanderを利用すれば、従来の方法で作業者が行っていた実行状況の確認は不要になります。割り込み作業はなくなるため、作業者はすべての負荷試験シナリオが完了するまでの時間を最大限に活用し、他のタスクを進めることができます。

負荷試験の実行状況の確認作業の自動化により、作業者は複数のシナリオを持つ負荷試験の実行待ち時間を効率的に利用できるようになりました。

負荷試験結果の記録の手間が不要になった

前節(結果の記録の手間)の通り、従来の負荷試験実施ではシナリオごとに作業者が結果を記録していました。

Gatling Commanderは負荷試験の実行後に、負荷試験結果とコンテナメトリクスをGoogle Sheetsに自動記録します。この機能により、従来の負荷試験実施で作業者がシナリオごとに行なっていた記録作業は不要になります。

またGatling Commanderの負荷試験コマンド実行の終了後、作業者はすぐにシナリオごとの結果をGoogle Sheetsで一覧できるようになりました。

結果の記録作業は負荷試験実施の作業でも大きなトイルであったため、この点の自動化は作業者の工数削減に大きく貢献しました。

今後の展望

本節では今後の展望として、Gatling Commanderを応用したAPIパフォーマンスの劣化検知と、Gatling Commanderの機能追加・改善について説明します。

APIパフォーマンス劣化検知への利用

MLOpsブロックではGatling Commanderを利用したAPIパフォーマンスの劣化検知の仕組み作成を検討しています。

MLOpsブロックでは複数のAPIを本番運用していますが、短いスパンでの負荷試験実施はできていません。

新規開発や大きな機能変更の際には必ず負荷試験を実施し、日々の運用ではレスポンスタイムの監視も行なっています。しかしこれらの負荷試験の実施頻度は中・長期であり、依存ライブラリの更新など比較的小さな変更によるAPIパフォーマンスの変化は確認できていません。

劣化検知の仕組みを作成することで、APIのパフォーマンス悪化を早期に発見し、より安定して運用できるようにします。

劣化検知のためには負荷試験の定期的な実施が必要ですが、MLOpsブロックで運用するAPIは10以上あるため全てのAPIで負荷試験を実施する工数は大きいです。Gatling Commanderを利用することで、この工数を削減できるため、APIパフォーマンスの劣化検知の仕組みを作成できると考えています。

本節ではGatling Commanderを利用したAPIパフォーマンスの劣化検知の仕組みの構成案を簡単に説明します。

GitHub Actionsとの連携による負荷試験の定期実行

APIパフォーマンスの劣化検知の仕組みの構成案として、Gatling Commanderを利用した負荷試験をGitHub Actionsでスケジュール実行することを考えています。

MLOpsブロックではCIツールとしてGitHub Actionsを利用しています。GitHub ActionsはGitHubが提供するCI/CDツールです。GitHub Actionsではscheduleイベントを利用することでスケジュールした時刻にGitHub Actionsのワークフローをトリガーできます。

Gatling CommanderはCLIツールであり、Goの実行環境があればインストール可能です。事前にGatlingのSimulation、Gatling Commanderの設定ファイルをリポジトリ内に用意します。GitHub Actionsのワークフローで、Gatling Commanderのコマンド実行時にこれらのファイルを指定して、負荷試験を実行できます。

このようにGatling CommanderのコマンドをCIでスケジュール実行することで、比較的簡単に定期的な負荷試験の実行が可能です。この構成案によりAPIパフォーマンスの劣化検知の仕組みを作成できると考えています。

機能追加・改善

本節では、Gatling Commanderで検討している今後の機能追加・改善について説明します。

Kubernetes以外で稼働するAPIへの対応

MLOpsブロックで運用するAPIはすべてKubernetes上で稼働しており、Gatling Commanderは負荷試験対象のAPIがKubernetes上で稼働する前提に実装されています。そのため本記事の公開時点でGatling Commanderは、Kubernetes以外の環境で動作するAPIの負荷試験に対応していません。

負荷試験ツールとしてより利用しやすくするために、任意のインフラで稼働するAPIを負荷試験対象にできるよう対応を検討中です。

Google Cloud以外のプロバイダへの対応

MLOpsブロックではGoogle Cloudのサービスを利用してインフラを構築しています。Gatling Commanderでは現状、Google Cloudが提供するCloud StorageArtifact RegistryContainer Registry以外のクラウドストレージ、レジストリサービスの利用に対応していません。そのため公開時点ではAmazon Web Services(AWS)のS3Elastic Container RegistryなどGoogle Cloud以外のプロバイダで提供されるクラウドストレージ、レジストリサービスを利用できません。

今後の機能改善としてAWSなど他のプロバイダが提供するクラウドストレージ、レジストリサービスへの対応を考えています。

取得するメトリクスの拡充

現状Gatling Commanderが取得可能なメトリクスはコンテナのCPU・メモリ使用率のみです。自動で取得可能なメトリクスが増えることで、APIのパフォーマンス計測において作業者はより充実した情報を、工数をかけずに得ることができます。

既存のコンテナメトリクスに加えて、Google CloudのCloud MonitoringのようなリソースモニタリングやDatadogのようなアプリケーションのパフォーマンス監視の値を取得可能にする機能追加を検討中です。

まとめ

最後までお読みいただきありがとうございました。

本記事ではMLOpsブロックで抱えていた負荷試験実施の課題と、解決のために開発、公開したOSSのCLIツール、Gatling Commanderについてご紹介しました。

Gatling Commanderを利用して負荷試験を実施することで、従来の負荷試験で作業者が実施していた手作業を大幅に削減できました。本記事での事例の紹介が皆様のお役にたてば幸いです。またGatling CommanderではIssueやPull Requestを歓迎しています。ご興味のある方はぜひご利用ください。

最後になりますが、ZOZOでは一緒にサービスを作り上げてくれる方を募集中です。MLOpsブロックでも絶賛採用しているので、ご興味のある方は以下のリンクからぜひご応募ください。

hrmos.co corp.zozo.com

カテゴリー