K6云原生性能测试:JavaScript脚本+Go运行时的现代压测实践
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等模块,也不支持 CommonJSrequire()。所有 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_durationP95 从 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 和应用日志完全空白,仿佛请求从未到达。
排查链路 :
-
k6 run --debug开启调试日志,发现大量failed to write request body: write tcp 10.10.10.5:54321->10.10.10.100:443: i/o timeout; -
tcpdump抓包确认:k6 客户端发出 SYN,服务端回 SYN-ACK,但客户端不再发 ACK(三次握手卡在第二步); -
检查 k6 runner 节点
sysctl net.ipv4.ip_local_port_range,发现范围是32768 60999(仅 28232 个端口); - 计算: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,但业务方反馈“大量订单状态为‘创建中’,未进入支付环节”。
排查链路 :
-
抓取 k6 发出的原始请求,发现
Content-Type: application/x-www-form-urlencoded,但服务端期望application/json; -
查看脚本,发现
http.post(url, 'key=value')被误用,正确应为http.post(url, JSON.stringify({key: 'value'}), {headers: {'Content-Type': 'application/json'}}); -
更致命的是:服务端对
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 个请求发出。
排查链路 :
-
k6 run --verbose script.js,发现日志末尾有FATAL: too many open files; -
ulimit -n查看当前限制:1024; -
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 分钟后恢复。
排查链路 :
-
查看 Prometheus,发现
process_cpu_seconds_total在同一时间点出现尖刺; -
登录 k6 runner 节点,
top显示k6进程 CPU 100%,但htop显示仅 1 个线程满载; -
perf record -p $(pgrep k6) -g -- sleep 30采样火焰图,热点在v8::internal::Scavenger::Scavenge(V8 GC); -
确认:脚本中存在
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。
排查链路 :
-
k6 run --debug日志显示:INFO[0001] Target RPS: 100, actual RPS: 305; -
查看脚本,发现
default()函数内包含 3 个http.get()调用; -
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()
如何“假装成功”。只有亲手把系统搞崩过,才能真正理解
更多推荐
所有评论(0)