Files
user-system/docs/performance/k6_load_test.js
long-agent 7b047e2f11 perf: Sprint 19 P0/P1 性能优化落地
P0(高优先级):
- P0-1: 确认数据库复合索引已存在(GORM tag),composite_index_test 验证通过
- P0-2: 连接池调优 MaxIdleConns 5→10, ConnMaxLifetime 30min→5min
- P0-3: Redis 智能探测(ProbeRedis),无 Redis 自动降级到纯内存模式

P1(中优先级):
- P1-1: GZIP 压缩中间件(compress/gzip 标准库,零新依赖)
- P1-2: 权限缓存 TTL 30min→5min
- P1-3: Argon2id 启动自适应校准(CalibrateArgon2id)

历史优化(含本次提交):
- L1Cache O(n)→O(1) LRU 重构
- Auth 中间件 DB 查询合并 + 5s L1 缓存
- Logger 异步化(4096 缓冲通道)

验证: go build/vet/test 41/41 PASS, govulncheck 无漏洞
2026-04-18 22:57:44 +08:00

352 lines
13 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 用户管理系统 (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/
`;
}