🙆

k6の拡張機能開発「xk6」入門

2024/09/03に公開

はじめに

k6で複雑なシナリオを書こうとすると、JavaScriptの組み込み関数(例:Math)だけでは実装が難しいことがあります。。

本来であればNode.jsやブラウザ上で動作するため、便利なライブラリが提供されていますが、これらをk6上で動作させることはできません。

自前で一から実装するのも簡単なのであれば良いのですが、非常に難しい場合もあります。そんなときに使うのがxk6です。このコマンドラインツールとGoパッケージを利用することで、k6を簡単にカスタムビルドし、特定の拡張機能を組み込んだバイナリを作成することができます。

公式ドキュメント
Extensions | Grafana k6 documentation

GitHub
https://github.com/grafana/xk6

xk6を使うメリット

xk6を利用する最大のメリットは、k6の機能を拡張できることです。

既存の拡張機能を追加したり、新たに開発した拡張機能を組み込むことで、テストツールをより柔軟にカスタマイズできます。

具体的には以下のようなケース

  1. プロトコルサポートの追加: 例えば、Kafkaメッセージングシステムをテストする際、xk6-kafka拡張機能を組み込むことで、Kafkaプロトコルを利用した負荷テストが可能になります。
  2. カスタムデータの取り扱い: 標準のk6では対応できないデータソースや出力先がある場合、そのための拡張機能を追加することで、特定の要件に対応したテストを実現できます。

既存の拡張機能は以下にあります↓
Explore k6 extensions | Grafana k6 documentation

xk6の使い方

xk6を活用する方法は多岐にわたりますが、ここでは主にDockerを利用したセットアップとローカル環境でのビルド方法について説明します。

Dockerを使った簡単セットアップ

例:カスタムk6バイナリのビルド

例えば、Linuxシステム上でxk6-kafkaxk6-output-influxdb拡張機能を組み込んだk6 v0.43.1バイナリを作成する場合、以下のコマンドを使用します:

docker run --rm -it -u "$(id -u):$(id -g)" -v "${PWD}:/xk6" grafana/xk6 build v0.43.1 \\
  --with github.com/mostafa/xk6-kafka@v0.17.0 \\
  --with github.com/grafana/xk6-output-influxdb@v0.3.0

現在のディレクトリにカスタムk6バイナリが作成されます。

他のOS向けバイナリのビルド

xk6はクロスコンパイルもサポートしており、異なるOS向けのバイナリを簡単にビルドできます。例えば、macOS用のバイナリをビルドするには、以下のようにします:

docker run --rm -it -e GOOS=darwin -u "$(id -u):$(id -g)" -v "${PWD}:/xk6" \\
  grafana/xk6 build v0.43.1 \\
  --with github.com/mostafa/xk6-kafka@v0.17.0 \\
  --with github.com/grafana/xk6-output-influxdb@v0.3.0

ローカルでのカスタムk6バイナリのビルド

Dockerを使わずにローカルでカスタムk6バイナリをビルドしたい場合、以下のコマンドでxk6をGoを使用してインストールできます

go install go.k6.io/xk6/cmd/xk6@latest

このコマンドにより、xk6バイナリがGoパスにインストールされ、すぐに使用可能になります。

例:カスタムビルドのコンパイル

次のコマンドを実行して特定の拡張機能を追加したk6バイナリを作成できます

xk6 build --with github.com/grafana/xk6-browser

このコマンドでは、ブラウザ自動化機能を持つxk6-browser拡張機能が追加され、ブラウザ操作をテストできるk6バイナリが生成されます。

応用的な使い方

以下では、拡張機能の開発プロセスについて解説します。

k6拡張機能の開発

拡張機能のフォルダ内でxk6をビルドサブコマンドなしで実行すると、拡張機能を含むk6をその場でビルドして実行できます。

xk6 run -u 10 -d 10s test.js

環境変数を使ったカスタマイズ

xk6では、環境変数を使用してビルドや動作をさらにカスタマイズできます。例えば:

  • K6_VERSION: 使用するk6のバージョンを指定します。
  • XK6_RACE_DETECTOR=1: Goのレースディテクタを有効にして、並行処理の問題を検出します。
  • XK6_K6_REPO: カスタムk6リポジトリのパスを設定します。フォークやカスタムバージョンを使用する際に便利です。

依存関係の管理

拡張機能を開発したり利用したりする際には、k6コアとの依存関係を同期させることが重要です。これにより、JSランタイムのバイナリ互換性が保証され、ビルドエラーの発生を防ぐことができます。go-depsyncツールを使用すると、依存関係を自動的にチェックし、同期させるためのgo getコマンドを生成できます。

実践編 - 拡張機能の開発

実際にxk6を使って拡張機能を開発してみましょう。以下は、シンプルな「Hello World」拡張機能の例です。

ソースコードはこちら

https://github.com/moko-poi/xk6-hello-world

1. 開発環境のセットアップ

まず、拡張機能の開発を行うためにGo開発環境をセットアップします。以下のツールが必要です:

  • Goのインストール: xk6はGoで開発されているため、Go 1.17以上が必要です。
  • xk6のインストール: xk6を使ってk6に新しい機能を追加し、カスタムビルドを作成することができます。以下のコマンドでインストールします。
