1. 为什么是 K6,而不是 JMeter 或 Locust?

我第一次在生产环境压测一个订单履约服务时,用的是 JMeter。当时团队刚上线新版本,老板说“先跑个500并发看看稳不稳”。我信心满满地搭好测试计划,加了响应断言、聚合报告、后置处理器,结果一跑起来——本机 CPU 直冲98%,内存飙到12GB,JMeter GUI 卡成PPT,线程数调到300就报 OutOfMemoryError。最后硬是靠三台从测试同事那儿借来的笔记本,装上 JMeter Server 分布式压测,才勉强凑出500并发。但整个过程光配置环境、同步脚本、排查端口冲突就花了整整两天。

后来我们切到 Locust,体验好了不少:Python 写脚本、协程模型轻量、分布式启动也简单。可问题又来了——某次压测中,Locust master 节点突然卡死,日志里只有一行 Worker heartbeat timeout ,查了两小时才发现是某个 worker 进程里写了 time.sleep(30) ,阻塞了整个事件循环。更麻烦的是,Locust 的指标上报依赖 HTTP 轮询,当集群规模扩大到20+ worker 时,master 的 /stats/requests 接口开始超时,监控数据断断续续,根本没法做实时决策。

直到去年接手一个海外支付网关的压测任务,要求支持每秒3万请求(RPS)、持续30分钟、全链路埋点、自动失败熔断、CI/CD 原生集成——我们试了 K6。第一天写完第一个 .js 脚本,本地跑通1000并发,资源占用不到400MB内存、CPU峰值35%;第二天接入 Grafana + Prometheus,所有指标毫秒级刷新;第三天把 k6 run 命令塞进 GitHub Actions,每次 PR 合并自动触发基准压测,失败直接阻断发布流水线。那一刻我才真正理解:K6 不是一个“又一个压测工具”,而是一套为现代云原生系统量身打造的 可观测性优先的性能验证工作流

它解决的从来不是“怎么发起请求”这个低阶问题,而是“如何让性能验证像单元测试一样可编程、可版本化、可自动化、可归因”。关键词很明确: K6、性能测试框架、云原生压测、JavaScript 脚本、CI/CD 集成、Prometheus 监控、HTTP/HTTPS/WebSocket 支持、资源轻量、分布式执行 。它适合三类人:正在被传统压测工具拖慢交付节奏的 DevOps 工程师;需要把性能门槛前移到开发阶段的 SRE;以及任何想用 Git 管理压测逻辑、用 PR Review 审核性能变更的现代技术团队。

这不是“换个工具试试”,而是把性能验证从“项目末期救火”变成“日常开发习惯”的一次范式迁移。

2. K6 的核心设计哲学:为什么用 JavaScript,为什么是 Go 写的运行时?

很多人第一眼看到 K6 的 .js 脚本,会下意识觉得:“啊?压测脚本还能用 JS 写?”——这恰恰是它最反直觉、也最精妙的设计起点。

先说结论:K6 的脚本层用 JavaScript(ES6+),底层运行时用 Go 编写,二者通过 V8 引擎桥接。这个组合不是为了赶时髦,而是为了解决三个本质矛盾:

  • 开发效率 vs 执行效率 :JS 提供极高的表达力和生态复用性(JSON 处理、正则、Promise、async/await),让编写复杂业务场景(如登录→领券→下单→支付→查单)变得像写业务代码一样自然;而 Go 运行时保证了单进程承载数千虚拟用户(VU)的能力,实测中一个 4C8G 的 k6 实例可稳定支撑 8000+ VU,内存占用始终控制在 1.2GB 以内。

  • 可调试性 vs 生产稳定性 :JS 脚本可直接在 Chrome DevTools 中调试(k6 支持 --inspect 模式),断点、变量监视、堆栈追踪一应俱全;而 Go 运行时屏蔽了所有底层调度细节,你永远不需要关心 goroutine 泄漏、GC 停顿或文件描述符耗尽——这些由 k6 runtime 全权接管。

  • 跨平台一致性 vs 云原生适配性 :JS 脚本零依赖,Windows/macOS/Linux 通用;Go 编译产物是静态二进制,无运行时环境要求,可直接打包进 Alpine 镜像,完美嵌入 Kubernetes Job 或 Argo Workflows。

