Files
tokens-reef/tests/performance/gateway-load-test.js
Developer 349d783fd1 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
2026-04-06 23:36:03 +08:00

461 lines
16 KiB
JavaScript

// Sub2API Gateway 负载测试脚本
// 使用 k6 进行性能测试
// 运行: k6 run --env BASE_URL=http://localhost:8080 --env API_KEY=your_key gateway-load-test.js
import http from 'k6/http';
import { check, sleep, group } from 'k6';
import { Rate, Trend, Counter, Gauge } from 'k6/metrics';
import { randomIntBetween } from 'https://jslib.k6.io/k6-utils/1.2.0/index.js';
// ============================================
// 自定义指标定义
// ============================================
const errorRate = new Rate('error_rate');
const successRate = new Rate('success_rate');
const responseTime = new Trend('response_time');
const responseTimeP95 = new Trend('response_time_p95');
const responseTimeP99 = new Trend('response_time_p99');
const requestCounter = new Counter('total_requests');
const successCounter = new Counter('success_requests');
const failureCounter = new Counter('failure_requests');
const activeVUs = new Gauge('active_vus');
// ============================================
// 测试配置
// ============================================
export const options = {
// 测试阶段配置
stages: [
{ duration: '2m', target: 50 }, // 渐进加压: 0 -> 50 VU
{ duration: '5m', target: 50 }, // 稳定期: 保持50 VU
{ duration: '2m', target: 100 }, // 峰值测试: 50 -> 100 VU
{ duration: '5m', target: 100 }, // 峰值稳定: 保持100 VU
{ duration: '2m', target: 200 }, // 压力测试: 100 -> 200 VU
{ duration: '3m', target: 200 }, // 压力稳定: 保持200 VU
{ duration: '2m', target: 0 }, // 减压: 200 -> 0 VU
],
// 性能阈值配置
thresholds: {
// HTTP请求持续时间
http_req_duration: ['p(50)<100', 'p(95)<500', 'p(99)<2000'],
http_req_failed: ['rate<0.01'], // 错误率 < 1%
error_rate: ['rate<0.001'], // 自定义错误率 < 0.1%
success_rate: ['rate>0.99'], // 成功率 > 99%
// 特定端点的阈值
'response_time{endpoint:models}': ['p(95)<200'],
'response_time{endpoint:chat_completions}': ['p(95)<5000'],
'response_time{endpoint:messages}': ['p(95)<5000'],
},
// 其他配置
discardResponseBodies: true, // 丢弃响应体以节省内存
maxRedirects: 4,
userAgent: 'Sub2API-LoadTest/1.0',
};
// ============================================
// 环境变量配置
// ============================================
const BASE_URL = __ENV.BASE_URL || 'http://localhost:8080';
const API_KEY = __ENV.API_KEY || 'sk-test-key';
const ENABLE_STREAMING = __ENV.ENABLE_STREAMING === 'true';
// ============================================
// 测试数据
// ============================================
const TEST_MODELS = [
'claude-3-opus-20240229',
'claude-3-sonnet-20240229',
'claude-3-haiku-20240307',
'gpt-4-turbo-preview',
'gpt-4',
'gpt-3.5-turbo',
];
const TEST_MESSAGES = [
[{ role: 'user', content: 'Hello, how are you?' }],
[{ role: 'user', content: 'What is the weather like today?' }],
[{ role: 'user', content: 'Explain quantum computing in simple terms.' }],
[{ role: 'user', content: 'Write a short poem about technology.' }],
[{ role: 'system', content: 'You are a helpful assistant.' }, { role: 'user', content: 'Help me with my homework.' }],
];
// ============================================
// 设置函数 - 每个VU执行一次
// ============================================
export function setup() {
console.log(`Starting load test against ${BASE_URL}`);
console.log(`API Key: ${API_KEY.substring(0, 10)}...`);
// 验证API Key有效性
const res = http.get(`${BASE_URL}/v1/models`, {
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
});
if (res.status !== 200) {
console.error(`API Key validation failed: ${res.status}`);
return { valid: false };
}
console.log('API Key validated successfully');
return { valid: true, startTime: new Date().toISOString() };
}
// ============================================
// 主测试函数 - 每个VU循环执行
// ============================================
export default function (data) {
if (!data.valid) {
console.error('Invalid setup, skipping test');
return;
}
activeVUs.add(__VU);
// 随机选择测试场景 (权重分配)
const scenario = randomIntBetween(1, 100);
if (scenario <= 40) {
// 40% - 模型列表查询 (轻量级)
testListModels();
} else if (scenario <= 70) {
// 30% - Chat Completions
testChatCompletions();
} else if (scenario <= 90) {
// 20% - Messages (Claude API)
testMessages();
} else {
// 10% - 混合操作
testMixedOperations();
}
// 随机休眠 100ms - 1000ms 模拟真实用户行为
sleep(randomIntBetween(1, 10) / 10);
}
// ============================================
// 测试场景: 获取模型列表
// ============================================
function testListModels() {
group('List Models', () => {
const startTime = new Date();
const res = http.get(`${BASE_URL}/v1/models`, {
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
tags: { endpoint: 'models' },
});
const duration = new Date() - startTime;
responseTime.add(duration, { endpoint: 'models' });
requestCounter.add(1, { endpoint: 'models' });
const success = check(res, {
'models_status_is_200': (r) => r.status === 200,
'models_response_has_data': (r) => {
try {
const body = JSON.parse(r.body);
return body.data && Array.isArray(body.data);
} catch (e) {
return false;
}
},
'models_response_time_ok': () => duration < 500,
});
if (success) {
successCounter.add(1, { endpoint: 'models' });
successRate.add(true);
errorRate.add(false);
} else {
failureCounter.add(1, { endpoint: 'models' });
successRate.add(false);
errorRate.add(true);
console.error(`Models request failed: ${res.status}, body: ${res.body}`);
}
});
}
// ============================================
// 测试场景: Chat Completions
// ============================================
function testChatCompletions() {
group('Chat Completions', () => {
const model = TEST_MODELS[randomIntBetween(0, TEST_MODELS.length - 1)];
const messages = TEST_MESSAGES[randomIntBetween(0, TEST_MESSAGES.length - 1)];
const payload = JSON.stringify({
model: model,
messages: messages,
max_tokens: randomIntBetween(50, 200),
temperature: 0.7,
stream: ENABLE_STREAMING,
});
const startTime = new Date();
const res = http.post(`${BASE_URL}/v1/chat/completions`, payload, {
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
tags: { endpoint: 'chat_completions' },
timeout: '30s',
});
const duration = new Date() - startTime;
responseTime.add(duration, { endpoint: 'chat_completions' });
requestCounter.add(1, { endpoint: 'chat_completions' });
const success = check(res, {
'chat_status_is_200': (r) => r.status === 200,
'chat_response_valid': (r) => {
try {
const body = JSON.parse(r.body);
return body.choices && body.choices.length > 0;
} catch (e) {
return false;
}
},
'chat_response_time_acceptable': () => duration < 10000, // 10s
});
if (success) {
successCounter.add(1, { endpoint: 'chat_completions' });
successRate.add(true);
errorRate.add(false);
} else {
failureCounter.add(1, { endpoint: 'chat_completions' });
successRate.add(false);
errorRate.add(true);
console.error(`Chat completions failed: ${res.status}, duration: ${duration}ms`);
}
});
}
// ============================================
// 测试场景: Messages (Claude API)
// ============================================
function testMessages() {
group('Messages', () => {
const model = TEST_MODELS[randomIntBetween(0, 2)]; // Claude models only
const messages = TEST_MESSAGES[randomIntBetween(0, TEST_MESSAGES.length - 1)];
const payload = JSON.stringify({
model: model,
messages: messages,
max_tokens: randomIntBetween(50, 200),
});
const startTime = new Date();
const res = http.post(`${BASE_URL}/v1/messages`, payload, {
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
'anthropic-version': '2023-06-01',
},
tags: { endpoint: 'messages' },
timeout: '30s',
});
const duration = new Date() - startTime;
responseTime.add(duration, { endpoint: 'messages' });
requestCounter.add(1, { endpoint: 'messages' });
const success = check(res, {
'messages_status_is_200': (r) => r.status === 200,
'messages_response_valid': (r) => {
try {
const body = JSON.parse(r.body);
return body.content && body.content.length > 0;
} catch (e) {
return false;
}
},
'messages_response_time_acceptable': () => duration < 10000,
});
if (success) {
successCounter.add(1, { endpoint: 'messages' });
successRate.add(true);
errorRate.add(false);
} else {
failureCounter.add(1, { endpoint: 'messages' });
successRate.add(false);
errorRate.add(true);
console.error(`Messages request failed: ${res.status}`);
}
});
}
// ============================================
// 测试场景: 混合操作
// ============================================
function testMixedOperations() {
group('Mixed Operations', () => {
// 1. 获取模型列表
testListModels();
sleep(0.5);
// 2. 随机选择一个API调用
if (randomIntBetween(1, 2) === 1) {
testChatCompletions();
} else {
testMessages();
}
});
}
// ============================================
// 拆卸函数 - 测试结束时执行
// ============================================
export function teardown(data) {
console.log('Load test completed');
console.log(`Start time: ${data.startTime}`);
console.log(`End time: ${new Date().toISOString()}`);
}
// ============================================
// 性能测试报告处理器
// ============================================
export function handleSummary(data) {
return {
'stdout': textSummary(data, { indent: ' ', enableColors: true }),
'summary.json': JSON.stringify({
metrics: {
http_req_duration: data.metrics.http_req_duration,
http_req_failed: data.metrics.http_req_failed,
error_rate: data.metrics.error_rate,
success_rate: data.metrics.success_rate,
},
thresholds: data.thresholds,
state: data.state,
}, null, 2),
'summary.html': generateHTMLReport(data),
};
}
// 生成文本摘要
function textSummary(data, options) {
const indent = options.indent || '';
const colors = options.enableColors !== false;
const red = colors ? '\x1b[31m' : '';
const green = colors ? '\x1b[32m' : '';
const yellow = colors ? '\x1b[33m' : '';
const reset = colors ? '\x1b[0m' : '';
let summary = '';
summary += `${indent}╔════════════════════════════════════════════════════════╗\n`;
summary += `${indent}║ Sub2API 负载测试报告 ║\n`;
summary += `${indent}╚════════════════════════════════════════════════════════╝\n\n`;
summary += `${indent}测试持续时间: ${(data.state.testRunDurationMs / 1000).toFixed(2)}s\n`;
summary += `${indent}虚拟用户数: ${data.metrics.vus ? data.metrics.vus.max : 'N/A'}\n`;
summary += `${indent}总请求数: ${data.metrics.http_reqs ? data.metrics.http_reqs.count : 'N/A'}\n\n`;
// 响应时间统计
if (data.metrics.http_req_duration) {
const dur = data.metrics.http_req_duration;
summary += `${indent}响应时间统计:\n`;
summary += `${indent} 平均: ${dur.avg.toFixed(2)}ms\n`;
summary += `${indent} 最小: ${dur.min.toFixed(2)}ms\n`;
summary += `${indent} 最大: ${dur.max.toFixed(2)}ms\n`;
summary += `${indent} P50: ${dur.med.toFixed(2)}ms\n`;
summary += `${indent} P95: ${dur['p(95)'].toFixed(2)}ms\n`;
summary += `${indent} P99: ${dur['p(99)'].toFixed(2)}ms\n\n`;
}
// 错误率
if (data.metrics.http_req_failed) {
const failRate = data.metrics.http_req_failed.rate * 100;
const color = failRate > 1 ? red : (failRate > 0.1 ? yellow : green);
summary += `${indent}错误率: ${color}${failRate.toFixed(4)}%${reset}\n`;
}
// 阈值检查结果
summary += `\n${indent}阈值检查:\n`;
for (const [name, result] of Object.entries(data.thresholds || {})) {
const color = result.ok ? green : red;
summary += `${indent} ${color}${result.ok ? '✓' : '✗'} ${name}${reset}\n`;
}
return summary;
}
// 生成HTML报告
function generateHTMLReport(data) {
return `
<!DOCTYPE html>
<html>
<head>
<title>Sub2API 负载测试报告</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; background: #f5f5f5; }
.container { max-width: 1200px; margin: 0 auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
h1 { color: #333; border-bottom: 3px solid #4CAF50; padding-bottom: 10px; }
.metric { display: inline-block; margin: 10px 20px 10px 0; padding: 15px; background: #f9f9f9; border-radius: 4px; }
.metric-label { font-size: 12px; color: #666; text-transform: uppercase; }
.metric-value { font-size: 24px; font-weight: bold; color: #333; }
.success { color: #4CAF50; }
.warning { color: #FF9800; }
.error { color: #f44336; }
table { width: 100%; border-collapse: collapse; margin: 20px 0; }
th, td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }
th { background: #4CAF50; color: white; }
tr:hover { background: #f5f5f5; }
</style>
</head>
<body>
<div class="container">
<h1>Sub2API 负载测试报告</h1>
<p>生成时间: ${new Date().toLocaleString()}</p>
<div class="metrics">
<div class="metric">
<div class="metric-label">总请求数</div>
<div class="metric-value">${data.metrics.http_reqs ? data.metrics.http_reqs.count.toLocaleString() : 'N/A'}</div>
</div>
<div class="metric">
<div class="metric-label">平均响应时间</div>
<div class="metric-value">${data.metrics.http_req_duration ? data.metrics.http_req_duration.avg.toFixed(2) : 'N/A'}ms</div>
</div>
<div class="metric">
<div class="metric-label">P95 响应时间</div>
<div class="metric-value">${data.metrics.http_req_duration ? data.metrics.http_req_duration['p(95)'].toFixed(2) : 'N/A'}ms</div>
</div>
<div class="metric">
<div class="metric-label">错误率</div>
<div class="metric-value ${data.metrics.http_req_failed && data.metrics.http_req_failed.rate > 0.01 ? 'error' : 'success'}">
${data.metrics.http_req_failed ? (data.metrics.http_req_failed.rate * 100).toFixed(4) : 'N/A'}%
</div>
</div>
</div>
<h2>阈值检查结果</h2>
<table>
<tr>
<th>阈值</th>
<th>状态</th>
</tr>
${Object.entries(data.thresholds || {}).map(([name, result]) => `
<tr>
<td>${name}</td>
<td class="${result.ok ? 'success' : 'error'}">${result.ok ? '✓ 通过' : '✗ 失败'}</td>
</tr>
`).join('')}
</table>
</div>
</body>
</html>
`;
}