Tutorials 11 August 2020

How to generate a constant request rate in k6 with the new scenarios API?

Mostafa Moradian, Developer Advocate

Overview

This v0.27.0 release includes a new execution engine and lots of new executors that cater to your specific needs. It also includes the new scenarios API with lots of different options to configure and model the load on the system under test (SUT). This is the result of 1.5 years of work on the infamous #1007 PR 😂.

For generating constant request rates, we may use the constant-arrival-rate executor. This executor starts the test with iterations at a fixed rate for a specified duration. This allows k6 to dynamically change the amount of active VUs during a test run, with the goal of achieving the specified amount of iterations per time unit. In this article, I am going to explain how to use this scenario to generate constant request rates.

Basics of Scenarios Configuration Options

Let's look at different terms used in k6 to describe a test configuration in a scenario that uses a constant-arrival-rate executor:

  • executor:

    Executors are the workhorses of the k6 execution engine. Each one schedules VUs and iterations differently, and you'll choose one depending on the type of traffic you want to model to test your services.

  • rate and timeUnit:

    k6 tries to start rate iterations every timeUnit period. For example:

    • rate: 1, timeUnit: '1s' means "try to start 1 iteration every second"
    • rate: 1, timeUnit: '1m' means "try to start 1 iteration every minute"
    • rate: 90, timeUnit: '1m' means "try to start 90 iterations per minute", i.e. 1.5 iterations/s or try to start a new iteration every 667ms
    • rate: 50, timeUnit: '1s' means "try to start 50 iterations every second", i.e. 50 RPS if we have 1 request in our iteration, i.e. try to start a new iteration every 20ms
  • duration:

    The total duration of the scenario, excluding gracefulStop.

  • preAllocatedVUs:

    The number of VUs to pre-allocate before the test starts.

  • maxVUs:

    The maximum number of VUs to allow during the test run.

Together, these terms form a scenario, which is part of the test configuration options. The code snippet below is an example of a constant-arrival-rate scenario.

In this configuration, we have a constant_request_rate scenario, which is a unique identifier used as a label for the scenario. This scenario uses the constant-arrival-rate executor and executes for 1 minute. Each second (timeUnit), 1 iteration will be made (rate). The pool of pre-allocated virtual users contains 20 instances and may go up to 100, depending on the number of requests and iterations.

Keep in mind that initializing VUs mid-test could be a CPU-heavy task and might skew your test results. In general, it's better for the preAllocatedVUs to be enough to run the load test. So, make sure to allocate more VUs depending on the number of requests you have in your test and the rate at which you want to run the test.

export let options = {
  scenarios: {
    constant_request_rate: {
      executor: 'constant-arrival-rate',
      rate: 1,
      timeUnit: '1s',
      duration: '1m',
      preAllocatedVUs: 20,
      maxVUs: 100,
    }
  }
};

Example of Generating Constant Request Rate with constant-arrival-rate

In the previous tutorial, we demonstrated how to calculate a constant request rate. Let's run through it again, taking into account how scenarios work:

Suppose that you expect your SUT to handle 1000 requests per second on an endpoint. Pre-allocating 100 VUs (with a maximum of 200) allows each VU to send roughly 5~10 requests (based on 100~200 VUs). If each request takes more than 1 second to complete, you'll end up making fewer requests than expected (more dropped_iterations), which is a sign of either performance issues with your SUT or having unrealistic expectations. Thus, fix performance issues and test again, or lower your expectations by adjusting the timeUnit.

In this scenario, each pre-allocated VU will make 10 requests (rate divided by preAllocatedVUs). If the requests don't make it in 1 second, e.g. the response took more than 1 second to receive or your SUT took more than 1 second to complete the task, k6 will increase the number of VUs to account for missing requests. The following test generates 1000 requests per second and runs for 30 seconds, which roughly generates 30,000 requests, as you can see below in the output: http_reqs and iterations. Also, k6 has only used 148 VUs from the pool of 200 VUs.

import http from 'k6/http';

export let options = {
    scenarios: {
        constant_request_rate: {
            executor: 'constant-arrival-rate',
            rate: 1000,
            timeUnit: '1s', // 1000 iterations per second, i.e. 1000 RPS
            duration: '30s',
            preAllocatedVUs: 100, // how large the initial pool of VUs would be
            maxVUs: 200, // if the preAllocatedVUs are not enough, we can initialize more
        }
    }
};

export default function () {
    http.get('http://test.k6.io/contacts.php');
}

The result of executing this script is as follows:

$ k6 run test.js

          /\      |‾‾|  /‾‾/  /‾/
     /\  /  \     |  |_/  /  / /
    /  \/    \    |      |  /  ‾‾\
   /          \   |  |\  \ | (_) |
  / __________ \  |__|  \__\ \___/ .io

  execution: local
     script: test.js
     output: -

  scenarios: (100.00%) 1 executors, 200 max VUs, 1m0s max duration (incl. graceful stop):
           * constant_request_rate: 1000.00 iterations/s for 30s (maxVUs: 100-200, gracefulStop: 30s)


running (0m30.2s), 000/148 VUs, 29111 complete and 0 interrupted iterations
constant_request_rate ✓ [======================================] 148/148 VUs  30s  1000 iters/s


    data_received..............: 21 MB  686 kB/s
    data_sent..................: 2.6 MB 85 kB/s
    *dropped_iterations.........: 889    29.454563/s
    http_req_blocked...........: avg=597.53µs min=1.64µs  med=7.28µs   max=152.48ms p(90)=9.42µs   p(95)=10.78µs
    http_req_connecting........: avg=561.67µs min=0s      med=0s       max=148.39ms p(90)=0s       p(95)=0s
    http_req_duration..........: avg=107.69ms min=98.75ms med=106.82ms max=156.54ms p(90)=111.73ms p(95)=116.78ms
    http_req_receiving.........: avg=155.12µs min=21.1µs  med=105.52µs max=34.21ms  p(90)=147.69µs p(95)=190.29µs
    http_req_sending...........: avg=46.98µs  min=9.81µs  med=41.19µs  max=5.85ms   p(90)=53.33µs  p(95)=67.3µs
    http_req_tls_handshaking...: avg=0s       min=0s      med=0s       max=0s       p(90)=0s       p(95)=0s
    http_req_waiting...........: avg=107.49ms min=98.62ms med=106.62ms max=156.39ms p(90)=111.52ms p(95)=116.51ms
    *http_reqs..................: 29111  964.512705/s
    iteration_duration.........: avg=108.54ms min=99.1ms  med=107.08ms max=268.68ms p(90)=112.09ms p(95)=118.96ms
    *iterations.................: 29111  964.512705/s
    vus........................: 148    min=108 max=148
    vus_max....................: 148    min=108 max=148

The are some things to consider while writing your test script:

  1. Since k6 follows redirects, the number of redirects add to the total number of RPS in the result output. If you don't want that, you can disable it globally by setting maxRedirects: 0 in your options. You can also configure the maximum redirects on the http request itself, which will override the global maxRedirects.
  2. Complexity adds to the mix. So, keep the function being executed simple, preferably only executing a few requests, avoiding additional processing or sleep() calls where possible.
  3. You need a fair amount of VUs to achieve the desired results, otherwise, you'll encounter warning message(s) like the following. In this case, just increase the preAllocatedVUs and/or maxVUs, but keep in mind that you will reach the maximum capacity of the machine running the test at some point, where neither preAllocatedVUs, nor maxVUs, will make any difference.
    WARN[0005] Insufficient VUs, reached 100 active VUs and cannot initialize more  executor=constant-arrival-rate scenario=constant_request_rate
  4. As you can see in the above results, there are dropped_iterations and the number of iterations and http_reqs is less than the specified rate. Having dropped_iterations set means that there weren't enough initialized VUs to execute some of the iterations. The issue can generally be solved by increasing preAllocatedVUs. The precise value requires a bit of trial and error since it depends on different factors, including the endpoint response time, network throughput, and other related latencies.
  5. While testing, you may encounter the following warning message(s) that signify you have reached your operating system limits. So, consider fine-tuning your operating system:
    WARN[0008] Request Failed                                error="Get \"http://test.k6.io/contacts.php\": dial tcp 3.227.130.174:80: socket: too many open files"
  6. Beware that the scenarios API deprecates global usages of duration, vus and stages, although they can still be used. This also means that you can't use them together with scenarios.

Conclusion

Before the release of k6 v0.27.0, there was insufficient support for generating constant request rates. Therefore, we introduced a JavaScript workaround by calculating the time it takes for requests to exhaust each iteration of the script. With v0.27.0, this is no longer needed.

In this article, I've explained how k6 can achieve a constant request rate by using the new scenarios API with the constant-arrival-rate executor. This executor simplifies the code and provides the means to achieve fixed RPS. This is in contrast with the previous version of the same article which I described another method to achieve pretty much the same results by calculating the number of VUs, iterations and duration using a formula and some boilerplate JavaScript code. Fortunately, this new approach works as intended and we don't need to use any hacks anymore.

I hope you enjoyed reading this article and I'd be happy to hear your feedback.

< Back to all posts