AIClient-2-API/tests/concurrent-test.js
hex2077 68719879c5 feat(architecture): 重构适配器注册机制并引入并发控制系统
建立可扩展的提供商适配器注册表,实现动态服务发现与插槽管理:

架构改进:
- 采用 Map 注册表替代 switch-case 硬编码,支持热插拔适配器
- 实现 acquireSlot/releaseSlot 机制,精确追踪活跃请求与等待队列
- 新增节点评分算法,综合考量并发数、队列长度、健康状态

核心能力:
- 支持并发限制与队列等待,避免单节点过载 (concurrencyLimit/queueLimit)
- 实现 Fallback 链式调用,429 错误自动切换备用凭证
- 添加请求级 IP 追踪,日志格式优化为 `clientIp:requestId`

配套更新:
- 管理界面新增并发/队列配置字段与 Grok 逆向提供商选项
- 用量查询服务扩展 Grok 支持,同步剩余查询次数 (固定总量 80)
- 新增并发测试脚本 (tests/concurrent-test.js),支持自定义并发数与 RPM 限制

配置项:
- GROK_COOKIE_TOKEN, GROK_CF_CLEARANCE, GROK_USER_AGENT, GROK_BASE_URL
2026-02-26 18:19:38 +08:00

454 lines
16 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 并发测试脚本
* 用于测试 API 服务器在高并发场景下的性能和稳定性
*
* 使用方法:
* node tests/concurrent-test.js [选项]
*
* 选项:
* --url <url> API 服务器地址 (默认: http://localhost:3000)
* --api-key <key> API 密钥 (默认: 123456)
* --concurrency <n> 并发数 (默认: 10)
* --requests <n> 总请求数 (默认: 100)
* --endpoint <path> 测试端点 (默认: /v1/chat/completions)
* --model <model> 模型名称 (默认: gpt-4)
* --stream 使用流式响应 (默认: false)
* --timeout <ms> 请求超时时间 (默认: 60000)
* --verbose 显示详细日志
*/
import http from 'http';
import https from 'https';
// 解析命令行参数
function parseArgs() {
const args = process.argv.slice(2);
const config = {
url: 'http://localhost:3000',
apiKey: '123456',
concurrency: 10,
totalRequests: 100,
rpm: 0,
endpoint: '/v1/chat/completions',
model: 'gpt-4',
stream: false,
timeout: 60000,
verbose: false
};
for (let i = 0; i < args.length; i++) {
switch (args[i]) {
case '--url':
config.url = args[++i];
break;
case '--api-key':
config.apiKey = args[++i];
break;
case '--concurrency':
config.concurrency = parseInt(args[++i], 10);
break;
case '--requests':
config.totalRequests = parseInt(args[++i], 10);
break;
case '--rpm':
config.rpm = parseInt(args[++i], 10);
break;
case '--endpoint':
config.endpoint = args[++i];
break;
case '--model':
config.model = args[++i];
break;
case '--stream':
config.stream = true;
break;
case '--timeout':
config.timeout = parseInt(args[++i], 10);
break;
case '--verbose':
config.verbose = true;
break;
case '--help':
console.log(`
并发测试脚本 - 测试 API 服务器性能
使用方法:
node tests/concurrent-test.js [选项]
选项:
--url <url> API 服务器地址 (默认: http://localhost:3000)
--api-key <key> API 密钥 (默认: 123456)
--concurrency <n> 并发数 (默认: 10)
--requests <n> 总请求数 (默认: 100)
--endpoint <path> 测试端点 (默认: /v1/chat/completions)
--model <model> 模型名称 (默认: gpt-4)
--stream 使用流式响应 (默认: false)
--timeout <ms> 请求超时时间 (默认: 60000)
--verbose 显示详细日志
--help 显示帮助信息
`);
process.exit(0);
}
}
return config;
}
// 统计数据
class Statistics {
constructor() {
this.completed = 0;
this.failed = 0;
this.responseTimes = [];
this.errors = {};
this.startTime = null;
this.endTime = null;
}
recordSuccess(responseTime) {
this.completed++;
this.responseTimes.push(responseTime);
}
recordFailure(error) {
this.failed++;
const errorKey = error.message || String(error);
this.errors[errorKey] = (this.errors[errorKey] || 0) + 1;
}
start() {
this.startTime = Date.now();
}
end() {
this.endTime = Date.now();
}
getReport() {
const totalTime = this.endTime - this.startTime;
const sortedTimes = [...this.responseTimes].sort((a, b) => a - b);
const percentile = (p) => {
if (sortedTimes.length === 0) return 0;
const index = Math.ceil((p / 100) * sortedTimes.length) - 1;
return sortedTimes[Math.max(0, index)];
};
const avg = sortedTimes.length > 0
? sortedTimes.reduce((a, b) => a + b, 0) / sortedTimes.length
: 0;
return {
totalRequests: this.completed + this.failed,
completed: this.completed,
failed: this.failed,
successRate: ((this.completed / (this.completed + this.failed)) * 100).toFixed(2) + '%',
totalTime: totalTime,
requestsPerSecond: ((this.completed + this.failed) / (totalTime / 1000)).toFixed(2),
responseTime: {
min: sortedTimes.length > 0 ? sortedTimes[0] : 0,
max: sortedTimes.length > 0 ? sortedTimes[sortedTimes.length - 1] : 0,
avg: avg.toFixed(2),
p50: percentile(50),
p90: percentile(90),
p95: percentile(95),
p99: percentile(99)
},
errors: this.errors
};
}
}
// 创建测试请求体
function createRequestBody(config, requestId) {
// OpenAI Chat Completions 格式
if (config.endpoint.includes('/chat/completions')) {
return JSON.stringify({
model: config.model,
messages: [
{
role: 'user',
content: `这是并发测试请求 #${requestId}。请简短回复"收到"。`
}
],
stream: config.stream,
max_tokens: 50
});
}
// OpenAI Responses 格式
if (config.endpoint.includes('/responses')) {
return JSON.stringify({
model: config.model,
input: `这是并发测试请求 #${requestId}。请简短回复"收到"。`,
stream: config.stream
});
}
// Claude Messages 格式
if (config.endpoint.includes('/messages')) {
return JSON.stringify({
model: config.model,
messages: [
{
role: 'user',
content: `这是并发测试请求 #${requestId}。请简短回复"收到"。`
}
],
stream: config.stream,
max_tokens: 50
});
}
// 默认格式
return JSON.stringify({
model: config.model,
messages: [
{
role: 'user',
content: `这是并发测试请求 #${requestId}。请简短回复"收到"。`
}
],
stream: config.stream,
max_tokens: 50
});
}
// 发送单个请求
function sendRequest(config, requestId) {
return new Promise((resolve, reject) => {
const startTime = Date.now();
const url = new URL(config.endpoint, config.url);
const isHttps = url.protocol === 'https:';
const client = isHttps ? https : http;
const requestBody = createRequestBody(config, requestId);
const options = {
hostname: url.hostname,
port: url.port || (isHttps ? 443 : 80),
path: url.pathname + url.search,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(requestBody),
'Authorization': `Bearer ${config.apiKey}`
},
timeout: config.timeout
};
const req = client.request(options, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
const responseTime = Date.now() - startTime;
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve({
success: true,
requestId,
statusCode: res.statusCode,
responseTime,
dataLength: data.length
});
} else {
reject({
success: false,
requestId,
statusCode: res.statusCode,
responseTime,
error: `HTTP ${res.statusCode}: ${data.substring(0, 200)}`
});
}
});
});
req.on('error', (error) => {
const responseTime = Date.now() - startTime;
reject({
success: false,
requestId,
responseTime,
error: error.code === 'ECONNREFUSED'
? `连接被拒绝 (${url.hostname}:${url.port || (isHttps ? 443 : 80)})`
: (error.message || error.code || 'Unknown error')
});
});
req.on('timeout', () => {
req.destroy();
const responseTime = Date.now() - startTime;
reject({
success: false,
requestId,
responseTime,
error: '请求超时'
});
});
req.write(requestBody);
req.end();
});
}
// 并发控制器
class ConcurrencyController {
constructor(concurrency) {
this.concurrency = concurrency;
this.running = 0;
this.queue = [];
}
async run(task) {
return new Promise((resolve, reject) => {
this.queue.push({ task, resolve, reject });
this.processQueue();
});
}
async processQueue() {
while (this.running < this.concurrency && this.queue.length > 0) {
const { task, resolve, reject } = this.queue.shift();
this.running++;
task()
.then(resolve)
.catch(reject)
.finally(() => {
this.running--;
this.processQueue();
});
}
}
}
// 进度条显示
function showProgress(current, total, stats) {
const percentage = ((current / total) * 100).toFixed(1);
const barLength = 30;
const filled = Math.round((current / total) * barLength);
const bar = '█'.repeat(filled) + '░'.repeat(barLength - filled);
process.stdout.write(`\r[${bar}] ${percentage}% (${current}/${total}) | 成功: ${stats.completed} | 失败: ${stats.failed}`);
}
// 主函数
async function main() {
const config = parseArgs();
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ API 并发测试脚本 ║');
console.log('╠════════════════════════════════════════════════════════════╣');
console.log(`║ 目标地址: ${config.url.padEnd(47)}`);
console.log(`║ 测试端点: ${config.endpoint.padEnd(47)}`);
console.log(`║ 并发数量: ${String(config.concurrency).padEnd(47)}`);
console.log(`║ 总请求数: ${String(config.totalRequests).padEnd(47)}`);
console.log(`║ 模型名称: ${config.model.padEnd(47)}`);
console.log(`║ 流式响应: ${String(config.stream).padEnd(47)}`);
console.log(`║ 超时时间: ${(config.timeout + 'ms').padEnd(47)}`);
console.log('╚════════════════════════════════════════════════════════════╝');
console.log('');
const stats = new Statistics();
const controller = new ConcurrencyController(config.concurrency);
console.log('开始测试...\n');
stats.start();
const tasks = [];
for (let i = 1; i <= config.totalRequests; i++) {
const requestId = i;
// 如果设置了 RPM计算延迟时间
if (config.rpm > 0) {
const delay = (60000 / config.rpm) * (i - 1);
tasks.push(
new Promise(resolve => setTimeout(resolve, delay))
.then(() => controller.run(() => sendRequest(config, requestId)))
.then((result) => {
stats.recordSuccess(result.responseTime);
if (config.verbose) {
console.log(`\n[成功] 请求 #${result.requestId} - ${result.responseTime}ms - ${result.dataLength} bytes`);
}
})
.catch((result) => {
stats.recordFailure(new Error(result.error));
if (config.verbose) {
console.log(`\n[失败] 请求 #${result.requestId} - ${result.error}`);
}
})
.finally(() => {
showProgress(stats.completed + stats.failed, config.totalRequests, stats);
})
);
} else {
tasks.push(
controller.run(() => sendRequest(config, requestId))
.then((result) => {
stats.recordSuccess(result.responseTime);
if (config.verbose) {
console.log(`\n[成功] 请求 #${result.requestId} - ${result.responseTime}ms - ${result.dataLength} bytes`);
}
})
.catch((result) => {
stats.recordFailure(new Error(result.error));
if (config.verbose) {
console.log(`\n[失败] 请求 #${result.requestId} - ${result.error}`);
}
})
.finally(() => {
showProgress(stats.completed + stats.failed, config.totalRequests, stats);
})
);
}
}
await Promise.all(tasks);
stats.end();
console.log('\n\n');
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ 测试结果报告 ║');
console.log('╚════════════════════════════════════════════════════════════╝');
const report = stats.getReport();
console.log('\n📊 总体统计:');
console.log(` 总请求数: ${report.totalRequests}`);
console.log(` 成功请求: ${report.completed}`);
console.log(` 失败请求: ${report.failed}`);
console.log(` 成功率: ${report.successRate}`);
console.log(` 总耗时: ${report.totalTime}ms`);
console.log(` 吞吐量: ${report.requestsPerSecond} req/s`);
console.log('\n⏱ 响应时间统计 (ms):');
console.log(` 最小值: ${report.responseTime.min}`);
console.log(` 最大值: ${report.responseTime.max}`);
console.log(` 平均值: ${report.responseTime.avg}`);
console.log(` P50: ${report.responseTime.p50}`);
console.log(` P90: ${report.responseTime.p90}`);
console.log(` P95: ${report.responseTime.p95}`);
console.log(` P99: ${report.responseTime.p99}`);
if (Object.keys(report.errors).length > 0) {
console.log('\n❌ 错误统计:');
for (const [error, count] of Object.entries(report.errors)) {
console.log(` ${error}: ${count}`);
}
}
console.log('\n════════════════════════════════════════════════════════════════');
// 返回退出码
process.exit(report.failed > 0 ? 1 : 0);
}
// 运行主函数
main().catch((error) => {
console.error('测试脚本执行失败:', error);
process.exit(1);
});