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:
Developer
2026-04-06 23:36:03 +08:00
parent 4d71566c0d
commit 349d783fd1
697 changed files with 24114 additions and 163282 deletions

View File

@@ -0,0 +1,235 @@
// Sub2API Test Scenarios Configuration
// 测试场景配置
// ============= 场景定义 =============
export const scenarios = {
// 基线测试场景
baseline: {
executor: 'ramping-vus',
startVUs: 10,
stages: [
{ duration: '2m', target: 10 }, // 预热
{ duration: '3m', target: 50 }, // 正常负载
{ duration: '1m', target: 0 }, // 冷却
],
tags: { test_type: 'baseline' },
},
// 负载测试场景
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 }, // 冷却
],
tags: { test_type: 'load' },
},
// 压力测试场景
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 }, // 冷却
],
tags: { test_type: 'stress' },
},
// 浸泡测试场景
soak: {
executor: 'constant-vus',
vus: 100,
duration: '8h',
tags: { test_type: 'soak' },
},
// 尖峰测试场景
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 }, // 冷却
],
tags: { test_type: 'spike' },
},
// Gateway 专用测试场景
gatewayLoad: {
executor: 'ramping-vus',
startVUs: 10,
stages: [
{ duration: '2m', target: 10 }, // 预热
{ duration: '5m', target: 100 }, // 正常负载
{ duration: '3m', target: 200 }, // 峰值负载
{ duration: '2m', target: 0 }, // 冷却
],
tags: { test_type: 'gateway_load' },
},
};
// ============= 场景权重 =============
// 混合场景中各接口的权重分布
export const scenarioWeights = {
// 典型用户行为分布
typicalUser: {
healthCheck: 0.20, // 健康检查(高频)
auth: 0.10, // 认证
apiKeys: 0.25, // API Key 管理
gateway: 0.40, // Gateway API核心业务
admin: 0.05, // 管理接口
},
// 高负载场景
highLoad: {
healthCheck: 0.10,
auth: 0.05,
apiKeys: 0.15,
gateway: 0.65, // Gateway 占比更高
admin: 0.05,
},
// 管理员场景
adminHeavy: {
healthCheck: 0.05,
auth: 0.15,
apiKeys: 0.30,
gateway: 0.30,
admin: 0.20,
},
};
// ============= 用户行为模拟 =============
export const userBehaviors = {
// 轻度用户
light: {
thinkTime: { min: 1, max: 5 }, // 秒
requestsPerSession: 10,
gatewayRequests: 3,
},
// 中度用户
medium: {
thinkTime: { min: 0.5, max: 3 },
requestsPerSession: 25,
gatewayRequests: 15,
},
// 重度用户
heavy: {
thinkTime: { min: 0.1, max: 1 },
requestsPerSession: 50,
gatewayRequests: 40,
},
};
// ============= Gateway 请求模板 =============
export const gatewayRequestTemplates = {
// OpenAI Chat Completion
openaiChat: {
method: 'POST',
path: '/v1/chat/completions',
body: {
model: 'gpt-3.5-turbo',
messages: [
{ role: 'system', content: 'You are a helpful assistant.' },
{ role: 'user', content: 'Hello, how are you?' }
],
max_tokens: 100,
temperature: 0.7,
},
},
// OpenAI Streaming
openaiStream: {
method: 'POST',
path: '/v1/chat/completions',
body: {
model: 'gpt-3.5-turbo',
messages: [
{ role: 'user', content: 'Write a short story about a robot.' }
],
max_tokens: 500,
stream: true,
},
},
// Claude Messages (Anthropic)
claudeMessages: {
method: 'POST',
path: '/v1/messages',
body: {
model: 'claude-3-5-haiku-20241022',
max_tokens: 100,
messages: [
{ role: 'user', content: 'Hello, how are you?' }
],
},
},
// Gemini Generate Content
geminiGenerate: {
method: 'POST',
path: '/v1beta/models/gemini-pro:generateContent',
body: {
contents: [
{
parts: [{ text: 'Hello, how are you?' }]
}
],
generationConfig: {
maxOutputTokens: 100,
},
},
},
};
// ============= 导出便捷函数 =============
/**
* 根据权重随机选择场景
*/
export function selectScenarioByWeight(scenarios) {
const entries = Object.entries(scenarios);
const totalWeight = entries.reduce((sum, [, weight]) => sum + weight, 0);
let random = Math.random() * totalWeight;
for (const [scenario, weight] of entries) {
random -= weight;
if (random <= 0) {
return scenario;
}
}
return entries[0][0];
}
/**
* 生成随机思考时间
*/
export function randomThinkTime(behavior) {
const { min, max } = userBehaviors[behavior]?.thinkTime || userBehaviors.medium.thinkTime;
return min + Math.random() * (max - min);
}
/**
* 获取指定类型的场景配置
*/
export function getScenario(name) {
return scenarios[name] || scenarios.baseline;
}

