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:
235
deploy/performance-testing/common/scenarios.js
Normal file
235
deploy/performance-testing/common/scenarios.js
Normal 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;
|
||||
}
|
||||
162
deploy/performance-testing/common/thresholds.js
Normal file
162
deploy/performance-testing/common/thresholds.js
Normal 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,
|
||||
})),
|
||||
};
|
||||
}
|
||||
303
deploy/performance-testing/common/utils.js
Normal file
303
deploy/performance-testing/common/utils.js
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user