go install go.k6.io/xk6/cmd/xk6@latest

2. Goモジュールの初期化

次に、拡張機能のプロジェクトディレクトリを作成し、Goモジュールを初期化します。以下のコマンドを使用します:

mkdir xk6-hello-world
cd xk6-hello-world
go mod init github.com/moko-poi/xk6-hello-world

ここでのモジュール名は、あなたのGitHubリポジトリに対応するパスに置き換えてください。

3. 拡張機能の実装

次に、xk6拡張機能を実装します。以下は、カスタム比較機能を提供するシンプルな拡張機能の例です。

まず、hello.goというファイルを作成し、次のコードを追加します:

package hello

import (
	"go.k6.io/k6/js/modules"
)

func init() {
	modules.Register("k6/x/hello", new(Hello))
}

type Hello struct{}

func (h *Hello) HelloWorld() string {
	return "Hello World!"
}

4. xk6を使ってビルドとテスト

次に、この拡張機能を含むカスタムk6バイナリをビルドします。以下のコマンドを実行してください:

xk6 build --with github.com/moko-poi/xk6-hello-world=.

これにより、拡張機能が組み込まれたk6バイナリが作成されます。

次に、テストスクリプトtest.jsを作成し、以下のコードを追加します:

import hello from 'k6/x/hello';

export default function () {
  console.log(`${hello.helloWorld()}`);
}

スクリプトを実行:

./k6 run test.js

このスクリプトを実行すると、コンソールに「Hello World!」と表示されるはずです。これで、xk6を使った拡張機能の基本的な開発プロセスが理解できたかと思います。

補足:GoからJavaScriptへの橋渡し (Go-to-JS Bridge) の仕組み

k6xk6を利用していると、Goで実装した機能をJavaScriptから呼び出すことができます。これを可能にしているのがGo-to-JS Bridgeという仕組みです。この橋渡し機能は、GoのコードをJavaScriptのランタイムであるsobekに渡す際に発生する様々な変換を担っています。

1. メソッドとフィールドの変換

Goで定義されたメソッドやフィールドは、そのままではJavaScriptから直接使用することはできません。これらは、Go-to-JS BridgeによってJavaScript側で利用可能な形式に変換されます。

例: メソッドの変換

Goのメソッドは通常PascalCaseで書かれますが、JavaScriptではcamelCaseでアクセスされるように変換されます。以下の例を見てみましょう。

Goでの定義:

package compare

type Compare struct{}

// GoではPascalCase
func (c *Compare) IsGreater(a, b int) bool {
    return a > b
}

JavaScriptでの使用:

import compare from 'k6/x/compare';

// JavaScriptではcamelCaseに変換される
if (compare.isGreater(5, 3)) {
    console.log("5 is greater than 3");
}

この変換により、GoのメソッドはJavaScriptから直感的に呼び出すことができるようになります。

例: フィールドの変換

フィールド名も同様に、PascalCaseからsnake_caseへと変換されます。

Goでの定義:

type Compare struct {
    ComparisonResult string
}

JavaScriptでの使用:

console.log(compare.comparison_result);  // snake_caseに変換されている

2. jsタグを使ったフィールドの明示的な制御

Go-to-JS Bridgeでは、フィールド名を明示的に指定したり、JavaScriptから非表示にしたりすることが可能です。

例: jsタグの使用

type Compare struct {
    ComparisonResult string `js:"result"`  // "result"として公開
    HiddenField      string `js:"-"`       // 非公開
}

このように設定することで、JavaScript側ではresultという名前でフィールドにアクセスでき、HiddenFieldはJavaScriptからは見えなくなります。

3. 型変換とネイティブコンストラクタ

Go-to-JS Bridgeは、Goの型をJavaScriptの型に変換する機能も持っています。ただし、複雑な型の場合には変換がうまく行かない場合があります。例えば、Goのint64型はJavaScriptのnumber型に自動的に変換されますが、構造体や複雑なオブジェクトはsobek.Objectsobek.Valueに変換する必要があります。

例: ネイティブコンストラクタの利用

sobekを使って、Goの構造体をJavaScriptからnewキーワードを使って生成することができます。

Goでの定義:

func (*Compare) XComparator(call sobek.ConstructorCall, rt *sobek.Runtime) *sobek.Object {
    return rt.ToValue(&Compare{}).ToObject(rt)
}

JavaScriptでの使用:

const comparator = new compare.Comparator();  // Goで定義した構造体をnewでインスタンス化

この仕組みによって、JavaScriptのような直感的なオブジェクト指向プログラミングが可能になります。

参照:https://grafana.com/docs/k6/latest/extensions/explanations/go-js-bridge/

まとめ

k6でシナリオを書いていると、k6のビルドイン関数では実装が難しいと感じることがしばしばあります。複雑な計算や特殊なプロトコルへの対応が求められる場面では、既存の機能だけでは不十分なことも多いです。

そんなときにxk6を利用することで、Go言語の強力な標準ライブラリを活用し、簡単にカスタム拡張機能を作成できるのは非常に便利です。

Goであれば、ほとんどのケースに標準ライブラリで対応でき、さらに書き慣れた環境でサクッと実装できました。

Discussion