/** * 用户管理系统 (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/ `; }