Why are VUs numbered randomly?

When I use vuserID = ${__VU};, I notice that the numbering does not start at 1 and increment from there, but instead starts at a random number from 1 to max VUs. Is there a reason for that?

I ask because I also use let login = logins[vuserID - 1]; to grab my logins from a file, and I would like more control over sequentially using my logins. Specifically, I always want the VU number and file index to match, and to proceed sequentially.

Is there an option to do this?

Hi @leathej1,

K6 VUs are sequentially numbered. The VUs execution the default/exec function start at 1 and go up and the VU(s) executing the initial init context and setup/teardown is numbered 0.

Due to scenarios VUs in a given scenario (if there are multiple) may not be ordered but in the whole instance if you see VU number 200 there are another 199 that are also there.

Can you provide any code sample that reproduces the behavior that you are seeing?

Here is a post explaining how to get unique indexes.

Hope this helps you!

Here is an example that shows this behavior:

import { sleep } from 'k6';
export let options = {
    rps: 1,
    stages: [
      {
        duration: '30s', target: 10
      }
    ],
  };

export default function printVuser() {

    let vuserID = `${__VU}`;
    let iterNum = `${__ITER}`;

    console.log(`VU: ${vuserID} ITER: ${iterNum}`);

    sleep(1);
}

This is somewhat complicated to explain, but here it goes:

Stages (or ramping-vus as it’s the actual executor name) works by going from one number of VU executing as many iterations as possible to another one gradually. This is called stages and in your case above it goes from 1 to 10 over 30s. It goes from 1 because this is the default number for vus.

This means that at the very beginning 1 VU will do iterations. In order to get to 10 over the 30s k6 will start 1 more VU every 30/(10-1)=30/9~=3.(3)s. This means that the first 4 iterations will be started by 1 VU. and around 0.3s on the 4th iteration a second VU will start doing iterations. Then after another 3.3s another VU will be started,(the first one would’ve already started its 7th iteration and the second VU would’ve started it 4th). And so on and so on.

This means that the “first” VU would do a lot more iteration then the latter ones.

Due to implementation details, it is not required that the “first” VU will be numbered 1 and the “second” 2 etc. This is probably why you decide that they are numbered “randomly”.

But if you for example start with 10 VUs so there isn’t actually a case where only some of the VUs are working, you will see how all the VUs are doing the same amount of iterations. Given that in general ramping up/down is a small portion of the execution of the script it should not be a problem that 1 VU did 10 more iterations when they all did over 10k or something like that.

Hope this explanation helps you understand what is happening.

1 Like

No, actually - I didn’t understand that. I guess my simpler question is how do I enable the behavior I was expecting - that VUs start in numerical order?

The short answer is that, unfortunately, you currently can’t… This problem will be partially solved by Improve execution information in scripts · Issue #1320 · loadimpact/k6 · GitHub, parts of which we hope to release in the upcoming k6 v0.32.0 (ETA May 3rd). However, depending on your use case and requirements, even that might be enough.

The root of the issue is that the __VU global constant is very restricted. It represents the unique global sequential number for the current virtual user. However, since v0.27.0, k6 initializes VUs in parallel, 1 VU for every CPU core on your machine. So, if you have 4 CPU cores on your machine, 4 VUs will be concurrently initialized, and if VU #3 is initialized a millisecond faster than VU #1, your pool of workers (i.e. VUs) will be [VU3, VU1, VU2, VU4, ...].

Moreover, when you have multiple scenarios, k6 reuses VUs between non-overlapping scenarios, to reduce resource usage. Some scenarios may also allocate new VUs mid-test. And, most importantly, the duration of every script iteration varies, so even if VUs were perfectly ordered by __VU at the start, after a few script iterations the order will completely disintegrate.

So, yeah, __VU cannot be relied on to be sequential, and we’ll solve some of these issues in Improve execution information in scripts · Issue #1320 · loadimpact/k6 · GitHub by introducing new APIs that would be able to return the sequential VU and iteration number in a single scenario. Depending on your use case, that might or might not be enough.

Here’s a modified version of your example script that demonstrates these issues:

import { sleep } from 'k6';
export let options = {
    stages: [
        { duration: '30s', target: 10 }
    ],
};

console.log(`Init phase of VU ${__VU}`);
sleep(3);

export default function printVuser() {
    console.log(`Running VU: ${__VU} ITER: ${__ITER}`);
    sleep(35);
}

console.log(`Init phase of VU ${__VU} done`);

I’ve made the VU initialization take 3 seconds with sleep(3), to make the init concurrency apparent. And the sleep(35) makes it so that a single script iteration is longer than the whole script duration, so every VU is going to execute only a single iteration for the whole script. If I run this script on my 4-core machine, I’d get a log like this:

INFO[0003] Init phase of VU 1                            source=console
INFO[0003] Init phase of VU 3                            source=console
INFO[0003] Init phase of VU 4                            source=console
INFO[0003] Init phase of VU 2                            source=console
INFO[0006] Init phase of VU 1 done                       source=console
INFO[0006] Init phase of VU 4 done                       source=console
INFO[0006] Init phase of VU 3 done                       source=console
INFO[0006] Init phase of VU 2 done                       source=console
INFO[0006] Init phase of VU 5                            source=console
INFO[0006] Init phase of VU 6                            source=console
INFO[0006] Init phase of VU 7                            source=console
INFO[0006] Init phase of VU 8                            source=console
INFO[0009] Init phase of VU 8 done                       source=console
INFO[0009] Init phase of VU 7 done                       source=console
INFO[0009] Init phase of VU 5 done                       source=console
INFO[0009] Init phase of VU 6 done                       source=console
INFO[0009] Init phase of VU 9                            source=console
INFO[0009] Init phase of VU 10                           source=console
INFO[0012] Init phase of VU 9 done                       source=console
INFO[0012] Init phase of VU 10 done                      source=console
INFO[0012] Init phase of VU 0                            source=console
INFO[0015] Init phase of VU 0 done                       source=console
INFO[0015] Running VU: 1 ITER: 0                         source=console
INFO[0018] Running VU: 4 ITER: 0                         source=console
INFO[0022] Running VU: 3 ITER: 0                         source=console
INFO[0025] Running VU: 2 ITER: 0                         source=console
INFO[0028] Running VU: 8 ITER: 0                         source=console
INFO[0032] Running VU: 7 ITER: 0                         source=console
INFO[0035] Running VU: 5 ITER: 0                         source=console
INFO[0038] Running VU: 6 ITER: 0                         source=console
INFO[0042] Running VU: 10 ITER: 0                        source=console
INFO[0075] Init phase of VU 0                            source=console
INFO[0078] Init phase of VU 0 done                       source=console

Ignoring the Init phase of VU 0 logs (that’s for setup() and teardown()), the timestamp between the INFO[] brackets makes it apparent that VU initialization happens 4 VUs at a time, and because of the concurrency, VUs may be initialized out of order.

If you pin k6 to use only a single CPU core, by running GOMAXPROCS=1 k6 run script.js, you’d get what you expected, albeit with a great performance penalty:

INFO[0003] Init phase of VU 1                            source=console
INFO[0006] Init phase of VU 1 done                       source=console
INFO[0006] Init phase of VU 2                            source=console
INFO[0009] Init phase of VU 2 done                       source=console
INFO[0009] Init phase of VU 3                            source=console
INFO[0012] Init phase of VU 3 done                       source=console
INFO[0012] Init phase of VU 4                            source=console
INFO[0015] Init phase of VU 4 done                       source=console
INFO[0015] Init phase of VU 5                            source=console
INFO[0018] Init phase of VU 5 done                       source=console
INFO[0018] Init phase of VU 6                            source=console
INFO[0021] Init phase of VU 6 done                       source=console
INFO[0021] Init phase of VU 7                            source=console
INFO[0024] Init phase of VU 7 done                       source=console
INFO[0024] Init phase of VU 8                            source=console
INFO[0027] Init phase of VU 8 done                       source=console
INFO[0027] Init phase of VU 9                            source=console
INFO[0030] Init phase of VU 9 done                       source=console
INFO[0030] Init phase of VU 10                           source=console
INFO[0033] Init phase of VU 10 done                      source=console
INFO[0033] Init phase of VU 0                            source=console
INFO[0036] Init phase of VU 0 done                       source=console
INFO[0036] Running VU: 1 ITER: 0                         source=console
INFO[0039] Running VU: 2 ITER: 0                         source=console
INFO[0043] Running VU: 3 ITER: 0                         source=console
INFO[0046] Running VU: 4 ITER: 0                         source=console
INFO[0049] Running VU: 5 ITER: 0                         source=console
INFO[0053] Running VU: 6 ITER: 0                         source=console
INFO[0056] Running VU: 7 ITER: 0                         source=console
INFO[0059] Running VU: 8 ITER: 0                         source=console
INFO[0063] Running VU: 9 ITER: 0                         source=console
INFO[0096] Init phase of VU 0                            source=console
INFO[0099] Init phase of VU 0 done                       source=console

Ah! That explains it very well. OK, well now that I understand the technical hurdles, I will work around them. Thanks again.