tl;dr;
High memory consumption during k6 load tests is often a result of high cardinality metrics caused by dynamic URLs and unique identifiers in tags. By strategically using custom tags, URL grouping, and organizing your script effectively, you can significantly reduce memory usage and obtain more meaningful metrics.
Implement these practices in your load testing strategy to enhance performance, reduce resource consumption, and gain clearer insights into your application's behavior under load.
When conducting load tests with k6, a powerful open-source tool for performance testing, you might encounter unexpected memory shortages. This issue often arises due to the generation of a large number of unique metrics, especially when testing applications with dynamic URLs. In this article, we'll delve into why this happens and how to effectively mitigate the problem.
Understanding the Issue
During extensive load testing, you may come across a warning like this:
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.
This warning indicates that k6 has generated over 100,000 unique time series metrics, leading to increased memory consumption. In some cases, this can even cause your load testing pod to be terminated by the Kubernetes OOMKilled mechanism due to excessive memory usage.
What causes high memory usage?
The root cause lies in the concept of metric cardinality. In k6, cardinality refers to the number of unique values in a metric. When you perform requests to dynamic URLs—like http://example.com/posts/1
, http://example.com/posts/2
, and so on—each unique URL is treated as a separate metric time series. By default, k6 uses the request URL as a tag for metrics if no explicit tag is provided.
As a result, tests involving dynamic URLs or unique query parameters can quickly generate an overwhelming number of unique metrics. Each unique metric consumes memory, and thousands of them can strain system resources.
Scenarios that increase cardinality
Here are common scenarios that can cause a spike in metric cardinality:
- Dynamic URLs: Each request to a unique URL (e.g., individual product pages) creates a new metric.
- Unique Query or Path Parameters: Different parameters in requests lead to separate metrics.
- Unique IDs in Tags or Labels: Assigning unique identifiers like user IDs or transaction IDs to tags.
The impact on load testing
High cardinality not only increases memory usage but also complicates the analysis of test results. With so many unique metrics, it becomes challenging to aggregate data and draw meaningful conclusions about your application's performance.
In practical terms, this means your load testing tool might consume all available memory and crash, or your test environment might kill the process to preserve system stability.
Solutions to reduce memory usage
To prevent high memory consumption, you need to reduce the number of unique metrics generated during testing. This can be achieved by aggregating metrics using tags and URL grouping.
Using custom tags
By explicitly setting a custom tag for your requests, you can group multiple dynamic URLs under a single metric. Here's how you can modify your script:
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' },
});
}
}
In this example, all requests to different posts
URLs are tagged with PostsItemURL
. This means k6 will treat them as a single metric, significantly reducing the number of unique metrics and, consequently, memory usage.
Using template literals with http.url
Alternatively, you can use http.url
with template literals to normalize dynamic URLs:
import http from 'k6/http';
export default function () {
for (let id = 1; id <= 100; id++) {
http.get(http.url`http://example.com/posts/${id}`);
}
}
By wrapping the URL with http.url
and using a template literal, k6 recognizes the pattern and treats all requests matching this template as the same metric.
Diving deeper: Transactions, Groups, and Tags in k6
To further optimize your load tests and make your scripts more maintainable, it's essential to understand how k6 handles transactions, groups, and tags.
Groups
Groups allow you to organize your requests into logical sections, making it easier to measure the total response time for a set of actions.
import { group } from 'k6';
export default function () {
group('01_VisitHomepage', function () {
// Code to visit the homepage
});
group('02_ClickOnProduct', function () {
// Code to navigate to a product page
});
}
In this script, requests are categorized into 01_VisitHomepage
and 02_ClickOnProduct
. k6 will record metrics for each group, enabling you to analyze performance at a higher level.
Tags
Tags are labels you can assign to requests and custom metrics. They are invaluable for filtering and analyzing data.
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',
},
});
});
}
Here, the request to the homepage is tagged with page: 'Homepage'
and type: 'HTML'
. These tags help you filter results when analyzing test data.
URL Grouping
URL Grouping lets you consolidate dynamically generated URLs into a single metric by assigning a common tag.
import http from 'k6/http';
export default function () {
let products = ['album', 'beanie', 'beanie-with-logo'];
let rand = Math.floor(Math.random() * products.length);
let productSelected = products[rand];
http.get(`http://ecommerce.test.k6.io/product/${productSelected}`, {
tags: { name: 'ProductPage' },
});
}
In this script, requests to various product pages are all tagged with ProductPage
, ensuring they are grouped under the same metric.
Enhancing script readability
Organizing your scripts improves not only performance but also maintainability. Use comments, functions, and modularization to make your code cleaner.
Using functions and comments
import http from 'k6/http';
export default function () {
homepage();
productPage();
checkoutPage();
}
function homepage() {
// Request to access the homepage
}
function productPage() {
// Request to access a product page
}
function checkoutPage() {
// Request to proceed to checkout
}
By encapsulating each set of actions into functions, you make your script more readable and reusable.
References
For more tips and insights, follow me on Twitter @Siddhant_K_code and stay updated with the latest & detailed tech content like this.