View File

@@ -0,0 +1,162 @@
// Sub2API Performance Thresholds
// 性能阈值定义
// ============= HTTP 请求阈值 =============
export const httpThresholds = {
// 全局 HTTP 请求阈值
httpReqFailed: ['rate<0.01'], // 错误率 < 1%
httpReqDuration: ['p(95)<1000'], // P95 < 1s
httpReqDurationP99: ['p(99)<3000'], // P99 < 3s
// 按路径的阈值
'http_req_duration{path:/health}': ['p(95)<100'], // 健康检查 P95 < 100ms
'http_req_duration{path:/api/v1/auth/login}': ['p(95)<500'], // 登录 P95 < 500ms
'http_req_duration{path:/api/v1/keys}': ['p(95)<300'], // API Key 列表 P95 < 300ms
};
// ============= Gateway 请求阈值 =============
export const gatewayThresholds = {
// Gateway 请求整体阈值
'gateway_req_duration{p(95)}': ['<2000'], // P95 < 2s
'gateway_req_duration{p(99)}': ['<5000'], // P99 < 5s
'gateway_req_failed': ['rate<0.05'], // 错误率 < 5%
// TTFT (Time To First Token)
'gateway_ttft{p(95)}': ['<3000'], // TTFT P95 < 3s
'gateway_ttft{p(99)}': ['<5000'], // TTFT P99 < 5s
// 按平台/模型分类
'gateway_latency_openai{p(95)}': ['<1500'], // OpenAI P95 < 1.5s
'gateway_latency_claude{p(95)}': ['<2000'], // Claude P95 < 2s
'gateway_latency_gemini{p(95)}': ['<1500'], // Gemini P95 < 1.5s
};
// ============= 数据库连接池阈值 =============
export const dbConnectionThresholds = {
// 连接数不应超过配置的 80%
dbConnectionsActive: ['value<0.8*max'], // 活跃连接 < 80%
dbConnectionsIdle: ['value>5'], // 空闲连接 > 5
};
// ============= Redis 连接池阈值 =============
export const redisConnectionThresholds = {
redisConnectionsTotal: ['value<0.8*pool_size'], // 总连接 < 80%
redisConnectionsIdle: ['value>3'], // 空闲连接 > 3
};
// ============= 速率限制阈值 =============
export const rateLimitThresholds = {
rateLimitHits: ['rate<0.1'], // 速率限制命中率应 < 10%
};
// ============= 缓存命中率 =============
export const cacheHitThresholds = {
cacheHitRate: ['rate>0.8'], // 缓存命中率 > 80%
cacheMissRate: ['rate<0.2'], // 缓存未命中率 < 20%
};
// ============= SLA 级别定义 =============
export const slaLevels = {
// 黄金 SLA: 最严格的要求
gold: {
availability: 0.9995, // 99.95%
latencyP95: 500, // ms
latencyP99: 1500, // ms
errorRate: 0.001, // 0.1%
ttftP99: 3000, // ms
},
// 白银 SLA: 标准要求
silver: {
availability: 0.99, // 99%
latencyP95: 1000, // ms
latencyP99: 3000, // ms
errorRate: 0.01, // 1%
ttftP99: 5000, // ms
},
// 青铜 SLA: 最低要求
bronze: {
availability: 0.95, // 95%
latencyP95: 2000, // ms
latencyP99: 5000, // ms
errorRate: 0.05, // 5%
ttftP99: 10000, // ms
},
};
// ============= 导出便捷函数 =============
/**
* 获取指定 SLA 级别的阈值配置
*/
export function getThresholdsForSLA(level = 'silver') {
const sla = slaLevels[level];
if (!sla) {
console.warn(`Unknown SLA level: ${level}, using silver`);
return slaLevels.silver;
}
return {
httpReqFailed: [`rate<${sla.errorRate}`],
httpReqDuration: [`p(95)<${sla.latencyP95}`],
httpReqDurationP99: [`p(99)<${sla.latencyP99}`],
};
}
/**
* 检查指标是否满足指定 SLA 级别
*/
export function meetsSLA(metrics, level = 'silver') {
const sla = slaLevels[level];
const checks = [
{
name: '可用性',
value: metrics.availability,
threshold: sla.availability,
pass: metrics.availability >= sla.availability,
},
{
name: 'P95 延迟',
value: metrics.latencyP95,
threshold: sla.latencyP95,
unit: 'ms',
pass: metrics.latencyP95 <= sla.latencyP95,
},
{
name: 'P99 延迟',
value: metrics.latencyP99,
threshold: sla.latencyP99,
unit: 'ms',
pass: metrics.latencyP99 <= sla.latencyP99,
},
{
name: '错误率',
value: metrics.errorRate,
threshold: sla.errorRate,
pass: metrics.errorRate <= sla.errorRate,
},
];
return {
level,
sla,
checks,
allPassed: checks.every(c => c.pass),
summary: checks.map(c => ({
name: c.name,
value: c.value,
threshold: c.threshold,
unit: c.unit || '%',
passed: c.pass,
})),
};
}

