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 无漏洞
352 lines
13 KiB
JavaScript
352 lines
13 KiB
JavaScript
/**
|
||
* 用户管理系统 (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/
|
||
`;
|
||
}
|