提示:不要把 k6 的 JS 当作 Node.js 来用。它没有 fs net child_process 等模块,也不支持 CommonJS require() 。所有 I/O 操作必须通过 k6 自带的 API(如 http.get() check() sleep() )完成。这是刻意为之的“沙箱隔离”——确保脚本行为在本地开发、CI 流水线、生产压测环境中完全一致。

再看它的执行模型。K6 不是“多线程模拟用户”,而是“基于 VU(Virtual User)的协程式并发”。每个 VU 是一个独立的 JS 执行上下文,拥有自己的变量作用域、生命周期钩子( setup() / default() / teardown() )和本地状态。VU 之间完全隔离,无法共享内存,通信只能通过 sharedArray (只读)或外部服务(如 Redis)。这种设计彻底规避了传统工具中常见的“线程安全陷阱”——你再也不用给全局计数器加锁,也不用担心 session ID 在不同线程间错乱。

举个真实例子:我们曾压测一个 JWT 鉴权服务,需要每个 VU 持有独立的 token 并在后续请求中复用。在 JMeter 里,这得靠 __setProperty + __property + 同步控制器,稍有不慎就 token 混用;而在 k6 中,只需:

export default function () {
  // 每个 VU 独立执行,token 变量天然隔离
  const token = getAuthToken(); 
  http.get('https://api.example.com/orders', {
    headers: { Authorization: `Bearer ${token}` }
  });
}

没有锁,没有竞态,没有调试噩梦。这就是“开发者心智负担最小化”的具象体现。

3. 从零写出第一个可落地的 K6 脚本:不只是 GET 请求

很多教程停在 k6 run script.js 和一个 http.get() 示例,但这离真实项目差了至少十层楼。真正的上手,是从写一个能反映业务真实路径、带校验、可配置、能进 CI 的脚本开始。下面我带你一步步拆解我们团队内部使用的「标准压测脚本模板」,它已稳定运行在 17 个微服务的每日回归压测中。

3.1 环境抽象与参数注入:告别硬编码

真实压测绝不会只跑一个 URL。你需要:

  • 区分 dev/staging/prod 环境的 base URL;
  • 动态传入并发数、持续时间、RPS 上限;
  • 为不同场景设置不同请求权重(如 70% 查询 / 20% 下单 / 10% 退款);
  • 加载测试数据(用户ID、商品SKU、优惠券码)。

K6 提供了三层参数机制:

  • 命令行参数 --vus , --duration , --rps )用于控制执行规模;
  • 环境变量 K6_ENV=staging )用于切换配置;
  • 自定义选项 --my-custom-opt value )配合 __ENV 对象读取。

我们的 config.js 如下:

// config.js - 统一配置中心
export const ENV_CONFIG = {
  staging: {
    baseUrl: 'https://staging-api.example.com',
    users: ['u1001', 'u1002', 'u1003'],
    skus: ['S1001', 'S1002']
  },
  prod: {
    baseUrl: 'https://api.example.com',
    users: __ENV.PROD_USERS?.split(',') || ['u9999'],
    skus: __ENV.PROD_SKUS?.split(',') || ['S9999']
  }
};

export const TEST_SCENARIOS = {
  readHeavy: { weight: 0.7, exec: 'readOrders' },
  writeHeavy: { weight: 0.2, exec: 'createOrder' },
  edgeCase: { weight: 0.1, exec: 'applyCoupon' }
};

脚本入口 main.js 中这样使用:

import { ENV_CONFIG, TEST_SCENARIOS } from './config.js';
import { check, sleep } from 'k6';
import http from 'k6/http';

const env = __ENV.K6_ENV || 'staging';
const config = ENV_CONFIG[env];

