refactor: clean up project structure
- 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
This commit is contained in:
247
deploy/performance-testing/test-suites/admin.test.js
Normal file
247
deploy/performance-testing/test-suites/admin.test.js
Normal file
@@ -0,0 +1,247 @@
|
||||
// Sub2API Admin API Performance Test
|
||||
// 管理后台 API 性能测试
|
||||
|
||||
import http from 'k6/http';
|
||||
import { check, sleep, group } from 'k6';
|
||||
import { Rate, Trend, Counter } from 'k6/metrics';
|
||||
import { config, getBaseUrl, getAdminCredentials } from '../common/utils.js';
|
||||
import { httpGet, httpPost, randomSleep } from '../common/utils.js';
|
||||
|
||||
// ============= 自定义指标 =============
|
||||
|
||||
const adminUserListDuration = new Trend('admin_user_list_duration');
|
||||
const adminGroupStatsDuration = new Trend('admin_group_stats_duration');
|
||||
const adminDashboardDuration = new Trend('admin_dashboard_duration');
|
||||
const adminErrorRate = new Rate('admin_errors');
|
||||
|
||||
// ============= 测试配置 =============
|
||||
|
||||
export const options = {
|
||||
scenarios: {
|
||||
admin_load: {
|
||||
executor: 'ramping-vus',
|
||||
startVUs: 2,
|
||||
stages: [
|
||||
{ duration: '1m', target: 5 },
|
||||
{ duration: '3m', target: 20 },
|
||||
{ duration: '2m', target: 0 },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
thresholds: {
|
||||
'admin_user_list_duration': ['p(95)<1000', 'p(99)<2000'],
|
||||
'admin_group_stats_duration': ['p(95)<1500', 'p(99)<3000'],
|
||||
'admin_errors': ['rate<0.02'],
|
||||
},
|
||||
};
|
||||
|
||||
// ============= 辅助函数 =============
|
||||
|
||||
function getAdminHeaders() {
|
||||
return {
|
||||
'Authorization': `Bearer ${getAdminToken()}`,
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
}
|
||||
|
||||
let adminToken = null;
|
||||
let tokenExpiry = 0;
|
||||
|
||||
function getAdminToken() {
|
||||
const now = Date.now();
|
||||
if (adminToken && now < tokenExpiry) {
|
||||
return adminToken;
|
||||
}
|
||||
|
||||
const credentials = getAdminCredentials();
|
||||
const res = http.post(
|
||||
`${getBaseUrl()}/api/v1/auth/login`,
|
||||
JSON.stringify({
|
||||
email: credentials.email,
|
||||
password: credentials.password,
|
||||
}),
|
||||
{
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
tags: { name: 'admin_login' },
|
||||
}
|
||||
);
|
||||
|
||||
if (res.status === 200) {
|
||||
adminToken = res.json('token');
|
||||
tokenExpiry = now + (55 * 60 * 1000);
|
||||
return adminToken;
|
||||
}
|
||||
|
||||
throw new Error('Admin login failed');
|
||||
}
|
||||
|
||||
// ============= 测试场景 =============
|
||||
|
||||
/**
|
||||
* 测试用户列表
|
||||
*/
|
||||
function testUserList() {
|
||||
const res = http.get(`${getBaseUrl()}/api/v1/admin/users?page=1&limit=20`, {
|
||||
headers: getAdminHeaders(),
|
||||
tags: { name: 'admin_user_list' },
|
||||
});
|
||||
|
||||
adminUserListDuration.add(res.timings.duration);
|
||||
adminErrorRate.add(res.status !== 200);
|
||||
|
||||
check(res, {
|
||||
'User List: status is 200': (r) => r.status === 200,
|
||||
'User List: has data': (r) => r.json('data') !== undefined,
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试分组统计
|
||||
*/
|
||||
function testGroupStats(groupId = 1) {
|
||||
const res = http.get(`${getBaseUrl()}/api/v1/admin/groups/${groupId}/stats`, {
|
||||
headers: getAdminHeaders(),
|
||||
tags: { name: 'admin_group_stats' },
|
||||
});
|
||||
|
||||
adminGroupStatsDuration.add(res.timings.duration);
|
||||
adminErrorRate.add(res.status !== 200);
|
||||
|
||||
check(res, {
|
||||
'Group Stats: status is 200': (r) => r.status === 200,
|
||||
'Group Stats: has stats': (r) => r.json() !== undefined,
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试仪表板
|
||||
*/
|
||||
function testDashboard() {
|
||||
const res = http.get(`${getBaseUrl()}/api/v1/admin/dashboard`, {
|
||||
headers: getAdminHeaders(),
|
||||
tags: { name: 'admin_dashboard' },
|
||||
});
|
||||
|
||||
adminDashboardDuration.add(res.timings.duration);
|
||||
adminErrorRate.add(res.status !== 200);
|
||||
|
||||
check(res, {
|
||||
'Dashboard: status is 200': (r) => r.status === 200,
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试余额调整
|
||||
*/
|
||||
function testBalanceAdjustment() {
|
||||
// 首先获取一个用户 ID
|
||||
const userListRes = testUserList();
|
||||
if (userListRes.status !== 200 || !userListRes.json('data')?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const userId = userListRes.json('data')[0].id;
|
||||
|
||||
const res = http.post(
|
||||
`${getBaseUrl()}/api/v1/admin/users/${userId}/balance`,
|
||||
JSON.stringify({
|
||||
balance: 10.00,
|
||||
operation: 'add',
|
||||
notes: 'Performance test adjustment',
|
||||
}),
|
||||
{
|
||||
headers: getAdminHeaders(),
|
||||
tags: { name: 'admin_balance_adjust' },
|
||||
}
|
||||
);
|
||||
|
||||
check(res, {
|
||||
'Balance Adjust: status is 200': (r) => r.status === 200,
|
||||
});
|
||||
|
||||
adminErrorRate.add(res.status !== 200);
|
||||
}
|
||||
|
||||
// ============= 主测试函数 =============
|
||||
|
||||
export default function () {
|
||||
group('Admin API', () => {
|
||||
const mode = __VU % 5;
|
||||
|
||||
switch (mode) {
|
||||
case 0:
|
||||
testDashboard();
|
||||
break;
|
||||
case 1:
|
||||
testUserList();
|
||||
randomSleep(0.1, 0.5);
|
||||
testUserList();
|
||||
break;
|
||||
case 2:
|
||||
testGroupStats(1);
|
||||
testGroupStats(2);
|
||||
break;
|
||||
case 3:
|
||||
testBalanceAdjustment();
|
||||
break;
|
||||
case 4:
|
||||
// 综合测试
|
||||
testDashboard();
|
||||
testUserList();
|
||||
testGroupStats(1);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
randomSleep(1, 3);
|
||||
}
|
||||
|
||||
export function handleSummary(data) {
|
||||
return {
|
||||
'admin-performance-report.json': JSON.stringify(data, null, 2),
|
||||
'admin-performance-summary.txt': `
|
||||
================================================================================
|
||||
Sub2API Admin API 性能测试报告
|
||||
================================================================================
|
||||
|
||||
测试时间: ${new Date().toISOString()}
|
||||
测试持续: ${(data.state.testRunDurationMs / 1000 / 60).toFixed(2)} 分钟
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
核心指标
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
总请求数: ${data.metrics?.requests_total?.values?.count || 0}
|
||||
错误率: ${((data.metrics?.admin_errors?.values?.rate || 0) * 100).toFixed(2)}%
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
操作类型性能
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
仪表板:
|
||||
平均响应: ${data.metrics?.admin_dashboard_duration?.values?.avg?.toFixed(2) || 0} ms
|
||||
P95 响应: ${data.metrics?.admin_dashboard_duration?.values?.['p(95)']?.toFixed(2) || 0} ms
|
||||
|
||||
用户列表:
|
||||
平均响应: ${data.metrics?.admin_user_list_duration?.values?.avg?.toFixed(2) || 0} ms
|
||||
P95 响应: ${data.metrics?.admin_user_list_duration?.values?.['p(95)']?.toFixed(2) || 0} ms
|
||||
P99 响应: ${data.metrics?.admin_user_list_duration?.values?.['p(99)']?.toFixed(2) || 0} ms
|
||||
|
||||
分组统计:
|
||||
平均响应: ${data.metrics?.admin_group_stats_duration?.values?.avg?.toFixed(2) || 0} ms
|
||||
P95 响应: ${data.metrics?.admin_group_stats_duration?.values?.['p(95)']?.toFixed(2) || 0} ms
|
||||
P99 响应: ${data.metrics?.admin_group_stats_duration?.values?.['p(99)']?.toFixed(2) || 0} ms
|
||||
|
||||
================================================================================
|
||||
测试完成
|
||||
================================================================================
|
||||
`,
|
||||
};
|
||||
}
|
||||
318
deploy/performance-testing/test-suites/api-keys.test.js
Normal file
318
deploy/performance-testing/test-suites/api-keys.test.js
Normal file
@@ -0,0 +1,318 @@
|
||||
// Sub2API API Key Management Performance Test
|
||||
// API Key 管理接口性能测试
|
||||
|
||||
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, generateTestAPIKeyName } from '../common/utils.js';
|
||||
|
||||
// ============= 自定义指标 =============
|
||||
|
||||
const apiKeyListDuration = new Trend('apikey_list_duration');
|
||||
const apiKeyCreateDuration = new Trend('apikey_create_duration');
|
||||
const apiKeyUpdateDuration = new Trend('apikey_update_duration');
|
||||
const apiKeyDeleteDuration = new Trend('apikey_delete_duration');
|
||||
const apiKeyErrorRate = new Rate('apikey_errors');
|
||||
|
||||
// ============= 测试配置 =============
|
||||
|
||||
export const options = {
|
||||
scenarios: {
|
||||
apikey_load: {
|
||||
executor: 'ramping-vus',
|
||||
startVUs: 5,
|
||||
stages: [
|
||||
{ duration: '1m', target: 20 }, // 预热
|
||||
{ duration: '3m', target: 50 }, // 正常负载
|
||||
{ duration: '2m', target: 100 }, // 峰值负载
|
||||
{ duration: '1m', target: 0 }, // 冷却
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
thresholds: {
|
||||
'apikey_list_duration': ['p(95)<500', 'p(99)<1000'],
|
||||
'apikey_create_duration': ['p(95)<1000', 'p(99)<2000'],
|
||||
'apikey_errors': ['rate<0.01'],
|
||||
},
|
||||
};
|
||||
|
||||
// ============= 辅助函数 =============
|
||||
|
||||
/**
|
||||
* 获取认证头
|
||||
*/
|
||||
function getHeaders() {
|
||||
return {
|
||||
'Authorization': `Bearer ${getAuthToken()}`,
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建测试用 API Key
|
||||
*/
|
||||
function createTestAPIKey(name) {
|
||||
const res = http.post(
|
||||
`${getBaseUrl()}/api/v1/keys`,
|
||||
JSON.stringify({ name: name || generateTestAPIKeyName() }),
|
||||
{ headers: getHeaders(), tags: { name: 'apikey_create' } }
|
||||
);
|
||||
apiKeyCreateDuration.add(res.timings.duration);
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出 API Keys
|
||||
*/
|
||||
function listAPIKeys(params = {}) {
|
||||
const url = `${getBaseUrl()}/api/v1/keys${params.page ? `?page=${params.page}` : ''}`;
|
||||
const res = http.get(url, { headers: getHeaders(), tags: { name: 'apikey_list' } });
|
||||
apiKeyListDuration.add(res.timings.duration);
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个 API Key 详情
|
||||
*/
|
||||
function getAPIKey(id) {
|
||||
const res = http.get(`${getBaseUrl()}/api/v1/keys/${id}`, {
|
||||
headers: getHeaders(),
|
||||
tags: { name: 'apikey_get' },
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 API Key
|
||||
*/
|
||||
function updateAPIKey(id, updates) {
|
||||
const res = http.patch(`${getBaseUrl()}/api/v1/keys/${id}`, JSON.stringify(updates), {
|
||||
headers: getHeaders(),
|
||||
tags: { name: 'apikey_update' },
|
||||
});
|
||||
apiKeyUpdateDuration.add(res.timings.duration);
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除 API Key
|
||||
*/
|
||||
function deleteAPIKey(id) {
|
||||
const res = http.delete(`${getBaseUrl()}/api/v1/keys/${id}`, {
|
||||
headers: getHeaders(),
|
||||
tags: { name: 'apikey_delete' },
|
||||
});
|
||||
apiKeyDeleteDuration.add(res.timings.duration);
|
||||
return res;
|
||||
}
|
||||
|
||||
// ============= 测试场景 =============
|
||||
|
||||
/**
|
||||
* 测试 CRUD 完整流程
|
||||
*/
|
||||
function testCRUDFlow() {
|
||||
// 1. 创建
|
||||
const createRes = createTestAPIKey(`perf-crud-${Date.now()}`);
|
||||
check(createRes, {
|
||||
'Create: status is 201': (r) => r.status === 201,
|
||||
'Create: has key data': (r) => r.json('id') !== undefined,
|
||||
});
|
||||
apiKeyErrorRate.add(createRes.status !== 201);
|
||||
|
||||
if (createRes.status !== 201) return null;
|
||||
|
||||
const keyId = createRes.json('id');
|
||||
|
||||
// 2. 读取
|
||||
const getRes = getAPIKey(keyId);
|
||||
check(getRes, {
|
||||
'Get: status is 200': (r) => r.status === 200,
|
||||
'Get: id matches': (r) => r.json('id') === keyId,
|
||||
});
|
||||
|
||||
// 3. 更新
|
||||
const updateRes = updateAPIKey(keyId, { name: `updated-${Date.now()}` });
|
||||
check(updateRes, {
|
||||
'Update: status is 200': (r) => r.status === 200,
|
||||
'Update: name changed': (r) => r.json('name').includes('updated'),
|
||||
});
|
||||
apiKeyErrorRate.add(updateRes.status !== 200);
|
||||
|
||||
// 4. 删除
|
||||
const deleteRes = deleteAPIKey(keyId);
|
||||
check(deleteRes, {
|
||||
'Delete: status is 204': (r) => r.status === 204,
|
||||
});
|
||||
apiKeyErrorRate.add(deleteRes.status !== 204);
|
||||
|
||||
return keyId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试列表查询
|
||||
*/
|
||||
function testListOperations() {
|
||||
// 测试分页
|
||||
const page1 = listAPIKeys({ page: 1 });
|
||||
check(page1, {
|
||||
'List Page 1: status is 200': (r) => r.status === 200,
|
||||
'List Page 1: has data': (r) => Array.isArray(r.json('data')),
|
||||
});
|
||||
apiKeyErrorRate.add(page1.status !== 200);
|
||||
|
||||
// 测试搜索
|
||||
const searchRes = http.get(`${getBaseUrl()}/api/v1/keys?search=perf`, {
|
||||
headers: getHeaders(),
|
||||
tags: { name: 'apikey_search' },
|
||||
});
|
||||
check(searchRes, {
|
||||
'Search: status is 200': (r) => r.status === 200,
|
||||
});
|
||||
|
||||
// 随机睡眠模拟用户浏览
|
||||
randomSleep(0.1, 0.5);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试并发创建
|
||||
*/
|
||||
function testConcurrentCreate() {
|
||||
// 批量创建请求
|
||||
const batch = [];
|
||||
const count = 5;
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
batch.push([
|
||||
'POST',
|
||||
`${getBaseUrl()}/api/v1/keys`,
|
||||
JSON.stringify({ name: `batch-${Date.now()}-${i}` }),
|
||||
{
|
||||
headers: getHeaders(),
|
||||
tags: { name: 'apikey_batch_create' },
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
const responses = http.batch(batch);
|
||||
let successCount = 0;
|
||||
|
||||
for (const res of responses) {
|
||||
if (res.status === 201) successCount++;
|
||||
apiKeyErrorRate.add(res.status !== 201);
|
||||
}
|
||||
|
||||
check(responses[0], {
|
||||
'Batch Create: at least 80% success': () => successCount / count >= 0.8,
|
||||
});
|
||||
|
||||
return successCount;
|
||||
}
|
||||
|
||||
// ============= 主测试函数 =============
|
||||
|
||||
export default function () {
|
||||
const vuId = __VU;
|
||||
const iteration = __ITER__;
|
||||
|
||||
group('API Key Management', () => {
|
||||
// 每个 VU 的操作模式
|
||||
const mode = vuId % 4;
|
||||
|
||||
switch (mode) {
|
||||
case 0:
|
||||
// 读为主:列表 + 单个查询
|
||||
listAPIKeys();
|
||||
randomSleep(0.2, 0.5);
|
||||
|
||||
// 获取一个 key 进行查询
|
||||
const listRes = listAPIKeys();
|
||||
if (listRes.status === 200 && listRes.json('data').length > 0) {
|
||||
const keyId = listRes.json('data')[0].id;
|
||||
getAPIKey(keyId);
|
||||
}
|
||||
break;
|
||||
|
||||
case 1:
|
||||
// 写为主:创建 + 更新
|
||||
testCRUDFlow();
|
||||
break;
|
||||
|
||||
case 2:
|
||||
// 混合操作
|
||||
listAPIKeys();
|
||||
randomSleep(0.1, 0.3);
|
||||
testListOperations();
|
||||
break;
|
||||
|
||||
case 3:
|
||||
// 批量操作
|
||||
if (iteration % 10 === 0) {
|
||||
testConcurrentCreate();
|
||||
} else {
|
||||
listAPIKeys();
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// 全局思考时间
|
||||
randomSleep(0.5, 2);
|
||||
}
|
||||
|
||||
// ============= 测试报告 =============
|
||||
|
||||
export function handleSummary(data) {
|
||||
return {
|
||||
'apikey-performance-report.json': JSON.stringify(data, null, 2),
|
||||
'apikey-performance-summary.txt': generateSummary(data),
|
||||
};
|
||||
}
|
||||
|
||||
function generateSummary(data) {
|
||||
const metrics = data.metrics;
|
||||
|
||||
return `
|
||||
================================================================================
|
||||
Sub2API API Key Management 性能测试报告
|
||||
================================================================================
|
||||
|
||||
测试时间: ${new Date().toISOString()}
|
||||
测试持续: ${(data.state.testRunDurationMs / 1000 / 60).toFixed(2)} 分钟
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
核心指标
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
总请求数: ${metrics?.requests_total?.values?.count || 0}
|
||||
错误率: ${((metrics?.apikey_errors?.values?.rate || 0) * 100).toFixed(2)}%
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
操作类型性能
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
列表操作:
|
||||
平均响应: ${metrics?.apikey_list_duration?.values?.avg?.toFixed(2) || 0} ms
|
||||
P95 响应: ${metrics?.apikey_list_duration?.values?.['p(95)']?.toFixed(2) || 0} ms
|
||||
P99 响应: ${metrics?.apikey_list_duration?.values?.['p(99)']?.toFixed(2) || 0} ms
|
||||
|
||||
创建操作:
|
||||
平均响应: ${metrics?.apikey_create_duration?.values?.avg?.toFixed(2) || 0} ms
|
||||
P95 响应: ${metrics?.apikey_create_duration?.values?.['p(95)']?.toFixed(2) || 0} ms
|
||||
P99 响应: ${metrics?.apikey_create_duration?.values?.['p(99)']?.toFixed(2) || 0} ms
|
||||
|
||||
更新操作:
|
||||
平均响应: ${metrics?.apikey_update_duration?.values?.avg?.toFixed(2) || 0} ms
|
||||
P95 响应: ${metrics?.apikey_update_duration?.values?.['p(95)']?.toFixed(2) || 0} ms
|
||||
|
||||
删除操作:
|
||||
平均响应: ${metrics?.apikey_delete_duration?.values?.avg?.toFixed(2) || 0} ms
|
||||
P95 响应: ${metrics?.apikey_delete_duration?.values?.['p(95)']?.toFixed(2) || 0} ms
|
||||
|
||||
================================================================================
|
||||
测试完成
|
||||
================================================================================
|
||||
`;
|
||||
}
|
||||
440
deploy/performance-testing/test-suites/gateway.test.js
Normal file
440
deploy/performance-testing/test-suites/gateway.test.js
Normal file
@@ -0,0 +1,440 @@
|
||||
// Sub2API Gateway Performance Test
|
||||
// Gateway API 性能测试
|
||||
|
||||
import http from 'k6/http';
|
||||
import { check, sleep, group } from 'k6';
|
||||
import { Rate, Trend, Counter, Gauge } from 'k6/metrics';
|
||||
import { config, getBaseUrl, getAuthToken } from '../common/utils.js';
|
||||
import { httpGet, httpPost, randomSleep, generateTestAPIKeyName } from '../common/utils.js';
|
||||
|
||||
// ============= 自定义指标 =============
|
||||
|
||||
// Gateway 请求指标
|
||||
const gatewayRequestDuration = new Trend('gateway_request_duration');
|
||||
const gatewayTTFT = new Trend('gateway_ttft'); // Time To First Token
|
||||
const gatewayTokenThroughput = new Trend('gateway_token_throughput');
|
||||
const gatewayErrorRate = new Rate('gateway_errors');
|
||||
|
||||
// 平台分类指标
|
||||
const platformMetrics = {
|
||||
openai: {
|
||||
duration: new Trend('gateway_openai_duration'),
|
||||
errors: new Rate('gateway_openai_errors'),
|
||||
},
|
||||
claude: {
|
||||
duration: new Trend('gateway_claude_duration'),
|
||||
errors: new Rate('gateway_claude_errors'),
|
||||
},
|
||||
gemini: {
|
||||
duration: new Trend('gateway_gemini_duration'),
|
||||
errors: new Rate('gateway_gemini_errors'),
|
||||
},
|
||||
};
|
||||
|
||||
// ============= 测试配置 =============
|
||||
|
||||
export const options = {
|
||||
scenarios: {
|
||||
gateway_load: {
|
||||
executor: 'ramping-vus',
|
||||
startVUs: 5,
|
||||
stages: [
|
||||
{ duration: '2m', target: 10 }, // 预热
|
||||
{ duration: '5m', target: 50 }, // 正常负载
|
||||
{ duration: '2m', target: 100 }, // 峰值负载
|
||||
{ duration: '2m', target: 0 }, // 冷却
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
thresholds: {
|
||||
// Gateway 整体阈值
|
||||
'gateway_request_duration': ['p(95)<2000', 'p(99)<5000'],
|
||||
'gateway_errors': ['rate<0.05'], // 5% 以下错误率
|
||||
|
||||
// TTFT 阈值
|
||||
'gateway_ttft': ['p(95)<3000', 'p(99)<5000'],
|
||||
|
||||
// 按平台分类
|
||||
'gateway_openai_duration': ['p(95)<1500'],
|
||||
'gateway_claude_duration': ['p(95)<2000'],
|
||||
'gateway_gemini_duration': ['p(95)<1500'],
|
||||
},
|
||||
};
|
||||
|
||||
// ============= 测试数据准备 =============
|
||||
|
||||
// 获取测试用 API Key
|
||||
function getTestAPIKey() {
|
||||
const token = getAuthToken();
|
||||
|
||||
// 列出 API Keys 获取第一个
|
||||
const res = http.get(`${getBaseUrl()}/api/v1/keys`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
tags: { name: 'list_api_keys' },
|
||||
});
|
||||
|
||||
if (res.status === 200) {
|
||||
const keys = res.json('data');
|
||||
if (keys && keys.length > 0) {
|
||||
return keys[0].key;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有 API Key,创建一个
|
||||
const createRes = http.post(
|
||||
`${getBaseUrl()}/api/v1/keys`,
|
||||
JSON.stringify({
|
||||
name: `perf-test-${Date.now()}`,
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
tags: { name: 'create_api_key' },
|
||||
}
|
||||
);
|
||||
|
||||
if (createRes.status === 201) {
|
||||
return createRes.json('key');
|
||||
}
|
||||
|
||||
throw new Error('Failed to get or create API key for testing');
|
||||
}
|
||||
|
||||
// ============= 测试场景 =============
|
||||
|
||||
/**
|
||||
* 测试 OpenAI Chat Completions
|
||||
*/
|
||||
function testOpenAIChat(apiKey) {
|
||||
const start = Date.now();
|
||||
|
||||
const res = http.post(
|
||||
`${getBaseUrl()}/v1/chat/completions`,
|
||||
JSON.stringify({
|
||||
model: 'gpt-3.5-turbo',
|
||||
messages: [
|
||||
{ role: 'user', content: 'Say hello in one sentence.' }
|
||||
],
|
||||
max_tokens: 50,
|
||||
temperature: 0.7,
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
tags: { name: 'gateway_openai_chat' },
|
||||
}
|
||||
);
|
||||
|
||||
const duration = Date.now() - start;
|
||||
|
||||
// 记录指标
|
||||
platformMetrics.openai.duration.add(duration);
|
||||
platformMetrics.openai.errors.add(res.status !== 200);
|
||||
|
||||
check(res, {
|
||||
'OpenAI Chat: status is 200': (r) => r.status === 200,
|
||||
'OpenAI Chat: has content': (r) => r.json('choices[0].message.content') !== undefined,
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试 OpenAI Streaming
|
||||
*/
|
||||
function testOpenAIStream(apiKey) {
|
||||
const start = Date.now();
|
||||
let firstTokenTime = 0;
|
||||
let tokenCount = 0;
|
||||
|
||||
const res = http.post(
|
||||
`${getBaseUrl()}/v1/chat/completions`,
|
||||
JSON.stringify({
|
||||
model: 'gpt-3.5-turbo',
|
||||
messages: [
|
||||
{ role: 'user', content: 'Count from 1 to 5.' }
|
||||
],
|
||||
max_tokens: 100,
|
||||
stream: true,
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
tags: { name: 'gateway_openai_stream' },
|
||||
}
|
||||
);
|
||||
|
||||
const duration = Date.now() - start;
|
||||
|
||||
// 解析 SSE 流获取 TTFT
|
||||
if (res.headers['Content-Type']?.includes('text/event-stream')) {
|
||||
const lines = res.body.split('\n');
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ') && !line.includes('[DONE]')) {
|
||||
if (firstTokenTime === 0) {
|
||||
firstTokenTime = Date.now();
|
||||
}
|
||||
tokenCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ttft = firstTokenTime > 0 ? firstTokenTime - start : duration;
|
||||
|
||||
// 记录指标
|
||||
platformMetrics.openai.duration.add(duration);
|
||||
platformMetrics.openai.errors.add(res.status !== 200);
|
||||
gatewayTTFT.add(ttft);
|
||||
|
||||
if (tokenCount > 0) {
|
||||
gatewayTokenThroughput.add(tokenCount / (duration / 1000));
|
||||
}
|
||||
|
||||
check(res, {
|
||||
'OpenAI Stream: status is 200': (r) => r.status === 200,
|
||||
'OpenAI Stream: is streaming': (r) => r.headers['Content-Type']?.includes('text/event-stream'),
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试 Claude Messages API
|
||||
*/
|
||||
function testClaudeMessages(apiKey) {
|
||||
const start = Date.now();
|
||||
|
||||
const res = http.post(
|
||||
`${getBaseUrl()}/v1/messages`,
|
||||
JSON.stringify({
|
||||
model: 'claude-3-5-haiku-20241022',
|
||||
max_tokens: 50,
|
||||
messages: [
|
||||
{ role: 'user', content: 'Say hello in one sentence.' }
|
||||
],
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': apiKey,
|
||||
'anthropic-version': '2023-06-01',
|
||||
},
|
||||
tags: { name: 'gateway_claude_messages' },
|
||||
}
|
||||
);
|
||||
|
||||
const duration = Date.now() - start;
|
||||
|
||||
// 记录指标
|
||||
platformMetrics.claude.duration.add(duration);
|
||||
platformMetrics.claude.errors.add(res.status !== 200);
|
||||
|
||||
check(res, {
|
||||
'Claude Messages: status is 200': (r) => r.status === 200,
|
||||
'Claude Messages: has content': (r) => r.json('content[0].text') !== undefined,
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试 Gemini Generate Content
|
||||
*/
|
||||
function testGeminiGenerate(apiKey) {
|
||||
const start = Date.now();
|
||||
|
||||
const res = http.post(
|
||||
`${getBaseUrl()}/v1beta/models/gemini-pro:generateContent`,
|
||||
JSON.stringify({
|
||||
contents: [
|
||||
{
|
||||
parts: [{ text: 'Say hello in one sentence.' }]
|
||||
}
|
||||
],
|
||||
generationConfig: {
|
||||
maxOutputTokens: 50,
|
||||
},
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
tags: { name: 'gateway_gemini_generate' },
|
||||
}
|
||||
);
|
||||
|
||||
const duration = Date.now() - start;
|
||||
|
||||
// 记录指标
|
||||
platformMetrics.gemini.duration.add(duration);
|
||||
platformMetrics.gemini.errors.add(res.status !== 200);
|
||||
|
||||
check(res, {
|
||||
'Gemini Generate: status is 200': (r) => r.status === 200,
|
||||
'Gemini Generate: has content': (r) => r.json('candidates[0].content.parts[0].text') !== undefined,
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 综合 Gateway 测试
|
||||
*/
|
||||
function testGatewayMixed(apiKey) {
|
||||
// 模拟真实用户行为:70% 非流式 + 30% 流式
|
||||
const rand = Math.random();
|
||||
|
||||
if (rand < 0.3) {
|
||||
return testOpenAIStream(apiKey);
|
||||
} else if (rand < 0.5) {
|
||||
return testOpenAIChat(apiKey);
|
||||
} else if (rand < 0.7) {
|
||||
return testClaudeMessages(apiKey);
|
||||
} else {
|
||||
return testGeminiGenerate(apiKey);
|
||||
}
|
||||
}
|
||||
|
||||
// ============= 主测试函数 =============
|
||||
|
||||
export default function () {
|
||||
// 提前获取 API Key(每个 VU 一次)
|
||||
const apiKey = __ITER__ === 0 ? getTestAPIKey() : null;
|
||||
|
||||
// 或者使用全局缓存
|
||||
if (!globalThis.__testApiKey__) {
|
||||
globalThis.__testApiKey__ = getTestAPIKey();
|
||||
}
|
||||
|
||||
group('Gateway API Performance', () => {
|
||||
// 随机选择测试场景
|
||||
const testType = __VU % 4;
|
||||
|
||||
switch (testType) {
|
||||
case 0:
|
||||
testOpenAIChat(globalThis.__testApiKey__);
|
||||
break;
|
||||
case 1:
|
||||
testOpenAIStream(globalThis.__testApiKey__);
|
||||
break;
|
||||
case 2:
|
||||
testClaudeMessages(globalThis.__testApiKey__);
|
||||
break;
|
||||
case 3:
|
||||
testGeminiGenerate(globalThis.__testApiKey__);
|
||||
break;
|
||||
default:
|
||||
testGatewayMixed(globalThis.__testApiKey__);
|
||||
}
|
||||
});
|
||||
|
||||
// 模拟用户思考时间
|
||||
randomSleep(0.5, 2);
|
||||
}
|
||||
|
||||
// ============= 测试结束清理 =============
|
||||
|
||||
export function handleSummary(data) {
|
||||
return {
|
||||
'gateway-performance-report.json': JSON.stringify(data, null, 2),
|
||||
'gateway-performance-summary.txt': generateSummary(data),
|
||||
};
|
||||
}
|
||||
|
||||
function generateSummary(data) {
|
||||
const metrics = data.metrics;
|
||||
|
||||
return `
|
||||
================================================================================
|
||||
Sub2API Gateway 性能测试报告
|
||||
================================================================================
|
||||
|
||||
测试时间: ${new Date().toISOString()}
|
||||
测试持续: ${(data.state.testRunDurationMs / 1000 / 60).toFixed(2)} 分钟
|
||||
峰值 VU: ${data.state.metrics?.vus?.peak || 0}
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
核心指标
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
总请求数: ${metrics?.requests_total?.values?.count || 0}
|
||||
成功请求: ${metrics?.requests_total?.values?.passes || 0}
|
||||
失败请求: ${metrics?.requests_total?.values?.fails || 0}
|
||||
错误率: ${((metrics?.gateway_errors?.values?.rate || 0) * 100).toFixed(2)}%
|
||||
|
||||
平均响应时间: ${metrics?.gateway_request_duration?.values?.avg?.toFixed(2) || 0} ms
|
||||
P50 响应时间: ${metrics?.gateway_request_duration?.values?.['p(50)']?.toFixed(2) || 0} ms
|
||||
P95 响应时间: ${metrics?.gateway_request_duration?.values?.['p(95)']?.toFixed(2) || 0} ms
|
||||
P99 响应时间: ${metrics?.gateway_request_duration?.values?.['p(99)']?.toFixed(2) || 0} ms
|
||||
最大响应时间: ${metrics?.gateway_request_duration?.values?.max?.toFixed(2) || 0} ms
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
TTFT (Time To First Token)
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
平均 TTFT: ${metrics?.gateway_ttft?.values?.avg?.toFixed(2) || 0} ms
|
||||
P95 TTFT: ${metrics?.gateway_ttft?.values?.['p(95)']?.toFixed(2) || 0} ms
|
||||
P99 TTFT: ${metrics?.gateway_ttft?.values?.['p(99)']?.toFixed(2) || 0} ms
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
按平台分类
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
OpenAI:
|
||||
平均响应时间: ${metrics?.gateway_openai_duration?.values?.avg?.toFixed(2) || 0} ms
|
||||
P95 响应时间: ${metrics?.gateway_openai_duration?.values?.['p(95)']?.toFixed(2) || 0} ms
|
||||
错误率: ${((metrics?.gateway_openai_errors?.values?.rate || 0) * 100).toFixed(2)}%
|
||||
|
||||
Claude:
|
||||
平均响应时间: ${metrics?.gateway_claude_duration?.values?.avg?.toFixed(2) || 0} ms
|
||||
P95 响应时间: ${metrics?.gateway_claude_duration?.values?.['p(95)']?.toFixed(2) || 0} ms
|
||||
错误率: ${((metrics?.gateway_claude_errors?.values?.rate || 0) * 100).toFixed(2)}%
|
||||
|
||||
Gemini:
|
||||
平均响应时间: ${metrics?.gateway_gemini_duration?.values?.avg?.toFixed(2) || 0} ms
|
||||
P95 响应时间: ${metrics?.gateway_gemini_duration?.values?.['p(95)']?.toFixed(2) || 0} ms
|
||||
错误率: ${((metrics?.gateway_gemini_errors?.values?.rate || 0) * 100).toFixed(2)}%
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
阈值检查
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
${checkThresholds(data)}
|
||||
|
||||
================================================================================
|
||||
测试完成
|
||||
================================================================================
|
||||
`;
|
||||
}
|
||||
|
||||
function checkThresholds(data) {
|
||||
const checks = [];
|
||||
const thresholds = options.thresholds;
|
||||
|
||||
for (const [metric, threshold] of Object.entries(thresholds)) {
|
||||
const actual = data.metrics?.[metric]?.values;
|
||||
if (!actual) continue;
|
||||
|
||||
const p95 = actual['p(95)'];
|
||||
const thresholdValue = parseFloat(threshold[0]);
|
||||
|
||||
if (p95 !== undefined) {
|
||||
const passed = p95 <= thresholdValue;
|
||||
checks.push(` ${passed ? 'OK' : 'FAIL'} ${metric}: P95=${p95.toFixed(2)}ms (threshold: ${thresholdValue}ms)`);
|
||||
}
|
||||
}
|
||||
|
||||
return checks.join('\n');
|
||||
}
|
||||
101
deploy/performance-testing/test-suites/health.test.js
Normal file
101
deploy/performance-testing/test-suites/health.test.js
Normal file
@@ -0,0 +1,101 @@
|
||||
// Sub2API Health Check Performance Test
|
||||
// 健康检查接口性能测试
|
||||
|
||||
import http from 'k6/http';
|
||||
import { check, sleep, group } from 'k6';
|
||||
import { Rate, Trend, Counter } from 'k6/metrics';
|
||||
import { getBaseUrl } from '../common/utils.js';
|
||||
|
||||
// ============= 自定义指标 =============
|
||||
|
||||
const healthCheckDuration = new Trend('health_check_duration');
|
||||
const healthErrorRate = new Rate('health_errors');
|
||||
|
||||
// ============= 测试配置 =============
|
||||
|
||||
export const options = {
|
||||
scenarios: {
|
||||
health_load: {
|
||||
executor: 'ramping-vus',
|
||||
startVUs: 5,
|
||||
stages: [
|
||||
{ duration: '30s', target: 20 },
|
||||
{ duration: '2m', target: 50 },
|
||||
{ duration: '30s', target: 0 },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
thresholds: {
|
||||
'health_check_duration': ['p(95)<100', 'p(99)<200'],
|
||||
'health_errors': ['rate<0.001'],
|
||||
},
|
||||
};
|
||||
|
||||
// ============= 测试场景 =============
|
||||
|
||||
export default function () {
|
||||
group('Health Check', () => {
|
||||
const start = Date.now();
|
||||
|
||||
// 健康检查端点
|
||||
const res = http.get(`${getBaseUrl()}/health`, {
|
||||
tags: { name: 'health_check' },
|
||||
});
|
||||
|
||||
const duration = Date.now() - start;
|
||||
healthCheckDuration.add(duration);
|
||||
|
||||
check(res, {
|
||||
'Health: status is 200': (r) => r.status === 200,
|
||||
'Health: has status field': (r) => r.json('status') !== undefined,
|
||||
'Health: response time < 100ms': () => duration < 100,
|
||||
});
|
||||
|
||||
healthErrorRate.add(res.status !== 200);
|
||||
|
||||
// 可选:详细健康检查
|
||||
if (__VU % 10 === 0) {
|
||||
const detailedRes = http.get(`${getBaseUrl()}/health/detailed`, {
|
||||
tags: { name: 'health_detailed' },
|
||||
});
|
||||
check(detailedRes, {
|
||||
'Detailed Health: status is 200': (r) => r.status === 200,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 健康检查通常不需要太长的思考时间
|
||||
sleep(0.01);
|
||||
}
|
||||
|
||||
export function handleSummary(data) {
|
||||
return {
|
||||
'health-performance-report.json': JSON.stringify(data, null, 2),
|
||||
'health-performance-summary.txt': `
|
||||
================================================================================
|
||||
Sub2API Health Check 性能测试报告
|
||||
================================================================================
|
||||
|
||||
测试时间: ${new Date().toISOString()}
|
||||
测试持续: ${(data.state.testRunDurationMs / 1000 / 60).toFixed(2)} 分钟
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
核心指标
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
总请求数: ${data.metrics?.requests_total?.values?.count || 0}
|
||||
错误率: ${((data.metrics?.health_errors?.values?.rate || 0) * 100).toFixed(3)}%
|
||||
|
||||
平均响应时间: ${data.metrics?.health_check_duration?.values?.avg?.toFixed(2) || 0} ms
|
||||
P50 响应时间: ${data.metrics?.health_check_duration?.values?.['p(50)']?.toFixed(2) || 0} ms
|
||||
P95 响应时间: ${data.metrics?.health_check_duration?.values?.['p(95)']?.toFixed(2) || 0} ms
|
||||
P99 响应时间: ${data.metrics?.health_check_duration?.values?.['p(99)']?.toFixed(2) || 0} ms
|
||||
最大响应时间: ${data.metrics?.health_check_duration?.values?.max?.toFixed(2) || 0} ms
|
||||
|
||||
================================================================================
|
||||
测试完成
|
||||
================================================================================
|
||||
`,
|
||||
};
|
||||
}
|
||||
414
deploy/performance-testing/test-suites/mixed-workload.test.js
Normal file
414
deploy/performance-testing/test-suites/mixed-workload.test.js
Normal file
@@ -0,0 +1,414 @@
|
||||
// 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
|
||||
|
||||
================================================================================
|
||||
测试完成
|
||||
================================================================================
|
||||
`,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user