【k6】負荷テストで大量のメトリクス生成によるメモリ不足を解決する
はじめに
k6は、HTTPリクエストをシミュレートしてアプリケーションのパフォーマンスをテストする強力なツールですが、特に長時間にわたる負荷テストでは、メトリクスの過剰な生成が原因でメモリ不足に陥ることがあります。この記事では、特に動的なURLを含むテストシナリオで発生するメモリ消費問題を防ぐための方法を紹介します。
原因
長時間や大規模な負荷テスト中に以下のようなエラーメッセージが表示されることがあります。
WARN[0029] The test has generated metrics with 100358 unique time series, which is higher than the suggested limit of 100000 and could cause high memory usage. Consider not using high-cardinality values like unique IDs as metric tags or, if you need them in the URL, use the name metric tag or URL grouping. See https://k6.io/docs/using-k6/tags-and-groups for details.
このエラーは、k6が100,000を超えるユニークなメトリクスを生成したために、メモリ消費量が増加していることを示しています。
動的なURL(例:http://example.com/posts/1
や http://example.com/posts/2
)のように、リクエストごとに異なるURLを扱うシナリオでは、メトリクスが過剰に生成される傾向があります。
k6では、明示的にタグを指定しない場合、デフォルトでリクエストのURLがそのままタグとして使用されます。その結果、動的なURLを持つリクエストごとに個別のタイムシリーズが生成され、タイムシリーズの数が急増してしまいます。
このエラーが発生する主な理由は、高いカーディナリティを持つメトリクスが過剰に生成されることです。カーディナリティとは、メトリクス内でユニークな値の数がどれほど多いかを指し、負荷テスト中に発生する「ユニークなURL」や「ユニークなID」などがその一例です。
例えば、以下のようなシナリオがカーディナリティの増加を引き起こします:
- 動的なURLを持つリクエスト (
http://example.com/posts/1
,http://example.com/posts/2
など) - 各リクエストに異なるクエリパラメータやパスパラメータが含まれている場合
- タグやラベルに一意のID(ユーザーID、リクエストID、トランザクションIDなど)を付与している場合
これらのユニークな値は、k6によってそれぞれ別個のタイムシリーズ(時間とともにメトリクスが収集されるユニークなデータセット)として扱われます。特にタグ指定がなくデフォルトのURLタグが適用される場合、動的なURLによりタイムシリーズが膨大に生成される可能性があります。
その結果、メモリ消費の増加が影響を及ぼすことがあります。
私の場合、負荷テストを実行しているk6のPodが、KubernetesのOOMKilledによって強制的に停止されました。これは、k6が動的なURLを含む多数のリクエストを処理する際、デフォルトのタグ設定により膨大なメトリクスが生成され、メモリを圧迫した結果です。
解決方法について
動的なURLを持つ負荷テストシナリオでメモリ使用量を削減するために、タグやURLグループ化を活用してメトリクスを集約することが有効です。これにより、同じアクションに関連する複数のリクエストを1つのメトリクスとして扱い、ユニークなメトリクスの数を減らすことができます。
コード例
以下のコードでは、動的なURLを1つのタグ「PostsItemURL
」にまとめて、メモリ消費を抑えつつ、テスト結果を効果的に集約しています。
import http from 'k6/http';
export default function () {
for (let id = 1; id <= 100; id++) {
http.get(`http://example.com/posts/${id}`, {
tags: { name: 'PostsItemURL' },
});
}
}
この例では、すべてのリクエストに対して「PostsItemURL
」という共通のタグが付与され、URLの違いに関わらず、メトリクスが同じグループに集約されます。
また、別の方法として、http.url ラッパーを使って、タグの name に文字列テンプレートを使用する方法もあります。これにより、動的なURLパターンに対応しながら、タグ名を統一することができます。
import http from 'k6/http';
export default function () {
for (let id = 1; id <= 100; id++) {
http.get(http.url`http://example.com/posts/${id}`);
}
}
この方法を使うと、リクエストにおいて異なるURLを扱っていても、すべてのメトリクスが同じ name タグ(http://example.com/posts/${} のように統一された形式)として定義されます。これにより、動的なURLでも同様にメモリ使用量を抑えることができます。
公式ドキュメント
Topic:k6シナリオのトランザクション、グループ、タグについての解説
グループ (Groups)
グループを使用すると、複数のリクエストを1つのトランザクションとしてまとめることができます。これにより、特定のアクションに関連するリクエストの合計応答時間を計測することができ、テスト結果の可読性が向上します。
例:
import { group } from 'k6';
export default function () {
group('01_VisitHomepage', function () {
// ホームページにアクセスするコード
});
group('02_ClickOnProduct', function () {
// 商品ページに移動するコード
});
}
このコードでは、01_VisitHomepage
や 02_ClickOnProduct
というグループ名でリクエストを分類しています。各グループの合計応答時間がメトリクスとして記録され、詳細なパフォーマンス分析が可能になります。
タグ (Tags)
タグは、リクエストやカスタムメトリクスにラベルを付けるための方法です。タグを使うことで、特定の条件に基づいてメトリクスをフィルタリングしたり、デバッグを容易にしたりすることができます。
例:
import { group } from 'k6';
import http from 'k6/http';
export default function () {
group('01_Homepage', function () {
http.get('<http://ecommerce.k6.io/>', {
tags: {
page: 'Homepage',
type: 'HTML',
}
});
});
}
このコードでは、リクエストに page
と type
という2つのタグが追加されており、これにより後から特定のタグに基づいたデータ分析が容易になります。
URLのグループ化 (URL Grouping)
k6では、動的に生成されるURLを1つのメトリクスにまとめるために、URLをグループ化する機能があります。これにより、商品ページなど同じカテゴリのリクエストを1つのメトリクスとして扱うことができます。
例:
import http from 'k6/http';
export default function () {
let product = ['album', 'beanie', 'beanie-with-logo'];
let rand = Math.floor(Math.random() * product.length);
let productSelected = product[rand];
let response = http.get(`http://ecommerce.test.k6.io/product/${productSelected}`, {
tags: { name: 'ProductPage' },
});
}
この例では、動的に生成された商品ページのURLがすべて ProductPage
という共通タグに集約されます。これにより、複数のリクエストを一括で分析することができます。
スクリプトの可読性向上
k6のスクリプトは、コメントや関数、モジュール化によって整理することができます。これにより、スクリプトの可読性が向上し、複雑なシナリオの管理が容易になります。
コメントと関数の活用例
import http from 'k6/http';
export default function () {
Homepage();
ProductPage();
CheckoutPage();
}
function Homepage() {
// ホームページにアクセスするリクエスト
}
function ProductPage() {
// 商品ページにアクセスするリクエスト
}
function CheckoutPage() {
// チェックアウトに進むリクエスト
}
このように、各トランザクションを関数に分けて整理することで、スクリプトの可読性を高め、再利用性も向上させることができます。
参照:
Reference
- https://grafana.com/docs/k6/latest/using-k6/http-requests/#url-grouping
- https://github.com/grafana/k6-learn/blob/main/Modules/III-k6-Intermediate/06-Organizing-code-in-k6-by-transaction_groups-and-tags.md#url-grouping
- https://community.grafana.com/t/getting-warn-consider-not-using-high-cardinality-values-like-unique-ids-as-metric-tags/98498
- https://community.grafana.com/t/k6-browser-generated-high-unique-time-series-data/99768
- https://stackoverflow.com/questions/78682383/k6-running-out-of-memory-because-of-metrics-during-load-test
Discussion