export const options = {
  vus: __ENV.K6_VUS ? parseInt(__ENV.K6_VUS) : 10,
  duration: __ENV.K6_DURATION || '30s',
  thresholds: {
    http_req_failed: ['rate<0.01'], // 错误率低于1%
    http_req_duration: ['p(95)<500'] // 95分位响应时间<500ms
  }
};

export default function () {
  const scenario = chooseScenario();
  if (scenario === 'readOrders') readOrders();
  else if (scenario === 'createOrder') createOrder();
  else applyCoupon();
}

function chooseScenario() {
  const rand = Math.random();
  let sum = 0;
  for (const [name, { weight }] of Object.entries(TEST_SCENARIOS)) {
    sum += weight;
    if (rand < sum) return name;
  }
  return 'readOrders';
}

注意: __ENV 是 k6 注入的全局对象,所有环境变量自动转为字符串。 parseInt() split(',') 是必须的手动类型转换——k6 不做隐式转换,这是为了杜绝因类型错误导致的压测逻辑偏差。

3.2 真实业务链路:带状态管理的多步骤流程

单纯发请求没意义。我们要模拟一个完整用户旅程:登录获取 token → 查询可用优惠券 → 下单并支付 → 查询订单状态。这个过程涉及:

  • Token 的获取与复用(每个 VU 独立);
  • 响应体中提取动态参数(如 coupon_id order_id );
  • 失败时的重试与降级(如 token 过期则重新登录);
  • 关键业务指标打点(如“下单成功耗时”、“支付回调延迟”)。

以下是 createOrder() 的完整实现:

function createOrder() {
  // 步骤1:确保有有效 token
  let token = getValidToken();
  
  // 步骤2:查询可用优惠券(带参数提取)
  const couponRes = http.get(`${config.baseUrl}/coupons/available`, {
    headers: { Authorization: `Bearer ${token}` }
  });
  const couponId = couponRes.json('data[0].id'); // 使用 JSONPath 提取
  
  // 步骤3:创建订单(带动态 body)
  const orderBody = JSON.stringify({
    user_id: config.users[__ENV.K6_VU_ID % config.users.length],
    sku: config.skus[__ENV.K6_VU_ID % config.skus.length],
    coupon_id: couponId || null
  });

  const orderRes = http.post(`${config.baseUrl}/orders`, orderBody, {
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json'
    }
  });

  // 步骤4:关键业务断言
  const checks = check(orderRes, {
    'order created': (r) => r.status === 201,
    'order id exists': (r) => r.json('id') !== undefined,
    'response time < 800ms': (r) => r.timings.duration < 800
  });

  // 步骤5:记录自定义指标(需配合 --metrics-enabled)
  if (checks['order created']) {
    // 这里可以 emit 自定义 metric,如 order_creation_time
  }

  sleep(1); // 模拟用户思考时间
}

其中 getValidToken() 是一个带缓存和过期检查的封装:

let cachedToken = null;
let tokenExpiry = 0;

function getValidToken() {
  if (Date.now() < tokenExpiry) return cachedToken;

  const loginRes = http.post(`${config.baseUrl}/auth/login`, JSON.stringify({
    username: 'testuser',
    password: 'testpass'
  }), {
    headers: { 'Content-Type': 'application/json' }
  });

  if (loginRes.status !== 200) {
    throw new Error(`Login failed: ${loginRes.status}`);
  }

  const data = loginRes.json();
  cachedToken = data.token;
  tokenExpiry = Date.now() + (data.expires_in - 60) * 1000; // 提前60秒刷新
  return cachedToken;
}

这个函数体现了 k6 的两个关键能力: VU 级别状态持久化 cachedToken 在当前 VU 生命周期内复用)和 异常中断机制 throw 会终止当前 VU 的本次迭代,但不影响其他 VU)。

3.3 数据驱动:从 CSV 到动态生成

硬编码测试数据只适用于 Demo。真实场景中,你需要:

  • 从 CSV 文件加载千级用户凭证;
  • 按比例分配不同等级会员(VIP/PRO/STANDARD);
  • 为每个用户生成唯一设备指纹(用于风控绕过)。

