Files
user-system/docs/performance/k6_load_test.js

352 lines
13 KiB
JavaScript
Raw Normal View History

/**
* 用户管理系统 (UMS) - k6 全场景性能测试套件
*
* 测试策略
* Stage 1 - 预热阶段 (2min): 0 10 VU验证系统基线
* Stage 2 - 正常负载 (5min): 50 VU验证日常运营能力
* Stage 3 - 峰值负载 (3min): 100 VU验证高峰时段
* Stage 4 - 持续峰值 (5min): 100 VU验证耐久性
* Stage 5 - 压力测试 (2min): 200 VU寻找系统断点
* Stage 6 - 尖峰测试 (1min): 500 VU模拟流量骤增
* Stage 7 - 冷却阶段 (2min): 200 0 VU
*
* 运行命令:
* k6 run --env BASE_URL=http://localhost:8080 docs/performance/k6_load_test.js
* k6 run --env BASE_URL=http://localhost:8080 --env SCENARIO=smoke docs/performance/k6_load_test.js
*/
import http from 'k6/http';
import { check, sleep, group } from 'k6';
import { Rate, Trend, Counter, Gauge } from 'k6/metrics';
import { SharedArray } from 'k6/data';
import exec from 'k6/execution';
// ─────────────────────────────────────────────
// 自定义指标
// ─────────────────────────────────────────────
const loginErrorRate = new Rate('login_errors');
const apiErrorRate = new Rate('api_errors');
const loginLatency = new Trend('login_latency_ms', true);
const userQueryLatency = new Trend('user_query_latency_ms', true);
const tokenRefreshLatency = new Trend('token_refresh_latency_ms', true);
const authRequests = new Counter('authenticated_requests');
const activeSessionGauge = new Gauge('active_sessions');
const BASE_URL = __ENV.BASE_URL || 'http://localhost:8080';
const SCENARIO = __ENV.SCENARIO || 'full';
// ─────────────────────────────────────────────
// 测试场景配置
// ─────────────────────────────────────────────
const scenarios = {
smoke: {
stages: [
{ duration: '30s', target: 5 },
{ duration: '1m', target: 5 },
{ duration: '30s', target: 0 },
],
},
full: {
stages: [
{ duration: '2m', target: 10 }, // 预热
{ duration: '5m', target: 50 }, // 正常负载
{ duration: '3m', target: 100 }, // 峰值负载
{ duration: '5m', target: 100 }, // 持续峰值(耐久)
{ duration: '2m', target: 200 }, // 压力测试
{ duration: '1m', target: 500 }, // 尖峰测试
{ duration: '2m', target: 0 }, // 冷却
],
},
stress: {
stages: [
{ duration: '2m', target: 200 },
{ duration: '5m', target: 200 },
{ duration: '2m', target: 400 },
{ duration: '5m', target: 400 },
{ duration: '2m', target: 0 },
],
},
soak: {
stages: [
{ duration: '2m', target: 50 },
{ duration: '30m', target: 50 }, // 耐力测试 30 分钟
{ duration: '2m', target: 0 },
],
},
};
export const options = {
stages: scenarios[SCENARIO]?.stages || scenarios.full.stages,
thresholds: {
// HTTP 级别 SLA
http_req_duration: ['p(95)<500', 'p(99)<1000'],
http_req_failed: ['rate<0.01'], // 错误率 < 1%
// 业务级别 SLA
login_latency_ms: ['p(95)<300', 'p(99)<800'],
user_query_latency_ms: ['p(95)<200', 'p(99)<500'],
token_refresh_latency_ms: ['p(95)<150', 'p(99)<400'],
// 错误率
login_errors: ['rate<0.02'], // 登录错误率 < 2%
api_errors: ['rate<0.01'], // API 错误率 < 1%
},
summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(90)', 'p(95)', 'p(99)', 'count'],
};
// ─────────────────────────────────────────────
// 辅助函数
// ─────────────────────────────────────────────
function getCsrfToken() {
const res = http.get(`${BASE_URL}/api/v1/auth/csrf-token`, {
headers: { 'Content-Type': 'application/json' },
});
if (res.status === 200) {
try {
return res.json('csrf_token') || res.json('data.csrf_token') || '';
} catch (_) {
return '';
}
}
return '';
}
function login(username, password, csrfToken) {
const start = Date.now();
const payload = JSON.stringify({
account: username,
password: password,
device_id: `load-test-device-${exec.vu.idInTest}`,
device_name: 'k6-load-tester',
device_browser: 'k6',
device_os: 'linux',
});
const res = http.post(`${BASE_URL}/api/v1/auth/login`, payload, {
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken,
},
});
const latencyMs = Date.now() - start;
loginLatency.add(latencyMs);
const success = check(res, {
'登录状态200': (r) => r.status === 200,
'返回access_token': (r) => {
try {
const body = r.json();
return !!(body.access_token || (body.data && body.data.access_token));
} catch (_) { return false; }
},
'登录延迟<800ms': (_) => latencyMs < 800,
});
loginErrorRate.add(!success);
return res.status === 200 ? res : null;
}
function getAccessToken(loginRes) {
if (!loginRes) return null;
try {
const body = loginRes.json();
return body.access_token || (body.data && body.data.access_token) || null;
} catch (_) { return null; }
}
function authHeaders(token, csrfToken) {
return {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken || '',
},
};
}
// ─────────────────────────────────────────────
// 测试场景主函数
// ─────────────────────────────────────────────
export default function () {
const csrfToken = getCsrfToken();
sleep(0.1);
// ── 场景1: 认证流程 (权重 30%) ──────────────
group('认证流程', function () {
const loginRes = login('admin', 'Admin@123456', csrfToken);
if (!loginRes) { sleep(1); return; }
const token = getAccessToken(loginRes);
if (!token) { sleep(1); return; }
activeSessionGauge.add(1);
authRequests.add(1);
// 获取用户信息
const userInfoRes = http.get(`${BASE_URL}/api/v1/auth/userinfo`, authHeaders(token, csrfToken));
check(userInfoRes, {
'用户信息200': (r) => r.status === 200,
'包含用户名': (r) => {
try { return !!r.json('username'); } catch (_) { return false; }
},
});
apiErrorRate.add(userInfoRes.status !== 200);
sleep(0.5 + Math.random() * 0.5);
// ── 场景2: 用户管理操作 (权重 40%) ──────────
group('用户管理', function () {
const start = Date.now();
const listRes = http.get(
`${BASE_URL}/api/v1/users?page=1&page_size=20`,
authHeaders(token, csrfToken)
);
const latencyMs = Date.now() - start;
userQueryLatency.add(latencyMs);
const listOk = check(listRes, {
'用户列表200': (r) => r.status === 200,
'返回数据数组': (r) => {
try {
const body = r.json();
return Array.isArray(body.data) || Array.isArray(body.items) ||
(body.data && Array.isArray(body.data.list));
} catch (_) { return false; }
},
'查询延迟<500ms': (_) => latencyMs < 500,
});
apiErrorRate.add(!listOk);
sleep(0.2 + Math.random() * 0.3);
// 角色列表查询
const rolesRes = http.get(`${BASE_URL}/api/v1/roles`, authHeaders(token, csrfToken));
check(rolesRes, {
'角色列表200': (r) => r.status === 200,
});
apiErrorRate.add(rolesRes.status !== 200);
sleep(0.2);
});
// ── 场景3: 日志查询(分页)──────────────────
group('日志查询', function () {
// offset 分页
const logRes = http.get(
`${BASE_URL}/api/v1/logs/login?page=1&page_size=20`,
authHeaders(token, csrfToken)
);
check(logRes, {
'日志列表200': (r) => r.status === 200,
});
sleep(0.3 + Math.random() * 0.2);
// cursor 分页(深翻)
const cursorRes = http.get(
`${BASE_URL}/api/v1/logs/login?size=20`,
authHeaders(token, csrfToken)
);
check(cursorRes, {
'cursor分页200': (r) => r.status === 200,
});
sleep(0.2);
});
// ── 场景4: Token 刷新 (每10次请求模拟一次) ──
if (exec.vu.iterationInScenario % 10 === 0) {
group('Token刷新', function () {
const start = Date.now();
const refreshRes = http.post(
`${BASE_URL}/api/v1/auth/refresh`,
null,
authHeaders(token, csrfToken)
);
const latencyMs = Date.now() - start;
tokenRefreshLatency.add(latencyMs);
check(refreshRes, {
'刷新成功200或401': (r) => r.status === 200 || r.status === 401,
});
sleep(0.1);
});
}
activeSessionGauge.add(-1);
sleep(1 + Math.random() * 1);
});
}
// ─────────────────────────────────────────────
// 测试结束汇总
// ─────────────────────────────────────────────
export function handleSummary(data) {
const now = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
function formatMetric(metric) {
if (!metric || !metric.values) return 'N/A';
const v = metric.values;
if (v.rate !== undefined) return `${(v.rate * 100).toFixed(2)}%`;
if (v['p(99)'] !== undefined) {
return `avg=${v.avg?.toFixed(1)}ms p50=${v.med?.toFixed(1)}ms p95=${v['p(95)']?.toFixed(1)}ms p99=${v['p(99)']?.toFixed(1)}ms max=${v.max?.toFixed(1)}ms`;
}
return JSON.stringify(v);
}
const report = {
summary: {
test_time: now,
scenario: SCENARIO,
base_url: BASE_URL,
total_requests: data.metrics.http_reqs?.values?.count,
total_duration: data.state?.testRunDurationMs,
peak_vus: data.metrics.vus_max?.values?.max,
},
sla_results: {
http_req_duration_p99: formatMetric(data.metrics.http_req_duration),
http_req_failed_rate: formatMetric(data.metrics.http_req_failed),
login_latency_p99: formatMetric(data.metrics.login_latency_ms),
user_query_latency_p99: formatMetric(data.metrics.user_query_latency_ms),
token_refresh_latency_p99: formatMetric(data.metrics.token_refresh_latency_ms),
login_error_rate: formatMetric(data.metrics.login_errors),
api_error_rate: formatMetric(data.metrics.api_errors),
},
raw_metrics: data.metrics,
};
return {
[`docs/performance/results/k6_result_${now}.json`]: JSON.stringify(report, null, 2),
stdout: generateTextSummary(data, report),
};
}
function generateTextSummary(data, report) {
const thresholds = data.metrics;
const passed = Object.entries(data.metrics)
.filter(([, m]) => m.thresholds)
.every(([, m]) => Object.values(m.thresholds).every(t => !t.ok === false));
return `
UMS 性能测试报告 (k6)
📊 测试概要
场景: ${report.summary.scenario}
目标地址: ${report.summary.base_url}
总请求数: ${report.summary.total_requests?.toLocaleString() || 'N/A'}
峰值 VU: ${report.summary.peak_vus || 'N/A'}
SLA 结果
HTTP P99: ${report.sla_results.http_req_duration_p99}
HTTP 错误率: ${report.sla_results.http_req_failed_rate}
登录 P99: ${report.sla_results.login_latency_p99}
用户查询 P99: ${report.sla_results.user_query_latency_p99}
Token刷新 P99: ${report.sla_results.token_refresh_latency_p99}
📝 详细结果已写入 docs/performance/results/
`;
}