tag:crieit.net,2005:https://crieit.net/tags/%E8%B2%A0%E8%8D%B7%E8%A9%A6%E9%A8%93/feed
「負荷試験」の記事 - Crieit
Crieitでタグ「負荷試験」に投稿された最近の記事
2022-04-26T23:06:22+09:00
https://crieit.net/tags/%E8%B2%A0%E8%8D%B7%E8%A9%A6%E9%A8%93/feed
tag:crieit.net,2005:PublicArticle/18176
2022-04-26T23:06:22+09:00
2022-04-26T23:06:22+09:00
https://crieit.net/posts/K6-TypeScript-Docker
負荷テストツールK6をTypeScript+Dockerで動かすためのテンプレートを作る
<h2 id="k6とは?"><a href="#k6%E3%81%A8%E3%81%AF%EF%BC%9F">k6とは?</a></h2>
<p><a target="_blank" rel="nofollow noopener" href="https://k6.io/">k6</a> は<a target="_blank" rel="nofollow noopener" href="https://go.dev/">Go製</a>のOSS負荷テストツールで普通のHTTPの他、HTTP/2, gRPC, WebSocketなどにも対応した高機能なテストシナリオ作成がJavaScriptで実施できる手軽さと<a target="_blank" rel="nofollow noopener" href="https://grafana.com/">Grafana</a>を使ったパフォーマンスレポートが魅力の製品です。</p>
<p>以前はloadimpactと呼ばれていました。こちらのほうが馴染みある人が多いのではないでしょうか?また、K6のクラウド版を使ったことありますー!という方もいらっしゃるのではないでしょうか?</p>
<p>私個人としては、この手の負荷試験ツールは長らく<a target="_blank" rel="nofollow noopener" href="https://locust.io/">Locust</a>を愛用してきてそれなりに複雑なテストシナリオを作ってきた経験もあるのですが、時代の流れ的にこっちのほうがいいのではないかと思い浮気することにしました。</p>
<h2 id="使い心地"><a href="#%E4%BD%BF%E3%81%84%E5%BF%83%E5%9C%B0">使い心地</a></h2>
<p>K6をMacやEC2などで作ったLinux上に構築する方法はググればたくさん出てくるのでここでは割愛しますが、まず<strong>非常に簡単に環境が構築できる。</strong> この点はしっかり強調しておきたいです。</p>
<p>MacならHomebrew入っていれば、</p>
<pre><code>brew install k6
</code></pre>
<p>で完了ですからね。環境構築は超簡単です。</p>
<p>シナリオファイルの作成もJavaScript ES6対応&<a target="_blank" rel="nofollow noopener" href="https://k6.io/docs/">公式ドキュメント</a>比較的充実しているので、ある程度JavaScript触ったことのある人であればノーストレスで実装できそうです。</p>
<p>また、細かいニーズのシナリオ作成については<a target="_blank" rel="nofollow noopener" href="https://github.com/grafana/k6/tree/master/samples">GitHubレポジトリのサンプルコード</a>がかなり参考になります。</p>
<p>なので、正直普通に使う分にはこの記事で紹介することなんてあんまりないのです。ここまで読んでくれた皆さん、ごめんなさい。</p>
<h2 id="ロマン"><a href="#%E3%83%AD%E3%83%9E%E3%83%B3">ロマン</a></h2>
<h3 id="完全Dockerコンテナ上で実行"><a href="#%E5%AE%8C%E5%85%A8Docker%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A%E4%B8%8A%E3%81%A7%E5%AE%9F%E8%A1%8C">完全Dockerコンテナ上で実行</a></h3>
<p>とはいえ、これで終わりならわざわざブログを書くこともないので早速ちょっとした改良をしていきます。</p>
<p>まず、完全Dockerコンテナ上での実行です。</p>
<p>開発チームの皆さんがMacを使っている会社さんなら不要な気もしますが中にはWindowsやLinuxを使っている人が混在している開発現場もありそうで、そういった現場で「はい。K6入れておいてね」はちょっと不親切です。かといって、全OSのマニュアルを用意するのもつらいです。</p>
<p>また、負荷テストはネットワークの安定したサーバー上で実行することもしばしばでMacで動くのに負荷テスト用サーバーで動きません!みたいになるのも悲しいです。そもそも専用のサーバーを立てるのもめんどくさいのでDocker Image化してDockerコンテナ管理サービスでサクッと実行させるのが賢そうです。なんならCIに組み込んで、パイプライン上で実行させるとかも面白そうです。</p>
<p>もちろんK6公式もぬかりなく<a target="_blank" rel="nofollow noopener" href="https://github.com/grafana/k6#docker">Docker Image</a>を用意してます。ただ、あくまでもこちらのImageはk6単体のImageなのでパフォーマンス可視化をするには別途<a target="_blank" rel="nofollow noopener" href="https://www.influxdata.com/">InfluxDB</a>と<a target="_blank" rel="nofollow noopener" href="https://grafana.com/">Grafana</a>を立ち上げる必要があります。</p>
<p>なので、まとめて環境をDocker composeしてしまいましょう。</p>
<p><del>もちろん、本来は結果をきちんと残しておくためにInfluxDBとGrafanaを永続化する必要がありますが、今回はテスト実行なのでInfluxDB永続化は行いません。</del> 永続化することにしました。代わりにInfluxDBのcleanコマンドを実装する形にしました。本格的に負荷テスト環境を作るならAWS ECSであればデータボリュームを<a target="_blank" rel="nofollow noopener" href="https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/efs-volumes.html">Amazon EFS ボリューム</a>とかで管理すると幸せになれる気がします。</p>
<p>次のような<strong>docker-compose.yml</strong>を作るだけで完成です。楽でいいですねdocker compose。</p>
<pre><code class="yml">version: '3.8'
services:
influxdb:
image: influxdb:1.8
ports:
- "8086:8086"
environment:
- INFLUXDB_DB=k6
networks:
- k6net
- grafanaNet
volumes:
- ./influxdb:/var/lib/influxdb
k6:
image: grafana/k6:latest
networks:
- k6net
ports:
- "6565:6565"
environment:
- K6_OUT=influxdb=http://influxdb:8086/k6
volumes:
- ./tests/scripts:/scripts
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
environment:
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
- GF_AUTH_ANONYMOUS_ENABLED=true
- GF_AUTH_BASIC_ENABLED=false
volumes:
- ./grafana:/etc/grafana/provisioning/
networks:
- grafanaNet
networks:
k6net:
driver: bridge
grafanaNet:
driver: bridge
</code></pre>
<p>やっていることは超基本的なこととしてnetworkを切ってサービス名でInfluxDBにアクセスできるようにする点くらいなのですが、k6のenvironmentで、</p>
<pre><code> environment:
- K6_OUT=influxdb=http://influxdb:8086/k6
</code></pre>
<p>とやってあげることで実行結果をInfluxDBに出力します。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://k6.io/docs/results-visualization/influxdb-+-grafana/#run-the-test-and-upload-the-results-to-influxdb">公式ドキュメント</a>のように環境変数でなく実行時の引数で設定できます。</p>
<p>これで、<a target="_blank" rel="nofollow noopener" href="https://localhost:3000">https://localhost:3000</a>にアクセスすることで<strong>実行結果を可視化</strong>できるようになりました。</p>
<p><img src="https://i.imgur.com/sX5HtCG.png" alt="grafana" /></p>
<h3 id="TypeScriptでテストシナリオを書けるようにする"><a href="#TypeScript%E3%81%A7%E3%83%86%E3%82%B9%E3%83%88%E3%82%B7%E3%83%8A%E3%83%AA%E3%82%AA%E3%82%92%E6%9B%B8%E3%81%91%E3%82%8B%E3%82%88%E3%81%86%E3%81%AB%E3%81%99%E3%82%8B">TypeScriptでテストシナリオを書けるようにする</a></h3>
<p>加えて、シナリオファイルの<strong>TypeScript化</strong>を実施します。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://k6.io/docs/using-k6/javascript-compatibility-mode/">公式ドキュメント</a>によるとGoのJavaScript VMが<strong>ES5にしか対応してない</strong>のでES6で書いたテストシナリオはどうやらK6のなかでES5に変換してから実行しているらしいです。</p>
<p>K6の<a target="_blank" rel="nofollow noopener" href="https://github.com/grafana/k6/blob/master/go.mod">go.mod</a>を見てみると使っているJavaScript VMは<a target="_blank" rel="nofollow noopener" href="https://github.com/dop251/goja">goja</a>みたいですね。</p>
<p>逆に、<strong>compatibility-mode=base</strong>というオプションを使ってES5のテストファイルを渡してあげた場合、ES6への変換の手間がなくなるので実行時間とメモリの使用量が改善され、<strong>パフォーマンスが改善</strong>されるとのことです。</p>
<p>じゃあせっかくなら<strong>TypeScript=>ES5のトランスパイル</strong>を実施して実行時はcompatibility-mode=baseオプションを指定すれば、テストシナリオをTypeScriptで実装しつつパフォーマンス面でも有利に働きそうです。</p>
<p>とりあえず<a target="_blank" rel="nofollow noopener" href="https://github.com/grafana/k6-hardware-benchmark">公式に載っている例</a>を参考にwebpack.config.jsをゴリっと書くことにしました。</p>
<p>babelってトランスパイルしてしまったのでこれ型チェックとかしないかもな...と思いつつ公式がそうやっているからそのまま真似していくつかplugin追加しただけです。</p>
<p>あと、target webで出力してますがsourcemapを出してしまうとK6で読み込めないよエラーが出るので出力させないです。(ちょっとハマった)</p>
<p>さらに、今回はおそらく使わないと思いましたが<a target="_blank" rel="nofollow noopener" href="https://webpack.js.org/plugins/copy-webpack-plugin/">copy-webpack-plugin</a>を使ってテスト用のassetをdistディレクトリにコピーする形にしてます。こうしておくことで、ファイルのアップロードみたいなテストケースも書けるようになりそうです。多分。</p>
<p>と思って色々試行錯誤していたら公式に実装例があり、ほぼやりたいことがそれでできていたので結局それをパクることにしました..。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://github.com/grafana/k6-template-typescript">Template to use TypeScript with k6</a></p>
<pre><code class="javascript">const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const CopyPlugin = require('copy-webpack-plugin');
const GlobEntries = require('webpack-glob-entries');
module.exports = {
mode: 'production',
entry: GlobEntries('./tests/scripts/*.ts'),
output: {
path: path.join(__dirname, 'dist'),
libraryTarget: 'commonjs',
filename: '[name].js',
},
resolve: {
extensions: ['.ts', '.js'],
},
module: {
rules: [
{
test: /\.ts$/,
use: 'babel-loader',
exclude: /node_modules/,
},
],
},
target: 'web',
externals: /^(k6|https?\:\/\/)(\/.*)?/,
stats: {
colors: true,
},
plugins: [
new CleanWebpackPlugin(),
new CopyPlugin({
patterns: [{
from: path.resolve(__dirname, 'tests/assets'),
noErrorOnMissing: true
}],
}),
],
optimization: {
minimize: false,
},
};
</code></pre>
<p>tsconfig.jsonも特に特徴はないですが、targetはes5にしてます。</p>
<pre><code>{
"compilerOptions": {
"target": "es5",
"moduleResolution": "node",
"module": "commonjs",
.....
}
}
</code></pre>
<p>これで<strong>webpackコマンド</strong>を実行することできちんとES5に変換されたテストシナリオのJavaScriptファイルがdist配下に出力されるようになりました。</p>
<h3 id="実行してみる"><a href="#%E5%AE%9F%E8%A1%8C%E3%81%97%E3%81%A6%E3%81%BF%E3%82%8B">実行してみる</a></h3>
<p>Docker composeにしてますので、必要なコンテナをupで立ち上げたのち、k6だけ個別にrunします。runするときにdist配下のテストファイルを標準入力として指定すればOKです。</p>
<pre><code>ddocker compose up -f
docker compose run k6 run --compatibility-mode=base - < ./dist/httpGet.js
</code></pre>
<p>無事実行できました!</p>
<pre><code>asset cookieAuthServerSession.js 5.3 KiB [emitted] (name: cookieAuthServerSession)
asset httpGet.js 2.63 KiB [emitted] (name: httpGet)
runtime modules 1.83 KiB 8 modules
orphan modules 451 bytes [orphan] 4 modules
built modules 2.52 KiB [built]
./tests/scripts/cookieAuthServerSession.ts + 4 modules 2.38 KiB [not cacheable] [built] [code generated]
./tests/scripts/httpGet.ts + 1 modules 141 bytes [not cacheable] [built] [code generated]
webpack 5.35.1 compiled successfully in 619 ms
failed to get console mode for stdin: The handle is invalid.
failed to get console mode for stdin: The handle is invalid.
failed to get console mode for stdin: The handle is invalid.
/\ |‾‾| /‾‾/ /‾‾/
/\ / \ | |/ / / /
/ \/ \ | ( / ‾‾\
/ \ | |\ \ | (‾) |
/ __________ \ |__| \__\ \_____/ .io
execution: local
script: -
output: InfluxDBv1 (http://influxdb:8086)
scenarios: (100.00%) 1 scenario, 1 max VUs, 10m30s max duration (incl. graceful stop):
* default: 1 iterations for each of 1 VUs (maxDuration: 10m0s, gracefulStop: 30s)
running (00m00.9s), 0/1 VUs, 1 complete and 0 interrupted iterations
default ✓ [ 100% ] 1 VUs 00m00.9s/10m0s 1/1 iters, 1 per VU
data_received..................: 21 kB 24 kB/s
data_sent......................: 519 B 574 B/s
http_req_blocked...............: avg=734.31ms min=734.31ms med=734.31ms max=734.31ms p(90)=734.31ms p(95)=734.31ms
http_req_connecting............: avg=161.49ms min=161.49ms med=161.49ms max=161.49ms p(90)=161.49ms p(95)=161.49ms
http_req_duration..............: avg=169.36ms min=169.36ms med=169.36ms max=169.36ms p(90)=169.36ms p(95)=169.36ms
{ expected_response:true }...: avg=169.36ms min=169.36ms med=169.36ms max=169.36ms p(90)=169.36ms p(95)=169.36ms
http_req_failed................: 0.00% ✓ 0 ✗ 1
http_req_receiving.............: avg=2.71ms min=2.71ms med=2.71ms max=2.71ms p(90)=2.71ms p(95)=2.71ms
http_req_sending...............: avg=49µs min=49µs med=49µs max=49µs p(90)=49µs p(95)=49µs
http_req_tls_handshaking.......: avg=381.41ms min=381.41ms med=381.41ms max=381.41ms p(90)=381.41ms p(95)=381.41ms
http_req_waiting...............: avg=166.6ms min=166.6ms med=166.6ms max=166.6ms p(90)=166.6ms p(95)=166.6ms
http_reqs......................: 1 1.105208/s
iteration_duration.............: avg=903.84ms min=903.84ms med=903.84ms max=903.84ms p(90)=903.84ms p(95)=903.84ms
iterations.....................: 1 1.105208/s
Done in 6.27s.
</code></pre>
<p>Locustに勝るとも劣らないかなりしっかりとしたレポートが出ますね!</p>
<h2 id="yarn scriptに何とかして取り込む"><a href="#yarn+script%E3%81%AB%E4%BD%95%E3%81%A8%E3%81%8B%E3%81%97%E3%81%A6%E5%8F%96%E3%82%8A%E8%BE%BC%E3%82%80">yarn scriptに何とかして取り込む</a></h2>
<p>まぁこれで完成なのですが、ちょっとまだ不満点があります。 docker compose runコマンドを打つ前にwebpackコマンドを実行しないといけない点です。</p>
<p>どうせならテスト実行一発でトランスパイル=>実行と移ってほしいのでyarn scriptを書いていきます。</p>
<p>今回実装するまで知らなかったのですが、yarn scriptに引数を渡すときコマンドの途中に引数の文字列を入れたいとき、ちょっと難しかったです。サクッとできるかなと思ったのですが。</p>
<p>結局、次のようにshellから0番目の引数を取って実行するという無茶苦茶実装にしました。こうすれば、 <strong>yarn start testFile</strong> で実行できます。</p>
<pre><code>start": "sh -c \"webpack && docker compose run k6 run --compatibility-mode=base - < ./dist/${0}.js\"",
</code></pre>
<h2 id="GitHub Actionsに組み込んでみる"><a href="#GitHub+Actions%E3%81%AB%E7%B5%84%E3%81%BF%E8%BE%BC%E3%82%93%E3%81%A7%E3%81%BF%E3%82%8B">GitHub Actionsに組み込んでみる</a></h2>
<p>最後にGitHub Actionsに組み込んでみることにします。</p>
<p>とはいってもGitHub Actionsは普通にdocker composeできるのであまり気にするところはなさそうです。</p>
<p>強いて言えば、docker compose runする際に-Tオプションをつけないと、コンソールが戻ってこないので無限に動いてしまいます。ちょっとだけ詰まりました。</p>
<pre><code class="yaml">name: Test Scenario
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js 14.x
uses: actions/setup-node@v1
with:
node-version: 14.x
- name: setup config
run: mv tests/config/configExample.ts tests/config/config.ts
- name: Setup test Env
run: |
yarn install
docker-compose up -d
yarn build
- name: run
# In GitHub Actions, if pseudo-tty is assigned in case of docker compose run,
# container execution will continue and step will not be completed, so disable it with -T option.
run: docker compose run -T k6 run --compatibility-mode=base - < ./dist/httpGet.js
</code></pre>
<h2 id="完成"><a href="#%E5%AE%8C%E6%88%90">完成</a></h2>
<p>というわけでこちらが完成したテンプレートです。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://github.com/tubone24/k6_template_with_typescript_on_docker">K6 with TypeScript on Docker</a></p>
<p>これからK6をプロジェクトで導入したいなと思っている人は上記をテンプレートとしてご活用いただければ幸いです。</p>
<h2 id="参考"><a href="#%E5%8F%82%E8%80%83">参考</a></h2>
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://k6.io/docs/">k6 documentation</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://github.com/grafana/k6-hardware-benchmark">k6-hardware-benchmark</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://github.com/grafana/k6-template-typescript">Template to use TypeScript with k6</a></li>
</ul>
tubone24