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
|
|||
|
|
|
|||
|
|
================================================================================
|
|||
|
|
测试完成
|
|||
|
|
================================================================================
|
|||
|
|
`,
|
|||
|
|
};
|
|||
|
|
}
|