4. k6 内置参数化方法:__VU 与 __ITER 内置变量

在容量压测中,模拟多用户并发执行多次迭代是核心需求。k6 提供了两个内置变量——__VU 和 __ITER,无需额外安装或配置即可在脚本中直接使用。它们是最轻量级的参数化手段,适用于循环控制、用户身份区分以及日志追踪场景。

4.1 内置变量概述

变量名 类型 含义 取值范围
__VU 整数 当前执行该迭代的虚拟用户编号 1 ~ options.scenarios[*].vus(或 options.vus)
__ITER 整数 当前虚拟用户已执行的迭代次数(从 0 开始) 0 ~ (iterations - 1) 或持续时间内递增
关键区别:__VU 在同一个 VU 的生命周期内保持不变;__ITER 在每个 VU 内部独立计数,不同 VU 的 __ITER 值互不影响。

4.2 基础用法:打印用户与迭代编号

以下脚本展示了如何在 default 函数中输出 __VU 和 __ITER,验证其取值规律:

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

export const options = {
  vus: 3,
  iterations: 6,
};

export default function () {
  console.log(`VU: ${__VU}, ITER: ${__ITER}`);
  http.get('https://test.k6.io');
  sleep(1);
}

执行结果分析(部分输出):

INFO[0000] VU: 1, ITER: 0
INFO[0000] VU: 2, ITER: 0
INFO[0000] VU: 3, ITER: 0
INFO[0001] VU: 1, ITER: 1
INFO[0001] VU: 2, ITER: 1
INFO[0001] VU: 3, ITER: 1

可以看到:3 个 VU 并行执行,每个 VU 各自从 ITER=0 开始计数,总共 6 次迭代(每个 VU 执行 2 次)。

4.3 基于 __VU 实现用户身份轮询

在容量压测中,不同 VU 通常需要模拟不同用户。利用 __VU 作为数组索引,可以轻松实现用户身份轮询:

const users = [
  { username: 'user_a', : '_a' },
  { username: 'user_b', : '_b' },
  { username: 'user_c', : '_c' },
];

export default function () {
  // 使用 __VU - 1 作为索引(__VU 从 1 开始)
  const currentUser = users[__VU - 1];
  console.log(`VU ${__VU} 使用用户: ${currentUser.username}`);

  const payload = JSON.stringify({
    username: currentUser.username,
    : currentUser.,
  });

  http.post('https://api.example.com/login', payload, {
    headers: { 'Content-Type': 'application/json' },
  });
  sleep(1);
}
数组越界风险:当 options.vus 大于用户数组长度时,__VU - 1 会超出数组范围,导致 undefined 错误。建议使用取模运算 users[(__VU - 1) % users.length] 来安全轮询。

4.4 基于 __ITER 实现迭代内循环控制

__ITER 常用于在同一个 VU 的多次迭代中切换行为。例如,每 3 次迭代执行一次特殊操作:

export default function () {
  // 每 3 次迭代执行一次清理操作
  if (__ITER % 3 === 0) {
    console.log(`VU ${__VU} 执行清理操作,当前 ITER: ${__ITER}`);
    http.post('https://api.example.com/cleanup');
  }

  http.get('https://api.example.com/data');
  sleep(1);
}

4.5 组合使用:用户 + 迭代双维度参数化

将 __VU 和 __ITER 组合,可以实现二维参数化,适用于需要同时区分用户身份和操作步骤的场景:

const searchKeywords = ['手机', '电脑', '耳机', '键盘', '鼠标'];

export default function () {
  // 用户维度:不同 VU 使用不同用户
  const userId = `user_${__VU}`;

  // 迭代维度:同一用户每次迭代搜索不同关键词
  const keywordIndex = __ITER % searchKeywords.length;
  const keyword = searchKeywords[keywordIndex];

  console.log(`VU ${__VU} (${userId}) 第 ${__ITER} 次搜索: ${keyword}`);

  http.get(`https://api.example.com/search?q=${keyword}&uid=${userId}`);
  sleep(1);
}

4.6 内置变量与场景配置的配合

当使用 scenarios 配置多场景时,__VU 和 __ITER 在每个场景内独立计数:

export const options = {
  scenarios: {
    smoke: {
      executor: 'per-vu-iterations',
      vus: 2,
      iterations: 3,
      exec: 'smokeTest',
    },
    load: {
      executor: 'ramping-vus',
      startVUs: 1,
      stages: [
        { duration: '10s', target: 5 },
        { duration: '20s', target: 5 },
      ],
      exec: 'loadTest',
    },
  },
};

export function smokeTest() {
  console.log(`[smoke] VU: ${__VU}, ITER: ${__ITER}`);
  http.get('https://test.k6.io');
}

export function loadTest() {
  console.log(`[load] VU: ${__VU}, ITER: ${__ITER}`);
  http.get('https://test.k6.io');
  sleep(1);
}

两个场景的 __VU 和 __ITER 互不干扰,各自从 1 和 0 开始计数。

4.7 内置变量工作流程

下图展示了 k6 执行引擎如何为每个 VU 分配 __VU 编号,并在每次迭代中递增 __ITER:

k6 内置变量 __VU 与 __ITER 工作流程 k6 执行引擎 虚拟用户池(VU Pool) VU 1 __VU = 1 __ITER: 0,1,2,… VU 2 __VU = 2 __ITER: 0,1,2,… VU 3 __VU = 3 __ITER: 0,1,2,… default() 函数执行体 每个 VU 独立维护 __ITER 计数器,__VU 在 VU 创建时分配且不再变化

4.8 常见陷阱与最佳实践

  • 陷阱 1:__VU 从 1 开始,不是 0。用作数组索引时需减 1。
  • 陷阱 2:在 setup() 和 teardown() 函数中,__VU 和 __ITER 不可用,因为这两个函数不在 VU 上下文中执行。
  • 陷阱 3:使用 shared-iterations 执行器时,__ITER 在所有 VU 间共享递增,不再是每个 VU 独立计数。
  • 最佳实践:在日志输出中始终包含 __VU 和 __ITER,便于问题定位和结果分析。
  • 最佳实践:当 VU 数量超过参数数据源长度时,使用取模运算 % 实现循环复用。