k6で日本語文字コード対応

最近、WebサイトやWeb APIの負荷試験でk6っていうツール使っているのだか、このツールの拡張として日本語文字コードでエンコードされたデータを読み取れるようにした話。

何故そんなことがしたかったのかと言うと、とあるWebサイトの負荷試験をしようとしてk6のスクリプトを組んでいて、HTTPレスポンスの中身(Body)に特定の文字列が含まれているかどうかを検証しようとしたときに、文字化けしてうまく検証ができなかったため。

k6とは

Go言語で開発された負荷試験ツール。テストスクリプトは Javascriptで記述する。

負荷試験ツールと言えば、Apache JMeterが定番なのだが(自分も昔使ってた)、k6はJMeterよりもメモリ効率が優れており、使われる事例も増えてきている(らしい)。
k6とJMeterの比較は、こちら

JMeterについて、個人的な実感(と言っても、最後に使ったのは7年ぐらい前だが)としては、GUIで設定できるのは良い点なのだが、使用メモリ量が多くて実行中によく落ちてた記憶がある。あと、設定値の意味が分かりづらかった印象。

k6を使ってみた実感としては、テストスクリプトをJavascriptで記載できるので複雑なシナリオを組みやすい。前のAPIのレスポンスの値を、次のAPIのリクエストにセットするといった順序性のあるシナリオを作るときには特に思う。
あと、k6の実行ファイルさえあればどこでも実行できるので、ツールの動作環境(JMeterでいえば 特定バージョン以上のJVMとか)を気軽にインストールできない環境(インターネットに繋がらないスタンドアロン環境等)でも、実行ファイルとスクリプトを持っていけば実行できるポータビリティ性も良い。あとはメモリもJMeterに比べて全然消費しない点も良い。

問題点

そんな感じで負荷試験ではk6推しだったのだが、問題点があった。k6ではHTTPレスポンスについて、バイナリデータ(ArrayBufferオブジェクト)か、それをUTF-8でデコードした文字列でしか受け取れないのである( Params.responseType )。

現在、世界のWebサイトの9割はUTF-8みたいで、W3CもHTML5のエンコーディングはUTF-8を推奨してはいるものの、日本語のサイトには Shift-JIS や EUC-JP のものがまだ多く存在する。
このようなサイトではレスポンスの検証をするようなテストスクリプトを組むのが難しくなる。

k6の拡張

テストスクリプト(Javascript)側で、何らかのjsライブラリを利用して対応しようとも考えたが、k6は結構簡単に拡張機能を追加できることを知り、この方法でやってみることにした(k6 Extensions)。

既に色々な拡張があり、簡単そうな拡張機能(xk6-file など)のコードを参考に、見よう見まねで作ってみた。以下がそのコード。

package decodejp

import (
	"bytes"
	"log"
	"strings"

	"io/ioutil"

	"github.com/dop251/goja"
	"go.k6.io/k6/js/modules"
	"golang.org/x/text/encoding"
	"golang.org/x/text/encoding/japanese"
	"golang.org/x/text/transform"
)

func init() {
	modules.Register("k6/x/decodejp", new(DecodeJp))
}

type DecodeJp struct{}

func (*DecodeJp) Decode(buf goja.ArrayBuffer, encode string) string {
	_encode := strings.ToLower(encode)
	if _encode == "utf-8" || _encode == "utf8" {
		return string(buf.Bytes())
	} else {
		var enc encoding.Encoding
		switch _encode {
		case "shiftjis":
			enc = japanese.ShiftJIS
		case "shift-jis":
			enc = japanese.ShiftJIS
		case "shift_jis":
			enc = japanese.ShiftJIS
		case "sjis":
			enc = japanese.ShiftJIS
		case "eucjp":
			enc = japanese.EUCJP
		case "euc-jp":
			enc = japanese.EUCJP
		case "iso2022jp":
			enc = japanese.ISO2022JP
		case "iso-2022-jp":
			enc = japanese.ISO2022JP
		case "jis":
			enc = japanese.ISO2022JP
		default:
			log.Panicf("Unknown encode %s", encode)
		}

		r := bytes.NewBuffer(buf.Bytes())
		resultBuf, err := ioutil.ReadAll(transform.NewReader(r, enc.NewDecoder()))
		if err != nil {
			log.Panic(err.Error())
		}
		return string(resultBuf)
	}
}
  • 18行目:ここで拡張した機能を追加し、Javascript側で呼び出せるように登録している(らしい)。Javascriptでは、ここで設定しているパス「k6/x/decodejp」でモジュールを呼び出す。
  • 23行目:Javascriptから今回作成した関数(Decode)を呼ぶときに引数として設定する ArrayBufferオブジェクト(HTTPレスポンスをバイナリで受け取った場合のデータ)の型は、Go側では goja.ArrayBuffer となる。

ビルド

ビルドには、Go(当然だが)、および、k6拡張ビルドツールである xk6 がインストールされている環境が必要である。

(既にGoはインストールされていることを前提にして)まずはxk6をインストール。

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

xk6コマンドでビルド。

# githubなどのリポジトリにある拡張機能を含めてビルド
xk6 build --with github.com/yksnyh/xk6-decode-jp@latest

# 開発中でローカルにソースがある場合は以下
xk6 build --with github.com/yksnyh/xk6-decode-jp@latest=.

ビルドしたら k6の実行ファイルができるはず。

利用方法

以下はサンプルのテストスクリプト。

import { check } from 'k6';
import http from "k6/http";
import decodejp from 'k6/x/decodejp';

export default function () {
    const url = 'http://abehiroshi.la.coocan.jp/';
    const res = http.get(url, { responseType: 'binary' });
    const resText = decodejp.decode(res.body, 'shift-jis');

    check(resText, {
        'title ok': r => {
            const m = (/<title>(.+)<\/title>/g).exec(r);
            if (!m) return false;
            return m[1] === '阿部寛のホームページ';
        }
    });
}
  • 3行目:Go側で登録したパス「k6/x/decodejp」からモジュールをimport。
  • 7行目:responseType は binary と指定すると、レスポンスボディは文字列に変換される前のArrayBufferオブジェクトで受け取れる。
  • 8行目:今回作成した関数を用いて、エンコードを指定して文字列に変換する。

おわりに

作成した拡張機能はGithubにも置いてる。
xk6-decode-jp

結構簡単に k6の拡張ってできるんだなって感じたのと、システムの特性に応じてゴリゴリにカスタマイズもできそうだなって思った。
あと、レスポンスのデコードも出来たので、逆にリクエストにおいて、日本語文字コードでエンコードしてリクエストするような画面の場合も同じようにしてできそうである。

以上です