AIClient-2-API/tests/concurrent-test.js
hex2077 13ed2087d2 fix(provider-pool): 修复并发选点时的竞争条件并改进评分算法
- 将链式 Promise 锁改为标志位锁,解决同一微任务循环内的并发问题
- 引入自增序列号确保毫秒级并发下的原子排序,避免节点重复选择
- 优化节点评分算法,平衡 lastUsedTime、usageCount 和 selectionSeq
- 增加并发测试脚本,支持压力测试和性能统计
2026-01-24 16:30:57 +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);
});