Spike testing with k6

3 minute read

Spike testing

Spike testing is a variation of stress testing that focuses on causing a sudden, short-lived surge of load on the system at test and then measuring:

  • how it performs during the spike
  • how it recovers, afterwards

Real-world examples of this scenario are the infamous Thundering Herd situation, where lots of processes/devices try to start/connect all at the same time, or the “HackerNews hug of death”, where your website suddenly becomes (even more) popular, potentially crashing it.

🎶 “too much love will kill you” 🎶

k6

k6 is an open-source tool for performance testing maintained by the Grafana team: in k6, you write your test scripts in Javascript and run them using its CLI. If you’re using Gitlab Premium (13.2+), there’s a built-in integration to run tests right from the pipeline.

In a k6 test there are a number of parallel virtual users (VUs) running a number of iterations (executions) over a span of time (duration): these are configured making use of the corresponding options. Next to that, you can decide how the executor will schedule VUs, according to the scenario you want to simulate.

Example: sudden increase of website views

Let’s see a concrete example: your website gets featured on the first page of Hacker News and it suddenly gets 10x more traffic as usual.

// tests/script.js

import http from 'k6/http';
import { check } from 'k6';

// 1. init code
export const options = {
  stages: [
    { duration: '30s', target: 20 },
    { duration: '1m', target: 200 },
    { duration: '30s', target: 20 },
  ],
};

// 2. VU code
export default function () {
  const res = http.get('<your website>');
  check(res, { 'status was 200': (r) => r.status == 200 });
}

Let’s start with a general walkthrough of a k6 script:

  • any code written outside of a function belongs to the init phase of the script, where you can configure how the test will run and declare any resources it needs (e.g. open files): this code is executed once per VU;
  • code written inside the default() function is executed by each VU (how many times and for how long depends on the test configuration);
  • there are 2 additional, optional functions called setup and teardown: these, if defined, are executed once per script (at the beginning and end of its execution, respectively);
export const options = {
  stages: [
    { duration: '30s', target: 20 },
    { duration: '1m', target: 200 },
    { duration: '30s', target: 20 },
  ],
};

The stages array defined inside the options object describes how VUs will ramp up/down during the test:

  • for the first 30 seconds, there will be 20 VUs (where 20 represents the usual number of concurrent visitors of your website)
  • for the next minute, there will be 200 VUs (the 10x spike)
  • for the final 30 seconds, there will be again 20 VUs (going back to the usual load)

When you run this script, with

k6 run tests/script.js

it will print to console a summary report showing the result of its execution.

Example: thundering herd

Let’s assume that you have 1000 IoT devices that, on boot, try to connect to your backend: you have 3 instance of a gateway application that receive the connection requests and issues authentication requests to the auth application to verify if the device is allowed to connect. You want to test how the auth application will perform when all devices try to reconnect at the same time.

// tests/script-2.js

import http from 'k6/http';
import { check } from 'k6';

export const options = {
  executor: 'shared-iterations',
  iterations: 1000,
  vus: 3,
};

export default function () {
  const res = http.get('<auth application endpoint>');
  check(res, { 'status was 200': (r) => r.status == 200 });
}

In this case, there’s no ramping up/down of VUs: what you want to do is send out the authentication requests as quickly as allowed by the response times of auth, thus there’s no need for stages.

This is exactly the situation for which the shared-iterations executor has been developed: this type of executor will spread the iterations over the 3 VUs using a work-stealing scheduling algorithm, meaning that it will assign an iteration to any VU that is currently available, aiming to reach the total number as quickly as possible without caring to spread the load equally over the VUs.

Conclusion

This is just a glimpse of what k6 can do: it has many more executors and options to choose from.