K6 原生支持 CSV 读取( open() 函数),但要注意: CSV 文件在 setup 阶段一次性加载进内存,所有 VU 共享只读副本 。这意味着你不能在 default() 中修改 CSV 数据,但可以用索引做轮询:

// data/users.csv
// username,password,level
// u1001,p1,PRO
// u1002,p2,VIP

const userData = open('./data/users.csv');
const csvData = parseCSV(userData);

export function setup() {
  // setup 阶段预处理:按 level 分组
  const grouped = {};
  for (const row of csvData) {
    if (!grouped[row.level]) grouped[row.level] = [];
    grouped[row.level].push(row);
  }
  return { usersByLevel: grouped };
}

export default function (data) {
  const level = chooseLevel(); // 根据权重选 VIP/PRO/STANDARD
  const users = data.usersByLevel[level];
  const user = users[__ENV.K6_VU_ID % users.length]; // 轮询取用户
  
  // 后续用 user.username/user.password 发起请求...
}

对于需要强唯一性的字段(如设备 ID),我们采用 __ENV.K6_VU_ID + 时间戳 + 随机数生成:

function generateDeviceId() {
  return `dev_${__ENV.K6_VU_ID}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}

这样每个 VU 在每次迭代中生成的 device_id 都不同,完美模拟真实终端多样性。

4. 生产级压测闭环:从执行到诊断的全链路实践

写完脚本能跑通只是起点。真正的“上手”,是建立起一套可持续、可归因、可优化的压测工作流。我们团队经过 23 次线上压测事故复盘后,固化了以下六个必做环节,缺一不可。

4.1 分布式执行:不止是 k6 cloud,还有自建集群

K6 官方提供 k6 Cloud 服务,但它更适合快速验证和小规模探索。在生产环境,我们全部采用自建 Kubernetes 集群 + k6 Operator 方案。原因很实际:

  • k6 Cloud 的免费额度仅支持 1000 VU,而我们常规压测需 5000~20000 VU;
  • 敏感接口(如支付、风控)严禁出内网,k6 Cloud 无法满足合规要求;
  • 自建集群可深度定制网络策略(如固定源 IP、QoS 限速)、资源配额(CPU/Memory Request/Limit)和日志采集。

我们的部署架构如下:

组件 数量 角色 资源规格
k6-controller 1 调度中心,接收 k6 run 命令,分发任务 2C4G
k6-runner 5~20 执行节点,每个 Pod 运行一个 k6 实例 4C8G(可水平扩展)
prometheus 1 指标采集与存储 4C16G
grafana 1 可视化看板 2C4G

关键 YAML 片段(k6-runner Deployment):

apiVersion: apps/v1
kind: Deployment
metadata:
  name: k6-runner
spec:
  replicas: 5
  template:
    spec:
      containers:
      - name: k6
        image: grafana/k6:v0.47.0
        resources:
          requests:
            memory: "6Gi"
            cpu: "3000m"
          limits:
            memory: "6Gi"
            cpu: "3000m"
        # 强制绑定到高性能节点
        nodeSelector:
          kubernetes.io/os: linux
          hardware-type: high-io

执行命令不再是 k6 run script.js ,而是:

k6 run \
  --vus 10000 \
  --duration 10m \
  --out statsd=10.10.10.10:8125 \  # 推送到 StatsD
  --tag env=staging \
  --tag service=payment-gateway \
  ./scripts/payment.js

注意: --out statsd 是关键。我们不依赖 k6 默认的 InfluxDB 输出,而是推送到公司统一的 StatsD 服务,再由 Telegraf 转发至 Prometheus。这样所有压测指标(HTTP 请求量、错误率、P95 延迟、VU 数)都进入同一监控体系,与应用自身指标(JVM GC、DB 连接池、线程数)同屏对比,故障定位效率提升 3 倍以上。

4.2 指标解读:看懂 k6 报告里的“真问题”

k6 默认输出的文本报告(summary)信息密度极高,但新手极易误读。我们整理了最常被误解的 5 个指标及其真实含义:

指标名 常见误读 真实含义 我们的判断阈值 诊断建议
http_req_waiting “等待服务器响应的时间” TCP 连接建立 + TLS 握手 + 服务器处理 + 网络传输总和 >200ms 需关注 http_req_connecting 高,查 DNS/网络;若 http_req_tls_handshaking 高,查证书链/OCSP;若 http_req_sending 高,查客户端带宽
http_req_duration “接口平均耗时” http_req_waiting + http_req_receiving (接收响应体时间) P95 < 500ms http_req_receiving 占比 >30%,说明响应体过大(如返回未分页的 10w 行日志)
vus_max “最大并发用户数” 压测期间达到的最高 VU 数(非目标值) 应 ≥ 设定 --vus 若远低于设定值,说明脚本存在阻塞(如 sleep(10) )或资源不足(CPU/Memory)
iterations “请求总次数” 所有 VU 完成的 default() 函数调用总数 每秒应 ≈ --vus × --rps 若远低于预期,检查脚本逻辑是否提前 return throw
checks “断言通过率” 所有 check() 函数的布尔结果汇总 关键业务 check 必须 100% 若某 check 失败率突增,立即停止压测,检查对应业务逻辑(如优惠券库存扣减)

我们强制要求:每次压测后,必须导出 JSON 报告( k6 run --out json=report.json script.js ),用 Python 脚本解析并生成诊断摘要。例如,自动识别“高延迟根因”:

# analyze_report.py
import json
with open('report.json') as f:
    data = json.load(f)

waiting_p95 = data['metrics']['http_req_waiting']['p95']
connecting_p95 = data['metrics']['http_req_connecting']['p95']
tls_p95 = data['metrics']['http_req_tls_handshaking']['p95']

if waiting_p95 > 300 and connecting_p95 < 50 and tls_p95 < 50:
    print("⚠️  问题定位:服务器处理耗时过高,请检查应用日志、DB 慢查询、线程阻塞")
elif connecting_p95 > 100:
    print("⚠️  问题定位:DNS 解析或网络连接异常,请检查 CoreDNS、Service Mesh 配置")

这套自动化诊断脚本已集成进 CI 流水线,压测结束 30 秒内即可邮件推送根因分析。

4.3 故障注入与混沌工程联动

真正的性能验证,不是“系统能不能扛住”,而是“系统在异常下是否优雅降级”。我们把 k6 和 Chaos Mesh 深度集成:

  • 在压测过程中,随机注入 3% 的 Pod Kill(模拟节点宕机);
  • 对数据库连接池注入 200ms 网络延迟(模拟 DB 响应变慢);
  • 对 Redis 实例注入 5% 的 key 丢失(模拟缓存穿透)。

具体做法:在 k6 脚本中加入 exec() 调用 Chaos Mesh CLI:

// 在 setup() 中初始化混沌实验
export function setup() {
  // 启动网络延迟实验
  exec('kubectl apply -f chaos/network-delay.yaml');
  sleep(5); // 等待实验生效
}

// 在 teardown() 中清理
export function teardown(data) {
  exec('kubectl delete -f chaos/network-delay.yaml');
}

然后观察 k6 指标变化:

  • http_req_failed 从 0% 突增至 15%,说明无熔断机制;
  • http_req_duration P95 从 400ms 涨至 2500ms 且不回落,说明无超时控制;
  • vus_max 断崖下跌,说明服务发现或负载均衡失效。

这种“压测+混沌”双引擎模式,让我们在过去半年中提前发现 7 个线上隐患,包括:支付回调重试风暴、优惠券库存预扣未释放、风控规则引擎 CPU 尖刺等。

4.4 性能基线管理:用 Git 管理“性能契约”

我们把每次压测的 JSON 报告、脚本版本、环境配置全部提交到 Git 仓库,目录结构如下:

/performance-baselines/
├── payment-gateway/
│   ├── v1.2.0/                 # 服务版本号
│   │   ├── script.js           # 脚本
│   │   ├── config.json         # 环境参数
│   │   ├── report_20240501.json # 基准报告
│   │   └── report_20240515.json # 回归报告
│   └── baseline.json           # 当前基线(自动更新)
└── README.md

关键动作:

  • 每次服务发布前,CI 自动运行 k6 run --out json=report.json ./scripts/payment.js
  • Python 脚本比对 report.json baseline.json 中的关键指标(P95 延迟、错误率、RPS);
  • 若 P95 延迟增长 >10% 或错误率 >0.5%,CI 直接失败,并附对比链接;
  • 若通过,则自动更新 baseline.json 并提交 PR。

这个机制倒逼团队形成共识: 性能不是“最好别崩”,而是“必须守住的契约” 。过去三个月,支付网关的 P95 延迟波动范围被严格控制在 ±3.2% 内,这是靠人工盯盘永远达不到的精度。

5. 那些没人告诉你的坑:来自 137 次压测的真实教训

纸上谈兵终觉浅。下面是我和团队踩过的、文档里几乎不提、但足以让一次压测完全失效的 5 个硬核坑。每一个都附带现场日志、根因分析和永久解决方案。

5.1 坑: http_req_sending 持续飙升,但服务器日志无请求记录

现象 :压测进行到第 8 分钟,k6 报告显示 http_req_sending P95 从 5ms 暴涨至 1200ms,但 Nginx access log 和应用日志完全空白,仿佛请求从未到达。

排查链路

  1. k6 run --debug 开启调试日志,发现大量 failed to write request body: write tcp 10.10.10.5:54321->10.10.10.100:443: i/o timeout
  2. tcpdump 抓包确认:k6 客户端发出 SYN,服务端回 SYN-ACK,但客户端不再发 ACK(三次握手卡在第二步);
  3. 检查 k6 runner 节点 sysctl net.ipv4.ip_local_port_range ,发现范围是 32768 60999 (仅 28232 个端口);
  4. 计算:10000 VU × 平均每个 VU 3 个并发连接 = 30000 连接需求 > 28232 端口上限。

根因 :Linux 默认 ephemeral port 范围不足,k6 在高并发下端口耗尽,新连接无法建立。

解决方案

# 在 k6 runner 节点执行
echo 'net.ipv4.ip_local_port_range = 1024 65535' >> /etc/sysctl.conf
sysctl -p
# 同时在 k6 脚本中启用连接复用
const params = {
  headers: { 'Connection': 'keep-alive' },
  tags: { name: 'keepalive-test' }
};
http.get('https://api.example.com', params);

经验:k6 默认不启用 HTTP Keep-Alive。在高并发场景,必须显式设置 Connection: keep-alive ,并将 maxIdleConnsPerHost 调大(k6 v0.45+ 支持 --http-max-idle-conns-per-host 参数)。

5.2 坑: check() 断言通过,但业务实际失败

现象 :压测报告 http_req_failed: 0.00% ,所有 check() 返回 true,但业务方反馈“大量订单状态为‘创建中’,未进入支付环节”。

排查链路

  1. 抓取 k6 发出的原始请求,发现 Content-Type: application/x-www-form-urlencoded ,但服务端期望 application/json
  2. 查看脚本,发现 http.post(url, 'key=value') 被误用,正确应为 http.post(url, JSON.stringify({key: 'value'}), {headers: {'Content-Type': 'application/json'}})
  3. 更致命的是:服务端对 Content-Type 错误的请求返回 200,但内部静默失败(未校验 header)。

根因 :k6 的 check() 只校验响应状态码和内容,不校验业务语义。而服务端的容错设计(返回 200 却不执行业务)掩盖了真实错误。

解决方案

  • 强制所有 POST/PUT 请求显式声明 Content-Type
  • check() 中增加业务字段校验,而非只看 status:
    check(res, {
      'status is success': (r) => r.json('status') === 'success',
      'order_id is string': (r) => typeof r.json('order_id') === 'string'
    });
    
  • 推动服务端修改: Content-Type 错误时必须返回 400,禁止静默处理。

5.3 坑: vus_max 始终为 1,无论 --vus 设多少

现象 k6 run --vus 1000 script.js ,但报告中 vus_max: 1 ,且只有 1 个请求发出。

排查链路

  1. k6 run --verbose script.js ,发现日志末尾有 FATAL: too many open files
  2. ulimit -n 查看当前限制:1024;
  3. lsof -p $(pgrep k6) | wc -l 统计打开文件数:1025。

根因 :k6 每个 VU 需要至少 1 个文件描述符(用于 socket、日志、临时文件),1000 VU 需要 >1000 fd。Linux 默认 ulimit -n 为 1024,k6 启动时即被系统拒绝。

解决方案

  • 启动 k6 前执行 ulimit -n 65536
  • 在 Kubernetes 中,通过 securityContext 设置:
    securityContext:
      ulimits:
      - name: nofile
        soft: 65536
        hard: 65536
    

5.4 坑: http_req_duration P95 稳定,但 P99 突然拉长 10 倍

现象 :压测平稳运行 20 分钟,P95 延迟维持在 320ms,但第 21 分钟 P99 从 850ms 暴涨至 8200ms,持续 3 分钟后恢复。

排查链路

  1. 查看 Prometheus,发现 process_cpu_seconds_total 在同一时间点出现尖刺;
  2. 登录 k6 runner 节点, top 显示 k6 进程 CPU 100%,但 htop 显示仅 1 个线程满载;
  3. perf record -p $(pgrep k6) -g -- sleep 30 采样火焰图,热点在 v8::internal::Scavenger::Scavenge (V8 GC);
  4. 确认:脚本中存在 const hugeData = new Array(1000000).fill(0) ,每次迭代创建百万级数组。

根因 :V8 引擎的 Scavenge GC 在新生代内存不足时触发,单次耗时可达数百毫秒,导致该 VU 的所有请求被阻塞。

解决方案

  • 禁止在 default() 中创建大对象,改用对象池复用;
  • 对于必须的大数据,移至 setup() 阶段预分配,并在 default() 中只做引用传递;
  • 启用 V8 堆快照分析: k6 run --v8-flags="--heap-prof" script.js

5.5 坑: --rps 控制失效,实际 RPS 是设定值的 3 倍

现象 k6 run --rps 100 script.js ,但 Prometheus 显示实际 RPS 为 290~310。

排查链路

  1. k6 run --debug 日志显示: INFO[0001] Target RPS: 100, actual RPS: 305
  2. 查看脚本,发现 default() 函数内包含 3 个 http.get() 调用;
  3. k6 的 --rps 是指 每秒执行 default() 函数的次数 ,而非每秒请求数。每个 default() 迭代产生 3 个请求,故实际 RPS = --rps × 每次迭代请求数。

根因 :对 --rps 的语义理解错误。k6 的 RPS 控制粒度是“VU 迭代频率”,不是“HTTP 请求频率”。

解决方案

  • 若需精确控制 HTTP RPS,改用 --stage 配置:
    export const options = {
      stages: [
        { duration: '10s', target: 100 }, // 100 VU
        { duration: '1m', target: 100 },  // 保持 100 VU
      ],
      // 然后在脚本中用 sleep() 控制单次迭代耗时
    };
    
  • 或者,在脚本中计算: sleep(1000 / (targetRPS / requestsPerIteration))

这些坑,每一个都曾让我们在凌晨三点的会议室里集体沉默。但正是它们,把 k6 从一个“能用的工具”,变成了我们性能保障体系里最值得信赖的基石。

我在实际压测中发现,最有效的学习方式不是背参数,而是故意制造一个故障,然后用 k6 的调试能力一层层剥开。比如,把 sleep(1) 改成 sleep(10) ,观察 vus_max 如何断崖下跌;或者把 Content-Type 写错,看 check() 如何“假装成功”。只有亲手把系统搞崩过,才能真正理解

更多推荐