Grafana k6 测试即代码与性能门禁手册
延迟变好了,系统却可能已经失守
一次订单基线把目标设为 300 次交易/秒。测试后 P95 从 420 ms 降到 260 ms,看上去像一次漂亮优化,服务端入口却只收到 180 次交易/秒。k6 摘要里的 dropped_iterations 持续增长:预分配 VU 不够,生成器没有按计划启动迭代。延迟改善来自少发了流量,不能用来签容量结论。
Grafana k6 把用户路径、负载模型、业务检查和性能阈值写进 JavaScript 或 TypeScript。VU 是执行用户代码的并发执行单元,Iteration 是场景函数的一次完整运行,Scenario 选择 Executor 来决定“保持多少并发”或“按什么速率启动迭代”。Check 记录业务真假,Threshold 才决定整个进程是否失败。
第一次运行前先取得目标环境和负载窗口授权,准备显式 BASE_URL、测试账号、数据清理接口、流量上限、停止联系人和服务端观测面板。默认地址只用回环地址或明确的测试域名;支付、短信、邮件和真实计费出口切到受控替身或白名单沙箱。
k6 不是 Node.js。它采用自己的运行时、模块和生命周期,Node.js 内置模块与 npm 包不能假定可用。代码依次经过 init context、可选的 setup()、每个 VU 的场景函数以及可选的 teardown();网络请求应发生在 setup() 或 VU 代码中,大数据文件在 init 阶段读取并通过 SharedArray 共享,避免每个 VU 复制一份。
执行前确认:
k6 version
k6 inspect -e BASE_URL=http://127.0.0.1:18080 tests/performance/smoke.js并发 VU、请求速率和在线用户不是同一个指标。闭合模型中,每个 VU 完成本轮后才开始下一轮,服务变慢时到达率也会下降;开放模型按目标速率启动迭代,更适合观察固定业务流量下的排队和容量。选错模型可能让系统越慢、施加的流量反而越小。
安装页提供 Linux 软件仓库、Homebrew、Windows MSI、Winget、Docker 和独立二进制入口。Windows 的 Chocolatey 包被标注为非官方;Winget 的 manifest 由社区维护。团队应记录安装来源、k6 version 输出和二进制校验值,CI 镜像固定版本或摘要。
# Debian / Ubuntu
curl -fsSL https://dl.k6.io/key.gpg | \
sudo gpg --dearmor -o /usr/share/keyrings/k6-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | \
sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update
sudo apt-get install k6
# Fedora / CentOS
sudo dnf install https://dl.k6.io/rpm/repo.rpm
sudo dnf install k6
# macOS
brew install k6
# Windows
winget install k6 --source winget容器入口适合 CI 或不希望污染开发机的场景:
K6_IMAGE='grafana/k6:2.1.0'
docker pull "$K6_IMAGE"
docker image inspect "$K6_IMAGE" --format '{{json .RepoDigests}}'
docker run --rm -i \
-e BASE_URL=http://host.docker.internal:18080 \
-v "$PWD/tests/performance:/scripts:ro" \
"$K6_IMAGE" \
run /scripts/smoke.js2.1.0 对应已经核验的稳定发布;流水线还应记录 RepoDigests 返回的架构相关 digest,后续运行按已批准 digest 固定镜像。只记录可变标签无法证明两次执行使用了同一镜像。
host.docker.internal 在 Docker Desktop 可用;Linux 需要增加 --add-host host.docker.internal:host-gateway,或者把 Mock 与 k6 放进同一用户定义网络并使用服务名。不要在可重复流水线中使用 latest 或 master-with-browser。浏览器测试选择带 Chromium 的固定版本镜像,但浏览器 VU 的资源成本远高于协议请求,不能照搬 HTTP 测试并发。扩展使用 xk6 或扩展构建服务生成自定义二进制;自定义二进制必须记录核心版本、扩展版本、校验值、构建方式和 SBOM。
安装后运行 k6 version,升级时先在低流量基线上做双版本对照。卸载 CLI 不会删除脚本、输出文件、缓存或 Grafana Cloud 项目;云 Token 和 Secret 必须单独撤销。
用 Scenario 表达负载,而不是堆 VU
最小脚本可以直接配置 vus 和 duration,但正式测试应使用 scenarios 表达阶段与执行器。constant-vus、ramping-vus 属于闭合模型;constant-arrival-rate、ramping-arrival-rate 属于开放模型。到达率 VU 分配说明要求预先准备足够的 preAllocatedVUs。设置 maxVUs 后可以在运行中继续分配,但初始化 VU 本身会消耗 CPU 和内存,正式基线优先在试跑后一次性预分配。可用 VU 不足时会出现 dropped iterations,这不是服务端成功扛住流量,而是生成器没有产生计划中的负载。
export const options = {
scenarios: {
baseline: {
executor: 'ramping-arrival-rate',
startRate: 10,
timeUnit: '1s',
preAllocatedVUs: 20,
maxVUs: 100,
stages: [
{ target: 50, duration: '1m' },
{ target: 50, duration: '5m' },
{ target: 0, duration: '30s' },
],
gracefulStop: '30s',
},
},
};选择速率要从业务量推导:按峰值请求、关键交易比例和每次迭代请求数计算,而不是把“500 个在线用户”直接写成 500 VU。sleep() 表达思考时间,主要影响闭合模型;开放到达率由执行器调度,不靠 sleep() 控制 RPS。
Check 保证正确,Threshold 决定通过
下面的脚本把目标、业务验证和性能门禁分开。check() 失败只增加 checks 指标,不会自动使命令失败,所以必须给 checks 配 Threshold;任一 Threshold 失败时 k6 才会返回非零退出码:
import http from 'k6/http';
import { check, sleep } from 'k6';
const baseUrl = __ENV.BASE_URL;
if (!baseUrl) {
throw new Error('缺少 BASE_URL,拒绝使用隐式环境');
}
export const options = {
vus: 5,
duration: '30s',
thresholds: {
checks: ['rate>0.99'],
http_req_failed: ['rate<0.01'],
http_req_duration: ['p(95)<500', 'p(99)<1000'],
},
};
export default function () {
const response = http.get(`${baseUrl}/health`, {
tags: { endpoint: 'health' },
timeout: '3s',
});
check(response, {
'HTTP 状态为 200': (r) => r.status === 200,
'业务状态正常': (r) => r.json('status') === 'UP',
});
sleep(1);
}Threshold 应来自 SLO 与测试目的。p(95)<500 表示 95% 的样本低于 500 ms,不代表剩余 5% 可以无限慢;还要看 P99、超时和错误分布。http_req_failed 依据预期响应定义,业务返回 HTTP 200 但状态失败时,需要 Check 或自定义 Rate 补充。按 endpoint、scenario 等低基数 Tag 设置分组阈值,避免全局快接口掩盖关键慢接口。
需要快速止损时使用长格式:
thresholds: {
http_req_failed: [{
threshold: 'rate<0.05',
abortOnFail: true,
delayAbortEval: '30s',
}],
}delayAbortEval 避免预热期样本太少造成瞬时误判,但它不是全部停止策略。数据库不可用、数据异常或生产误连时,外部作业取消必须立即生效。
指标、百分位与基数
k6 内置 HTTP、执行、网络和 VU 指标,也可以用 Counter、Rate、Trend 和 Gauge 定义业务指标。指标名与 Tag 会形成时间序列;把用户 ID、订单 ID、完整 URL 或错误正文作为 Tag 会造成高基数、内存增长和结果后端费用上升。动态路径使用固定 name Tag 聚合,例如把 /orders/123 与 /orders/456 都命名为 /orders/:id。
平均值只适合描述中心,不适合判定尾延迟。报告至少同时观察吞吐、P50/P90/P95/P99、错误率、dropped iterations、iteration duration 和 VU 使用量。样本量不足时 P99 波动很大;PR Smoke 不应以极短测试的 P99 作为唯一门禁。
参数化与认证不能破坏用户模型
账号数据在 init context 用 open() 读取,并用 SharedArray 解析一次。文件只提交字段示例,真实账号由运行器挂载;脚本启动时校验数据量至少覆盖最大活跃 VU。数据耗尽时应失败或停止对应 VU,不能悄悄从头循环,让同一账号并发登录、争抢购物车或触发会话互踢。
import { SharedArray } from 'k6/data';
import exec from 'k6/execution';
const users = new SharedArray('users', () =>
JSON.parse(open(__ENV.USERS_FILE || '../data/users.example.json'))
);
export function userForCurrentVu() {
const vuId = exec.vu.idInTest;
const user = users[vuId - 1];
if (!user) throw new Error(`测试数据不足,缺少全局 VU ${vuId} 的账号`);
return user;
}exec.vu.idInTest 在本地、Cloud 和 segmented/distributed 执行中是整次测试全局唯一的 VU ID;旧的 __VU 在 Cloud 中按负载生成器计数,不适合做全局唯一映射。按“每次迭代只消费一次”的数据则用 exec.scenario.iterationInTest,并注意它只在单个 Scenario 内唯一。认证若是“一用户一会话”,每个 VU 在自己的首次迭代登录并缓存 Token;若 Token 可以安全共享,才由 setup() 获取后传给 VU。登录失败、Token 为空和业务鉴权失败都要进入 Check 或自定义 Rate,Token、Cookie、邮箱和用户 ID 不进入 Tag、Check 名称或错误日志。
生成器资源与停止预算
k6 的 Go 引擎通常比浏览器型工具轻量,但每个脚本的响应解析、JS 对象、TLS、指标 Tag 和输出插件成本不同。压测机必须采集 CPU、内存、网络、文件句柄、DNS/连接错误和 dropped_iterations。生成器 CPU 或网络饱和、maxVUs 用尽、输出阻塞时,实际流量会偏离计划。
在正式测试前用简单目标或 Mock 做生成器基线,再用业务脚本比较开销。禁用响应正文虽然能降低内存,但脚本若依赖正文做关联与断言就不能盲目开启;应保留业务正确性所需数据,并通过 discardResponseBodies 与单请求 responseType 精细控制。
用正反实验证明 Threshold 会阻断
将前面的脚本保存为 tests/performance/smoke.js。为了不误打共享环境,可以把下面的 Mock 保存到本次唯一运行目录中的 mock_server.py。它只绑定回环地址,只响应 /health,不会暴露所在目录:
from http.server import BaseHTTPRequestHandler, HTTPServer
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path != "/health":
self.send_error(404)
return
body = b'{"status":"UP","marker":"healthy"}'
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, message, *args):
print(message % args)
HTTPServer(("127.0.0.1", 18080), Handler).serve_forever()在一个终端运行 python mock_server.py,另一个终端执行 k6。每轮创建新的证据目录,避免覆盖旧结果:
run_id="$(date -u +%Y%m%dT%H%M%SZ)-$$"
run_dir="artifacts/k6/$run_id"
mkdir -p "$run_dir"
k6 inspect -e BASE_URL=http://127.0.0.1:18080 tests/performance/smoke.js
k6 run \
-e BASE_URL=http://127.0.0.1:18080 \
--out "json=$run_dir/points.json" \
tests/performance/smoke.js
exit_code=$?
printf 'exit=%s evidence=%s\n' "$exit_code" "$run_dir"
test "$exit_code" -eq 0正向响应下,预期 checks、http_req_failed 和 http_req_duration 三组 Threshold 都显示通过,退出码为 0。还应确认 iterations 大于 0、dropped_iterations 为 0,并从 Mock 访问日志核对实际请求数。JSON 文件包含逐点数据,可抽查延迟样本:
jq 'select(.type == "Point" and .metric == "http_req_duration")' \
"$run_dir/points.json" | head反向实验不改目标地址,只把 Mock 状态改为 DOWN,或在独立副本中把期望值改成不可能出现的 __must_fail__。预期业务 Check 失败,checks: ['rate>0.99'] Threshold 显示失败,k6 返回非零退出码。CI 必须捕获并保留这个退出码;把命令接在会吞掉状态的管道后面,会制造绿色构建。
实验结果只清理刚才打印的运行目录。脚本先解析绝对路径并确认它严格位于工作区 artifacts/k6 下;校验失败就保留证据并退出,不对动态上级目录执行递归删除。写业务数据时使用运行 ID 作为精确前缀,调用幂等清理接口后再次查询,期望残留数为 0;teardown() 因强制终止而未运行时,外部清理作业仍可重试。
如果目标必须经过代理、证书或内网 DNS,先用 curl 从同一执行环境验证,再运行 1 VU、1 Iteration。不要通过跳过 TLS 验证把证书问题带入正式基线。
测试即代码的价值来自可维护结构,而不是把所有流程写进一个文件:
tests/performance/k6/
scenarios/
smoke.js
baseline.js
stress.js
flows/
login.js
checkout.js
config/
thresholds.js
environments.example.js
data/
users.example.json
lib/
auth.js
cleanup.js
README.md
artifacts/
.gitkeep公共模块保存请求封装、低基数 Tag 和业务检查;Scenario 保存负载模型;环境只通过显式参数选择。不要让共享阈值模块变成所有系统的最低公分母,关键服务仍需维护自己的 SLO。setup() 可准备短生命周期数据,teardown() 可清理,但作业被强制终止时 teardown() 未必完成,因此还需要可重复执行的外部清理任务。
CI 分层运行。PR 执行 30 秒到数分钟的 Smoke,验证脚本、接口和核心阈值;预发布环境执行基线和目标负载;压力、浸泡和断点测试放在受审批的专用环境与时间窗。流水线保存控制台摘要、原始或实时输出、脚本版本、目标构建、生成器规格、服务端观测链接和清理结果。
最小 CI 命令就是:
k6 run -e BASE_URL="$PERF_BASE_URL" tests/performance/k6/scenarios/smoke.jsThreshold 失败会让 k6 返回失败状态,流水线可直接阻断。BASE_URL 可以是普通变量,Token 应来自 Secret Source 或 CI Secret;命令回显、错误正文和自定义摘要都要避免泄漏。
# 创建脚本骨架
k6 new tests/performance/k6/scenarios/smoke.js
# 静态查看最终选项与依赖
k6 inspect -e BASE_URL=https://test.example.com tests/performance/k6/scenarios/smoke.js
# 用显式参数执行本地测试
k6 run -e BASE_URL=https://test.example.com smoke.js
# 输出逐点 JSON
k6 run -e BASE_URL=https://test.example.com --out json=artifacts/results.json smoke.js
# 创建可分发归档;默认不会自动包含系统环境变量
k6 archive -e BASE_URL=https://test.example.com smoke.js -O artifacts/smoke.tar
# 在 Grafana Cloud k6 执行,需要已批准账号和 Token
k6 cloud run -e BASE_URL=https://test.example.com smoke.jsinspect、archive 和 cloud run 不会因为 shell 中存在同名变量就自动把它交给脚本;-e BASE_URL=... 是一次显式的数据边界。示例地址必须替换为已授权目标。Cloud 侧使用 Secret 时,应在受控项目中配置并限制读取权限,而不是把 Token 跟随归档上传。
本地 Web Dashboard、Prometheus Remote Write 或 OpenTelemetry 输出适合实时观察,但输出端故障可能阻塞、丢弃或放大指标成本。先用小负载证明输出链路,再比较开启与关闭输出时的生成器开销。自定义 handleSummary() 可以生成 JSON 或 HTML 摘要,但它基于结束时聚合结果,不替代逐点原始数据与服务端观测。
Kubernetes 分布式执行使用 k6 Operator 的 TestRun。集群执行不是“对当前上下文直接 apply”:先把目标 context 和 namespace 写成必须显式提供的输入,核对当前身份,并确认 Operator CRD 已存在。下面先生成一个自包含的低负载脚本,再由它创建 ConfigMap;BASE_URL 仍然通过 Runner 环境显式注入:
: "${KUBE_CONTEXT:?必须显式设置 KUBE_CONTEXT}"
: "${K6_NAMESPACE:?必须显式设置 K6_NAMESPACE}"
: "${K6_BASE_URL:?必须显式设置 K6_BASE_URL}"
export K6_BASE_URL
test "$(kubectl config current-context)" = "$KUBE_CONTEXT" || {
printf '当前 context 不是目标 context,拒绝继续\n' >&2
exit 1
}
kubectl --context "$KUBE_CONTEXT" auth whoami
kubectl --context "$KUBE_CONTEXT" get namespace "$K6_NAMESPACE"
kubectl --context "$KUBE_CONTEXT" get crd testruns.k6.io
mkdir -p artifacts/k6-operator
cat > artifacts/k6-operator/baseline.js <<'JS'
import http from 'k6/http';
import { check, sleep } from 'k6';
const baseUrl = __ENV.BASE_URL;
if (!baseUrl) throw new Error('缺少 BASE_URL');
export const options = {
vus: 4,
duration: '30s',
thresholds: {
checks: ['rate>0.99'],
http_req_failed: ['rate<0.01'],
},
};
export default function () {
const response = http.get(`${baseUrl}/health`, { timeout: '3s' });
check(response, {
'HTTP 状态为 200': (r) => r.status === 200,
'业务状态正常': (r) => r.json('status') === 'UP',
});
sleep(1);
}
JS
kubectl --context "$KUBE_CONTEXT" -n "$K6_NAMESPACE" create configmap k6-checkout \
--from-file=baseline.js=artifacts/k6-operator/baseline.js \
--dry-run=client -o yaml > artifacts/k6-operator/configmap.yamlcat > artifacts/k6-operator/testrun.yaml <<'YAML'
apiVersion: k6.io/v1alpha1
kind: TestRun
metadata:
name: checkout-baseline
spec:
parallelism: 2
script:
configMap:
name: k6-checkout
file: baseline.js
runner:
env:
- name: BASE_URL
value: "https://test.example.com"
YAML先把清单中的 BASE_URL 替换为已批准的 K6_BASE_URL。不要用包含 /、& 等字符的变量直接拼接 sed;使用 YAML 处理器完成结构化赋值:
command -v yq >/dev/null || {
printf '缺少 yq,拒绝用字符串替换修改 YAML\n' >&2
exit 1
}
yq -i '.spec.runner.env[] |=
(select(.name == "BASE_URL").value = strenv(K6_BASE_URL))' \
artifacts/k6-operator/testrun.yaml
kubectl --context "$KUBE_CONTEXT" -n "$K6_NAMESPACE" apply \
-f artifacts/k6-operator/configmap.yaml
kubectl --context "$KUBE_CONTEXT" -n "$K6_NAMESPACE" apply \
-f artifacts/k6-operator/testrun.yaml
kubectl --context "$KUBE_CONTEXT" -n "$K6_NAMESPACE" get testrun checkout-baseline -w
# 另一个终端观察 Runner;标签以实际 Operator 版本创建结果为准
kubectl --context "$KUBE_CONTEXT" -n "$K6_NAMESPACE" get pods \
-l k6_cr=checkout-baseline -o wide
kubectl --context "$KUBE_CONTEXT" -n "$K6_NAMESPACE" logs \
-l k6_cr=checkout-baseline --all-containers --prefixget -w 只是观察,不是停止入口。错误率、目标地址或服务端指标触发停止条件时,先删除 TestRun,等待 Runner Pod 消失,再删除只属于本次运行的 ConfigMap;最后检查没有残留资源。若 ConfigMap 被其他作业共享,就不能随本次运行删除,必须改用带运行 ID 的独立名称。
kubectl --context "$KUBE_CONTEXT" -n "$K6_NAMESPACE" delete \
testrun checkout-baseline --wait=true
kubectl --context "$KUBE_CONTEXT" -n "$K6_NAMESPACE" wait \
--for=delete pod -l k6_cr=checkout-baseline --timeout=120s || true
kubectl --context "$KUBE_CONTEXT" -n "$K6_NAMESPACE" delete \
configmap k6-checkout --wait=true
kubectl --context "$KUBE_CONTEXT" -n "$K6_NAMESPACE" get \
testrun,pod,configmap -l k6_cr=checkout-baseline最后一条查询可能无法显示没有同一标签的 ConfigMap,所以还要执行 kubectl --context "$KUBE_CONTEXT" -n "$K6_NAMESPACE" get configmap k6-checkout,预期返回 NotFound。|| true 只用于“等待一个可能已被控制器删除的 Pod”;创建、执行和删除 TestRun 的失败状态不能吞掉。脚本、扩展、Secret、网络策略和结果输出都要在集群内可用。TestRun 配置说明规定 parallelism 是 Runner 数量,每个 Runner 获得相等的 execution segment:脚本配置 4 VU、parallelism: 2 时,目标总量仍是 4 VU,每个 Runner 约 2 VU。它不会替团队解决账号分片、共享 NAT、节点资源和结果后端瓶颈,所以上线前仍要按 Runner 与全局两层核对实际 VU、迭代、到达率和 dropped iterations。上述集群命令需要在已安装 Operator 且获得授权的环境实际执行后,才能登记为通过。
Check 大量失败但 CI 仍是绿色。 check() 本身不会使 k6 失败。为 checks 或自定义 Rate 配置 Threshold,故意触发失败并核对退出码。
设定 1,000 RPS,实际吞吐达不到。 查看 dropped_iterations、vus、vus_max、生成器 CPU 和网络。若 maxVUs 用尽或生成器饱和,先增加预分配、优化脚本或拆分生成器;若生成器健康而服务端响应变慢,再分析目标系统饱和点。
本地能运行,CI 报模块找不到。 检查是否引用 Node.js 内置模块、依赖未打包的本地文件或从外网动态加载远程模块。把依赖固定在仓库或可追溯构建中,不依赖运行时公共 CDN。
错误率很低,业务数据却不正确。 http_req_failed 主要反映预期 HTTP 响应。为业务码、字段与副作用增加 Check 或自定义 Rate,并在目标系统核对创建与清理的数据。
P95 突然改善,同时吞吐下降。 检查是否出现 dropped iterations、请求被限流、Scenario 提前结束或一部分接口没有执行。延迟改善只有在实际到达率、成功吞吐和场景比例都满足计划时才有意义。
容器里访问 localhost 失败。 容器内 localhost 指向容器自身。使用容器网络服务名、明确的宿主机入口或受控网关,并从容器内部先执行 DNS 与连接验证。
Cloud 执行找不到环境变量。 k6 默认不把系统环境变量随 archive、cloud run、inspect 自动上传,这是安全设计。使用经批准的 Cloud 环境变量或 Secret 管理,不要把凭证硬编码进脚本。
普通环境变量可通过 -e 暴露给 __ENV,但环境变量可能出现在进程、调试日志和 CI 配置中。k6 Secret Source 通过 k6/secrets API 读取并自动在日志传播中脱敏,适合 Token 等机密;本地内置来源包括 file、mock 和 url。Secret 文件本身仍需限制权限并排除 Git。
import secrets from 'k6/secrets';
import http from 'k6/http';
export default async function () {
const token = await secrets.get('api_token');
http.get('https://test.example.com/orders', {
headers: { Authorization: `Bearer ${token}` },
});
}k6 run --secret-source=file=secrets.local secure-test.js不要把 Token、Cookie、邮箱、用户 ID、订单号和完整动态 URL 放进 Tag、Check 名称、错误文本或结果后端。Grafana Cloud 执行意味着脚本、指标、标签和可能的测试数据离开本地信任边界;启用前确认区域、保留、访问角色、审计、合同和删除机制。Cloud Token 使用团队服务账号与最小权限,禁止共享个人 Token。
代理与证书必须在负载生成器和目标之间统一验证。企业根证书应进入受控信任库;跳过 TLS 校验只能用于隔离实验,不能成为团队模板。压力测试本身具有拒绝服务能力,运行账号和 Kubernetes ServiceAccount 只允许创建指定命名空间中的 TestRun,不能拥有集群管理员权限。
k6 的代码可读性让开发团队更容易共同维护,但脚本通过 Review 不等于测试可信。业务 owner 负责用户路径和成功条件,性能 owner 负责负载模型、阈值和生成器基线,平台 owner 负责 Runner、Operator、Cloud、输出和 Secret,目标系统 owner 负责容量解释与停止决策。
团队模板应固定脚本结构、Tag 规范、环境选择、Secret 注入、输出和证据包。Threshold 修改必须像生产 SLO 修改一样说明理由,不能由脚本作者为通过构建随意放宽。基线报告记录 k6 版本、扩展、脚本 SHA、生成器规格、目标版本、数据规模、预热方式和依赖状态,才能跨版本比较。
升级先跑单用户正确性,再跑固定低负载对照,比较请求数、分位数、错误、生成器资源和输出格式。使用扩展时维护自定义二进制 SBOM 与回退版本。Cloud 与自建 Operator 都应有退出方案:脚本保持可本地执行,结果数据可导出,Token 可撤销,避免测试能力被单一托管服务锁死。
开放与闭合模型会得出相反结论
闭合模型中服务变慢会拖长 Iteration,系统实际接收的流量随之下降;曲线可能显示错误不多,却是因为没有维持业务到达率。开放模型能保持目标速率,但需要足够 VU 和生成器资源,否则 dropped iterations 增长。选择标准是业务流量是否会等待上一次操作完成:固定数量用户反复操作适合闭合模型,外部请求按速率到达更接近开放模型。
报告必须同时写计划速率、实际成功吞吐、dropped iterations 和 VU 峰值。只展示 http_reqs 而不展示未生成的迭代,会把施压失败误判为服务稳定。
生成器瓶颈会伪装成服务端限流
JS 解析大响应、高基数 Tag、浏览器 VU、频繁日志和多个实时输出都会消耗生成器。正式测试先用协议级脚本确定引擎基线,再逐项启用业务解析与输出;生成器 CPU、内存和网络达到红线时停止扩压。对同一 Mock 目标增加生成器数量,若吞吐不能近似线性增加,应先排查共享出口、DNS、NAT、结果后端或脚本热点。
discardResponseBodies 能减少内存,却可能让业务断言失去依据。架构取舍不是“全部保存”或“全部丢弃”,而是只保留关联和正确性验证需要的响应。
Threshold 不是一组永远正确的数字
P95/P99 的稳定性依赖样本量,错误率也会因重试和预期响应定义变化。PR Smoke 主要验证无明显回归和功能正确;目标负载基线才验证容量 SLO;压力测试寻找拐点,不应因越过目标而被普通门禁提前终止。每种测试类型需要独立 Threshold 和报告语义。
使用 abortOnFail 可以降低事故半径,但过短 delayAbortEval 会在预热期误停,过长又会让故障持续。将客户端 Threshold 与服务端外部熔断条件结合:核心错误、数据一致性、依赖雪崩或误连生产应由平台立即取消作业,不等待测试结束计算百分位。
分布式执行先解决数据和总量,再谈副本数
k6 Operator 负责在 Kubernetes 中编排,不会自动解决唯一账号、数据分片、跨 Pod 初始化、时钟、出口带宽和结果归并。并行 Pod 共用同一 CSV 可能重复登录或争用订单;共享 NAT 可能先耗尽端口;所有 Pod 同时预热可能制造不真实尖峰。
分布式上线前用小流量核对每实例迭代数、总到达率、数据分片和结果标签。扩到目标规模后观察节点、Pod、出口网关与输出后端。需要跨地域时应明确是在模拟用户地域,还是单纯扩充生成器;两者的网络时延含义完全不同。
Cloud 解决执行资源,不替代治理责任
Grafana Cloud k6 可以托管负载和结果,但账号权限、数据出境、地域、配额、费用和压测授权仍由团队负责。脚本中的 URL、请求参数、响应派生指标和 Tag 都可能构成敏感信息。采用 Cloud 前建立字段 allowlist、短期 Token、预算告警、项目隔离和删除策略。
云端结果与本地结果还可能因负载区域、网络路径和 DNS 不同而不可直接比较。基线必须记录执行位置;架构决策应比较同一网络条件下的重复测试,而不是把云端一次结果与开发机一次结果拼成趋势。
压测目标、环境、流量、时间窗、停止条件、owner 与数据清理已经批准。k6 核心、Docker 镜像和扩展版本已锁定,未依赖浮动标签。明确选择开放或闭合模型,并记录速率、VU、阶段与业务依据。
Check 覆盖 HTTP 与业务正确性,Threshold 能让失败返回非零状态。同时观察吞吐、P50/P90/P95/P99、错误率、dropped iterations 和 VU。动态 URL、用户 ID、订单 ID 和错误正文没有进入高基数 Tag。
生成器 CPU、内存、网络、文件句柄与输出开销已纳入结果可信性判断。PR Smoke、目标负载、压力和浸泡测试具有不同执行环境与门禁语义。Secret 使用受控来源,脚本、日志、指标和报告没有泄漏凭证或个人数据。
Operator 并行执行的数据分片、总负载、出口容量和结果归并已验证。Grafana Cloud 的区域、权限、保留、费用和数据边界已经审批。作业具有独立取消入口,停止后完成积压、恢复和测试数据清理验证。