View File

@@ -0,0 +1,303 @@
// Sub2API Performance Test Utilities
// 性能测试工具函数
import http from 'k6/http';
import { Rate, Trend, Counter, Gauge } from 'k6/metrics';
import { config, getBaseUrl, getAdminCredentials } from '../config.js';
// 重新导出配置函数供其他模块使用
export { getBaseUrl, getAdminCredentials };
// ============= 自定义指标 =============
export const errorRate = new Rate('errors');
export const responseTimeTrend = new Trend('response_time');
export const throughputCounter = new Counter('requests_total');
// 按接口分类的指标
export const endpointMetrics = {
healthCheck: new Trend('health_check_duration'),
auth: new Trend('auth_duration'),
apiKeys: new Trend('api_keys_duration'),
gateway: new Trend('gateway_duration'),
admin: new Trend('admin_duration'),
};
// ============= 认证相关 =============
let authTokenCache = null;
let tokenExpiry = 0;
/**
* 获取认证令牌(带缓存)
*/
export function getAuthToken() {
const now = Date.now();
if (authTokenCache && now < tokenExpiry) {
return authTokenCache;
}
const credentials = getAdminCredentials();
const loginRes = http.post(
`${getBaseUrl()}/api/v1/auth/login`,
JSON.stringify({
email: credentials.email,
password: credentials.password,
}),
{
headers: { 'Content-Type': 'application/json' },
tags: { name: 'auth_login' },
}
);
if (loginRes.status !== 200) {
throw new Error(`Login failed: ${loginRes.status} ${loginRes.body}`);
}
const token = loginRes.json('token');
authTokenCache = token;
tokenExpiry = now + (55 * 60 * 1000); // 55分钟后过期
return token;
}
/**
* 获取带认证的请求头
*/
export function getAuthHeaders() {
return {
'Content-Type': 'application/json',
'Authorization': `Bearer ${getAuthToken()}`,
};
}
/**
* 清除认证缓存(用于测试认证失效场景)
*/
export function clearAuthCache() {
authTokenCache = null;
tokenExpiry = 0;
}
// ============= 请求辅助函数 =============
/**
* 通用 HTTP GET 请求
*/
export function httpGet(url, params = {}) {
const start = Date.now();
const defaultParams = {
headers: getAuthHeaders(),
tags: { name: 'http_get' },
};
const mergedParams = { ...defaultParams, ...params };
const res = http.get(url, mergedParams);
const duration = Date.now() - start;
recordMetrics(res, duration, params.tags?.name || 'http_get');
return res;
}
/**
* 通用 HTTP POST 请求
*/
export function httpPost(url, body, params = {}) {
const start = Date.now();
const defaultParams = {
headers: getAuthHeaders(),
tags: { name: 'http_post' },
};
const mergedParams = { ...defaultParams, ...params };
const res = http.post(url, JSON.stringify(body), mergedParams);
const duration = Date.now() - start;
recordMetrics(res, duration, params.tags?.name || 'http_post');
return res;
}
/**
* 记录请求指标
*/
function recordMetrics(response, duration, endpointName) {
// 记录响应时间趋势
responseTimeTrend.add(duration);
throughputCounter.add(1);
// 记录端点特定指标
if (endpointMetrics[endpointName]) {
endpointMetrics[endpointName].add(duration);
}
// 记录错误率(非 2xx 状态码)
const isSuccess = response.status >= 200 && response.status < 300;
errorRate.add(!isSuccess);
// Verbose 模式打印详情
if (config.mode.verbose || config.mode.recordDetailedRequests) {
console.log({
timestamp: new Date().toISOString(),
endpoint: endpointName,
status: response.status,
duration: `${duration}ms`,
url: response.url,
});
}
}
// ============= 检查响应 =============
/**
* 检查响应是否成功
*/
export function checkResponse(response, checks = {}) {
const results = {
status: response.status >= 200 && response.status < 300,
};
if (checks.jsonPath) {
try {
const value = response.json(checks.jsonPath);
results.jsonParsed = true;
if (checks.expectedValue !== undefined) {
results.valueMatch = value === checks.expectedValue;
}
} catch (e) {
results.jsonParsed = false;
}
}
if (checks.maxDuration) {
results.timingOk = response.timings.duration <= checks.maxDuration;
}
return results;
}
// ============= 睡眠/等待 =============
/**
* 随机睡眠(模拟用户思考时间)
*/
export function randomSleep(minSec = 0.1, maxSec = 1) {
const delay = minSec + Math.random() * (maxSec - minSec);
sleep(delay);
}
/**
* 睡眠指定秒数
*/
export function sleepSec(seconds) {
sleep(seconds);
}
// ============= 数据生成 =============
/**
* 生成随机字符串
*/
export function randomString(length = 10) {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
/**
* 生成测试用 API Key
*/
export function generateTestAPIKeyName() {
return `perf-test-${Date.now()}-${randomString(8)}`;
}
/**
* 从数组中随机选择一个元素
*/
export function randomChoice(array) {
return array[Math.floor(Math.random() * array.length)];
}
// ============= 批量操作 =============
/**
* 并发执行多个请求
*/
export async function batchRequests(requests) {
return http.batch(requests);
}
/**
* 创建批量请求列表
*/
export function createBatchRequest(urls, params = {}) {
return urls.map((url) => [
http.get,
url,
{
headers: getAuthHeaders(),
...params,
},
]);
}
// ============= 报告辅助 =============
/**
* 生成测试报告摘要
*/
export function generateReport(data) {
return {
timestamp: new Date().toISOString(),
duration: data.duration,
totalRequests: data.metrics?.requests_total?.values?.count || 0,
failedRequests: data.metrics?.errors?.values?.passes || 0,
passRate: ((data.metrics?.requests_total?.values?.count - data.metrics?.errors?.values?.passes) /
data.metrics?.requests_total?.values?.count * 100).toFixed(2) + '%',
avgResponseTime: (data.metrics?.response_time?.values?.avg || 0).toFixed(2) + 'ms',
p95ResponseTime: (data.metrics?.response_time?.values?.['p(95)'] || 0).toFixed(2) + 'ms',
p99ResponseTime: (data.metrics?.response_time?.values?.['p(99)'] || 0).toFixed(2) + 'ms',
maxResponseTime: (data.metrics?.response_time?.values?.max || 0).toFixed(2) + 'ms',
rps: (data.metrics?.requests_total?.values?.rate || 0).toFixed(2),
};
}
// ============= 错误处理 =============
/**
* 重试失败的请求
*/
export function retryRequest(method, url, body, options = {}, maxRetries = 3) {
const { retryDelay = 1, ...restOptions } = options;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
let res;
if (method === 'GET') {
res = http.get(url, restOptions);
} else if (method === 'POST') {
res = http.post(url, body, restOptions);
} else {
throw new Error(`Unsupported method: ${method}`);
}
if (res.status >= 200 && res.status < 300) {
return res;
}
if (attempt < maxRetries && res.status >= 500) {
sleep(retryDelay * (attempt + 1));
continue;
}
return res;
} catch (e) {
if (attempt === maxRetries) {
throw e;
}
sleep(retryDelay * (attempt + 1));
}
}
}