- Remove old review reports (keep latest only) - Move docs/ to deploy/docs-backup/ - Move performance-testing/ to deploy/ - Clean up test output files - Organize root directory
415 lines
11 KiB
JavaScript
415 lines
11 KiB
JavaScript
// Sub2API Mixed Workload Performance Test
|
||
// 综合负载性能测试 - 模拟真实用户行为
|
||
|
||
import http from 'k6/http';
|
||
import { check, sleep, group } from 'k6';
|
||
import { Rate, Trend, Counter } from 'k6/metrics';
|
||
import { config, getBaseUrl, getAuthToken } from '../common/utils.js';
|
||
import { httpGet, httpPost, randomSleep, randomChoice } from '../common/utils.js';
|
||
|
||
// ============= 自定义指标 =============
|
||
|
||
const totalRequestDuration = new Trend('total_request_duration');
|
||
const errorRate = new Rate('errors');
|
||
const throughputCounter = new Counter('throughput');
|
||
|
||
// 分模块指标
|
||
const moduleMetrics = {
|
||
health: new Trend('health_duration'),
|
||
auth: new Trend('auth_duration'),
|
||
apiKeys: new Trend('apikeys_duration'),
|
||
gateway: new Trend('gateway_duration'),
|
||
admin: new Trend('admin_duration'),
|
||
};
|
||
|
||
// ============= 测试配置 =============
|
||
|
||
export const options = {
|
||
scenarios: {
|
||
baseline: {
|
||
executor: 'ramping-vus',
|
||
startVUs: 10,
|
||
stages: [
|
||
{ duration: '2m', target: 10 }, // 预热
|
||
{ duration: '3m', target: 50 }, // 正常负载
|
||
{ duration: '1m', target: 0 }, // 冷却
|
||
],
|
||
},
|
||
load: {
|
||
executor: 'ramping-vus',
|
||
startVUs: 20,
|
||
stages: [
|
||
{ duration: '2m', target: 20 }, // 预热
|
||
{ duration: '2m', target: 100 }, // 正常负载
|
||
{ duration: '2m', target: 200 }, // 峰值负载
|
||
{ duration: '2m', target: 200 }, // 持续峰值
|
||
{ duration: '2m', target: 0 }, // 冷却
|
||
],
|
||
},
|
||
stress: {
|
||
executor: 'ramping-vus',
|
||
startVUs: 50,
|
||
stages: [
|
||
{ duration: '1m', target: 50 }, // 预热
|
||
{ duration: '2m', target: 200 }, // 正常负载
|
||
{ duration: '2m', target: 500 }, // 高负载
|
||
{ duration: '3m', target: 1000 }, // 极限负载
|
||
{ duration: '2m', target: 0 }, // 冷却
|
||
],
|
||
},
|
||
soak: {
|
||
executor: 'constant-vus',
|
||
vus: 100,
|
||
duration: '8h',
|
||
},
|
||
spike: {
|
||
executor: 'ramping-vus',
|
||
startVUs: 50,
|
||
stages: [
|
||
{ duration: '30s', target: 50 }, // 基线
|
||
{ duration: '1m', target: 1000 }, // 尖峰
|
||
{ duration: '1m', target: 1000 }, // 保持尖峰
|
||
{ duration: '30s', target: 50 }, // 恢复
|
||
{ duration: '2m', target: 0 }, // 冷却
|
||
],
|
||
},
|
||
},
|
||
|
||
thresholds: {
|
||
// 全局阈值
|
||
'errors': ['rate<0.02'], // 错误率 < 2%
|
||
'total_request_duration': ['p(95)<2000', 'p(99)<5000'],
|
||
|
||
// 各模块阈值
|
||
'health_duration': ['p(95)<200'],
|
||
'auth_duration': ['p(95)<500'],
|
||
'apikeys_duration': ['p(95)<1000'],
|
||
'gateway_duration': ['p(95)<3000'],
|
||
'admin_duration': ['p(95)<1500'],
|
||
},
|
||
};
|
||
|
||
// ============= 辅助函数 =============
|
||
|
||
function getHeaders() {
|
||
return {
|
||
'Authorization': `Bearer ${getAuthToken()}`,
|
||
'Content-Type': 'application/json',
|
||
};
|
||
}
|
||
|
||
// ============= 测试场景 =============
|
||
|
||
/**
|
||
* 健康检查测试
|
||
*/
|
||
function testHealthCheck() {
|
||
const start = Date.now();
|
||
const res = http.get(`${getBaseUrl()}/health`, {
|
||
tags: { name: 'health' },
|
||
});
|
||
|
||
moduleMetrics.health.add(Date.now() - start);
|
||
errorRate.add(res.status !== 200);
|
||
check(res, {
|
||
'Health check OK': (r) => r.status === 200,
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 认证测试
|
||
*/
|
||
function testAuth() {
|
||
const start = Date.now();
|
||
|
||
// 登录
|
||
const loginRes = http.post(
|
||
`${getBaseUrl()}/api/v1/auth/login`,
|
||
JSON.stringify({
|
||
email: 'user@example.com',
|
||
password: 'password123',
|
||
}),
|
||
{
|
||
headers: { 'Content-Type': 'application/json' },
|
||
tags: { name: 'auth_login' },
|
||
}
|
||
);
|
||
|
||
moduleMetrics.auth.add(Date.now() - start);
|
||
errorRate.add(loginRes.status !== 200);
|
||
|
||
check(loginRes, {
|
||
'Login OK': (r) => r.status === 200,
|
||
'Has token': (r) => r.json('token') !== undefined,
|
||
});
|
||
}
|
||
|
||
/**
|
||
* API Key 管理测试
|
||
*/
|
||
function testAPIKeys() {
|
||
const start = Date.now();
|
||
|
||
// 列出 keys
|
||
const listRes = http.get(`${getBaseUrl()}/api/v1/keys`, {
|
||
headers: getHeaders(),
|
||
tags: { name: 'apikeys_list' },
|
||
});
|
||
|
||
check(listRes, {
|
||
'List OK': (r) => r.status === 200,
|
||
});
|
||
|
||
// 随机选择一个操作
|
||
const op = randomChoice(['create', 'update', 'delete']);
|
||
|
||
if (listRes.status === 200) {
|
||
const keys = listRes.json('data');
|
||
|
||
if (op === 'create' && keys.length < 10) {
|
||
// 创建新 key
|
||
const createRes = http.post(
|
||
`${getBaseUrl()}/api/v1/keys`,
|
||
JSON.stringify({ name: `perf-test-${Date.now()}` }),
|
||
{ headers: getHeaders(), tags: { name: 'apikeys_create' } }
|
||
);
|
||
check(createRes, {
|
||
'Create OK': (r) => r.status === 201,
|
||
});
|
||
} else if (keys.length > 0) {
|
||
// 更新或删除
|
||
const keyId = keys[0].id;
|
||
|
||
if (op === 'update') {
|
||
const updateRes = http.patch(
|
||
`${getBaseUrl()}/api/v1/keys/${keyId}`,
|
||
JSON.stringify({ name: `updated-${Date.now()}` }),
|
||
{ headers: getHeaders(), tags: { name: 'apikeys_update' } }
|
||
);
|
||
check(updateRes, {
|
||
'Update OK': (r) => r.status === 200,
|
||
});
|
||
}
|
||
// delete 操作可选执行,避免清理测试数据
|
||
}
|
||
}
|
||
|
||
moduleMetrics.apikeys.add(Date.now() - start);
|
||
}
|
||
|
||
/**
|
||
* Gateway API 测试
|
||
*/
|
||
function testGateway() {
|
||
const start = Date.now();
|
||
|
||
// 准备 API Key
|
||
const listRes = http.get(`${getBaseUrl()}/api/v1/keys`, {
|
||
headers: getHeaders(),
|
||
});
|
||
|
||
if (listRes.status !== 200) {
|
||
return;
|
||
}
|
||
|
||
const keys = listRes.json('data');
|
||
if (!keys || keys.length === 0) {
|
||
return;
|
||
}
|
||
|
||
const apiKey = keys[0].key;
|
||
|
||
// 随机选择一个平台
|
||
const platform = randomChoice(['openai', 'claude', 'gemini']);
|
||
|
||
let res;
|
||
let model;
|
||
|
||
switch (platform) {
|
||
case 'openai':
|
||
res = http.post(
|
||
`${getBaseUrl()}/v1/chat/completions`,
|
||
JSON.stringify({
|
||
model: 'gpt-3.5-turbo',
|
||
messages: [{ role: 'user', content: 'Hi' }],
|
||
max_tokens: 10,
|
||
}),
|
||
{
|
||
headers: {
|
||
'Authorization': `Bearer ${apiKey}`,
|
||
'Content-Type': 'application/json',
|
||
},
|
||
tags: { name: 'gateway_openai' },
|
||
}
|
||
);
|
||
break;
|
||
|
||
case 'claude':
|
||
res = http.post(
|
||
`${getBaseUrl()}/v1/messages`,
|
||
JSON.stringify({
|
||
model: 'claude-3-5-haiku-20241022',
|
||
messages: [{ role: 'user', content: 'Hi' }],
|
||
max_tokens: 10,
|
||
}),
|
||
{
|
||
headers: {
|
||
'Authorization': `Bearer ${apiKey}`,
|
||
'Content-Type': 'application/json',
|
||
'anthropic-version': '2023-06-01',
|
||
},
|
||
tags: { name: 'gateway_claude' },
|
||
}
|
||
);
|
||
break;
|
||
|
||
case 'gemini':
|
||
res = http.post(
|
||
`${getBaseUrl()}/v1beta/models/gemini-pro:generateContent`,
|
||
JSON.stringify({
|
||
contents: [{ parts: [{ text: 'Hi' }] }],
|
||
}),
|
||
{
|
||
headers: {
|
||
'Authorization': `Bearer ${apiKey}`,
|
||
'Content-Type': 'application/json',
|
||
},
|
||
tags: { name: 'gateway_gemini' },
|
||
}
|
||
);
|
||
break;
|
||
}
|
||
|
||
moduleMetrics.gateway.add(Date.now() - start);
|
||
// Gateway 错误不计入全局错误率(上游可能不可用)
|
||
check(res, {
|
||
'Gateway OK or expected error': (r) => r.status < 500,
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Admin API 测试(仅部分 VU)
|
||
*/
|
||
function testAdmin() {
|
||
if (__VU % 10 !== 0) {
|
||
return; // 只有 10% 的 VU 执行 admin 测试
|
||
}
|
||
|
||
const start = Date.now();
|
||
|
||
const res = http.get(`${getBaseUrl()}/api/v1/admin/dashboard`, {
|
||
headers: getHeaders(),
|
||
tags: { name: 'admin_dashboard' },
|
||
});
|
||
|
||
moduleMetrics.admin.add(Date.now() - start);
|
||
check(res, {
|
||
'Admin OK': (r) => r.status === 200,
|
||
});
|
||
}
|
||
|
||
// ============= 用户行为模拟 =============
|
||
|
||
/**
|
||
* 模拟真实用户行为
|
||
*/
|
||
function simulateUserBehavior() {
|
||
// 根据权重选择操作
|
||
const rand = Math.random();
|
||
|
||
if (rand < 0.05) {
|
||
// 5% 健康检查
|
||
testHealthCheck();
|
||
} else if (rand < 0.10) {
|
||
// 5% 登录
|
||
testAuth();
|
||
} else if (rand < 0.25) {
|
||
// 15% API Key 管理
|
||
testAPIKeys();
|
||
} else if (rand < 0.95) {
|
||
// 70% Gateway 请求
|
||
testGateway();
|
||
} else {
|
||
// 5% Admin 请求
|
||
testAdmin();
|
||
}
|
||
}
|
||
|
||
// ============= 主测试函数 =============
|
||
|
||
export default function () {
|
||
const start = Date.now();
|
||
|
||
group('Mixed Workload', () => {
|
||
simulateUserBehavior();
|
||
});
|
||
|
||
totalRequestDuration.add(Date.now() - start);
|
||
throughputCounter.add(1);
|
||
|
||
// 随机思考时间
|
||
randomSleep(0.5, 3);
|
||
}
|
||
|
||
// ============= 测试报告 =============
|
||
|
||
export function handleSummary(data) {
|
||
const metrics = data.metrics;
|
||
|
||
return {
|
||
'mixed-workload-report.json': JSON.stringify(data, null, 2),
|
||
'mixed-workload-summary.txt': `
|
||
================================================================================
|
||
Sub2API 综合负载性能测试报告
|
||
================================================================================
|
||
|
||
测试时间: ${new Date().toISOString()}
|
||
测试持续: ${(data.state.testRunDurationMs / 1000 / 60).toFixed(2)} 分钟
|
||
峰值 VU: ${metrics?.vus?.peak || 0}
|
||
最终 VU: ${metrics?.vus?.value || 0}
|
||
|
||
--------------------------------------------------------------------------------
|
||
核心指标
|
||
--------------------------------------------------------------------------------
|
||
|
||
总请求数: ${metrics?.throughput?.values?.count || 0}
|
||
错误率: ${((metrics?.errors?.values?.rate || 0) * 100).toFixed(2)}%
|
||
|
||
平均响应时间: ${metrics?.total_request_duration?.values?.avg?.toFixed(2) || 0} ms
|
||
P50 响应时间: ${metrics?.total_request_duration?.values?.['p(50)']?.toFixed(2) || 0} ms
|
||
P95 响应时间: ${metrics?.total_request_duration?.values?.['p(95)']?.toFixed(2) || 0} ms
|
||
P99 响应时间: ${metrics?.total_request_duration?.values?.['p(99)']?.toFixed(2) || 0} ms
|
||
最大响应时间: ${metrics?.total_request_duration?.values?.max?.toFixed(2) || 0} ms
|
||
|
||
--------------------------------------------------------------------------------
|
||
分模块性能
|
||
--------------------------------------------------------------------------------
|
||
|
||
健康检查:
|
||
平均: ${metrics?.health_duration?.values?.avg?.toFixed(2) || 0} ms
|
||
P95: ${metrics?.health_duration?.values?.['p(95)']?.toFixed(2) || 0} ms
|
||
|
||
认证:
|
||
平均: ${metrics?.auth_duration?.values?.avg?.toFixed(2) || 0} ms
|
||
P95: ${metrics?.auth_duration?.values?.['p(95)']?.toFixed(2) || 0} ms
|
||
|
||
API Key 管理:
|
||
平均: ${metrics?.apikeys_duration?.values?.avg?.toFixed(2) || 0} ms
|
||
P95: ${metrics?.apikeys_duration?.values?.['p(95)']?.toFixed(2) || 0} ms
|
||
|
||
Gateway:
|
||
平均: ${metrics?.gateway_duration?.values?.avg?.toFixed(2) || 0} ms
|
||
P95: ${metrics?.gateway_duration?.values?.['p(95)']?.toFixed(2) || 0} ms
|
||
P99: ${metrics?.gateway_duration?.values?.['p(99)']?.toFixed(2) || 0} ms
|
||
|
||
管理后台:
|
||
平均: ${metrics?.admin_duration?.values?.avg?.toFixed(2) || 0} ms
|
||
P95: ${metrics?.admin_duration?.values?.['p(95)']?.toFixed(2) || 0} ms
|
||
|
||
================================================================================
|
||
测试完成
|
||
================================================================================
|
||
`,
|
||
};
|
||
}
|