mirror of
https://github.com/ZeroCatDev/ClassworksKV.git
synced 2025-07-01 20:09:23 +00:00
Integrate OpenTelemetry instrumentation into app.js
, add global and method-based rate limiting middleware, and enhance CORS configuration. Update package.json
to include new dependencies for OpenTelemetry and rate limiting tests.
This commit is contained in:
parent
1b05d73405
commit
6897a082e7
22
app.js
22
app.js
@ -1,3 +1,4 @@
|
||||
import "./instrumentation.js";
|
||||
// import createError from "http-errors";
|
||||
import express from "express";
|
||||
import { join, dirname } from "path";
|
||||
@ -8,6 +9,11 @@ import bodyParser from "body-parser";
|
||||
import errorHandler from "./middleware/errorHandler.js";
|
||||
import errors from "./utils/errors.js";
|
||||
import { initReadme, getReadmeValue } from "./utils/siteinfo.js";
|
||||
import {
|
||||
globalLimiter,
|
||||
apiLimiter,
|
||||
methodBasedRateLimiter,
|
||||
} from "./middleware/rateLimiter.js";
|
||||
|
||||
import kvRouter from "./routes/kv.js";
|
||||
|
||||
@ -15,7 +21,12 @@ var app = express();
|
||||
|
||||
import cors from "cors";
|
||||
app.options("*", cors());
|
||||
app.use(cors());
|
||||
app.use(
|
||||
cors({
|
||||
exposedHeaders: ["ratelimit-policy", "retry-after", "ratelimit"], // 告诉浏览器这些响应头可以暴露
|
||||
})
|
||||
);
|
||||
app.disable("x-powered-by");
|
||||
|
||||
// 获取当前文件的目录路径
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
@ -24,6 +35,9 @@ const __dirname = dirname(__filename);
|
||||
// 初始化 readme
|
||||
initReadme();
|
||||
|
||||
// 应用全局限速
|
||||
app.use(globalLimiter);
|
||||
|
||||
// view engine setup
|
||||
app.set("views", join(__dirname, "views"));
|
||||
app.set("view engine", "ejs");
|
||||
@ -60,7 +74,7 @@ app.use((req, res, next) => {
|
||||
app.get("/", (req, res) => {
|
||||
res.render("index.ejs", { readmeValue: getReadmeValue() });
|
||||
});
|
||||
app.get("/check", (req, res) => {
|
||||
app.get("/check", apiLimiter, (req, res) => {
|
||||
res.json({
|
||||
status: "success",
|
||||
message: "API is running",
|
||||
@ -68,8 +82,8 @@ app.get("/check", (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// Mount the KV store router
|
||||
app.use("/", kvRouter);
|
||||
// Mount the KV store router with method-based rate limiting
|
||||
app.use("/", methodBasedRateLimiter, kvRouter);
|
||||
|
||||
// 兜底404路由 - 处理所有未匹配的路由
|
||||
app.use((req, res, next) => {
|
||||
|
42
instrumentation.js
Normal file
42
instrumentation.js
Normal file
@ -0,0 +1,42 @@
|
||||
import "dotenv/config";
|
||||
import { NodeSDK } from "@opentelemetry/sdk-node";
|
||||
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
|
||||
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
|
||||
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
|
||||
import { resourceFromAttributes } from "@opentelemetry/resources";
|
||||
import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions";
|
||||
if (process.env.AXIOM_TOKEN && process.env.AXIOM_DATASET) {
|
||||
// Initialize OTLP trace exporter with the endpoint URL and headers
|
||||
// Initialize OTLP trace exporter with the endpoint URL and headers
|
||||
const traceExporter = new OTLPTraceExporter({
|
||||
url: "https://api.axiom.co/v1/traces",
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.AXIOM_TOKEN}`,
|
||||
"X-Axiom-Dataset": process.env.AXIOM_DATASET,
|
||||
},
|
||||
});
|
||||
|
||||
const resourceAttributes = {
|
||||
[SemanticResourceAttributes.SERVICE_NAME]: "node traces",
|
||||
};
|
||||
|
||||
const resource = resourceFromAttributes(resourceAttributes);
|
||||
|
||||
// Configuring the OpenTelemetry Node SDK
|
||||
const sdk = new NodeSDK({
|
||||
// Adding a BatchSpanProcessor to batch and send traces
|
||||
spanProcessor: new BatchSpanProcessor(traceExporter),
|
||||
|
||||
// Registering the resource to the SDK
|
||||
resource: resource,
|
||||
|
||||
// Adding auto-instrumentations to automatically collect trace data
|
||||
instrumentations: [getNodeAutoInstrumentations()],
|
||||
});
|
||||
|
||||
console.log("✅成功加载 Axiom 遥测");
|
||||
// Starting the OpenTelemetry SDK to begin collecting telemetry data
|
||||
sdk.start();
|
||||
} else {
|
||||
console.log("❌未设置 Axiom 遥测");
|
||||
}
|
93
middleware/rateLimiter.js
Normal file
93
middleware/rateLimiter.js
Normal file
@ -0,0 +1,93 @@
|
||||
import rateLimit from "express-rate-limit";
|
||||
|
||||
// 获取客户端真实IP的函数
|
||||
export const getClientIp = (req) => {
|
||||
return (
|
||||
req.headers["x-forwarded-for"] ||
|
||||
req.connection.remoteAddress ||
|
||||
req.socket.remoteAddress ||
|
||||
req.connection.socket?.remoteAddress ||
|
||||
"0.0.0.0"
|
||||
);
|
||||
};
|
||||
|
||||
// 配置全局限速中间件
|
||||
export const globalLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15分钟
|
||||
limit: 200, // 每个IP在windowMs时间内最多允许200个请求
|
||||
standardHeaders: "draft-7", // 返回标准的RateLimit头信息
|
||||
legacyHeaders: false, // 禁用X-RateLimit-*头
|
||||
message: "请求过于频繁,请稍后再试",
|
||||
keyGenerator: getClientIp, // 使用真实IP作为限速键
|
||||
skipSuccessfulRequests: false, // 成功的请求也计入限制
|
||||
skipFailedRequests: false, // 失败的请求也计入限制
|
||||
});
|
||||
|
||||
// API限速器
|
||||
export const apiLimiter = rateLimit({
|
||||
windowMs: 1 * 60 * 1000, // 1分钟
|
||||
limit: 20, // 每个IP在windowMs时间内最多允许20个请求
|
||||
standardHeaders: "draft-7",
|
||||
legacyHeaders: false,
|
||||
message: "API请求过于频繁,请稍后再试",
|
||||
keyGenerator: getClientIp,
|
||||
skipSuccessfulRequests: false,
|
||||
skipFailedRequests: false,
|
||||
});
|
||||
|
||||
// 写操作限速器(更严格)
|
||||
export const writeLimiter = rateLimit({
|
||||
windowMs: 1 * 60 * 1000, // 1分钟
|
||||
limit: 10, // 每个IP在windowMs时间内最多允许10个写操作
|
||||
standardHeaders: "draft-7",
|
||||
legacyHeaders: false,
|
||||
message: "写操作请求过于频繁,请稍后再试",
|
||||
keyGenerator: getClientIp,
|
||||
skipSuccessfulRequests: false,
|
||||
skipFailedRequests: false,
|
||||
});
|
||||
|
||||
// 删除操作限速器(最严格)
|
||||
export const deleteLimiter = rateLimit({
|
||||
windowMs: 1 * 60 * 1000, // 5分钟
|
||||
limit: 1, // 每个IP在windowMs时间内最多允许5个删除操作
|
||||
standardHeaders: "draft-7",
|
||||
legacyHeaders: false,
|
||||
message: "删除操作请求过于频繁,请稍后再试",
|
||||
keyGenerator: getClientIp,
|
||||
skipSuccessfulRequests: false,
|
||||
skipFailedRequests: false,
|
||||
});
|
||||
|
||||
// 认证相关路由限速器(防止暴力破解)
|
||||
export const authLimiter = rateLimit({
|
||||
windowMs: 30 * 60 * 1000, // 30分钟
|
||||
limit: 5, // 每个IP在windowMs时间内最多允许5次认证尝试
|
||||
standardHeaders: "draft-7",
|
||||
legacyHeaders: false,
|
||||
message: "认证请求过于频繁,请30分钟后再试",
|
||||
keyGenerator: getClientIp,
|
||||
skipSuccessfulRequests: true, // 成功的认证不计入限制
|
||||
skipFailedRequests: false, // 失败的认证计入限制
|
||||
});
|
||||
|
||||
// 创建一个路由处理中间件,根据HTTP方法应用不同的限速器
|
||||
export const methodBasedRateLimiter = (req, res, next) => {
|
||||
// 根据HTTP方法应用不同限速
|
||||
if (req.method === "GET") {
|
||||
// 读操作使用普通API限速
|
||||
return apiLimiter(req, res, next);
|
||||
} else if (
|
||||
req.method === "POST" ||
|
||||
req.method === "PUT" ||
|
||||
req.method === "PATCH"
|
||||
) {
|
||||
// 写操作使用更严格的限速
|
||||
return writeLimiter(req, res, next);
|
||||
} else if (req.method === "DELETE") {
|
||||
// 删除操作使用最严格的限速
|
||||
return deleteLimiter(req, res, next);
|
||||
}
|
||||
// 其他方法使用API限速
|
||||
return apiLimiter(req, res, next);
|
||||
};
|
15
package.json
15
package.json
@ -7,11 +7,22 @@
|
||||
"prisma": "prisma generate",
|
||||
"prisma:pull": "prisma db pull",
|
||||
"dev": "NODE_ENV=development nodemon node .bin/www",
|
||||
"setup": "node ./scripts/setup.js"
|
||||
"setup": "node ./scripts/setup.js",
|
||||
"test:rate-limit": "node ./scripts/test-rate-limit.js",
|
||||
"test:stress": "node ./scripts/stress-test.js",
|
||||
"test:distributed": "node ./scripts/distributed-test.js",
|
||||
"test:all-limits": "node ./scripts/run-all-tests.js"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@opentelemetry/auto-instrumentations-node": "^0.58.1",
|
||||
"@opentelemetry/exporter-trace-otlp-proto": "^0.200.0",
|
||||
"@opentelemetry/resources": "^2.0.0",
|
||||
"@opentelemetry/sdk-node": "^0.200.0",
|
||||
"@opentelemetry/sdk-trace-base": "^2.0.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.33.0",
|
||||
"@prisma/client": "6.7.0",
|
||||
"axios": "^1.9.0",
|
||||
"body-parser": "^1.20.3",
|
||||
"cookie-parser": "~1.4.4",
|
||||
"cors": "^2.8.5",
|
||||
@ -19,8 +30,8 @@
|
||||
"dotenv": "^16.5.0",
|
||||
"ejs": "^3.1.10",
|
||||
"express": "~4.16.1",
|
||||
"express-rate-limit": "^7.5.0",
|
||||
"http-errors": "~1.6.3",
|
||||
"jade": "~1.11.0",
|
||||
"morgan": "~1.9.1",
|
||||
"uuid": "^11.1.0"
|
||||
},
|
||||
|
1854
pnpm-lock.yaml
generated
1854
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
75
scripts/README.md
Normal file
75
scripts/README.md
Normal file
@ -0,0 +1,75 @@
|
||||
# 接口限速测试脚本
|
||||
|
||||
这个目录包含了用于测试API接口限速功能的脚本。
|
||||
|
||||
## 前置条件
|
||||
|
||||
在运行测试脚本之前,请确保安装了所需的依赖:
|
||||
|
||||
```bash
|
||||
npm install axios
|
||||
```
|
||||
|
||||
## 可用测试脚本
|
||||
|
||||
### 1. 功能测试 (test-rate-limit.js)
|
||||
|
||||
测试不同类型的限速功能是否正常工作,包括全局限速、API限速、写操作限速和删除操作限速。
|
||||
|
||||
```bash
|
||||
npm run test:rate-limit
|
||||
# 或直接运行
|
||||
node scripts/test-rate-limit.js
|
||||
```
|
||||
|
||||
### 2. 压力测试 (stress-test.js)
|
||||
|
||||
对指定端点进行高并发请求,测试限速在高负载下的表现。
|
||||
|
||||
```bash
|
||||
npm run test:stress
|
||||
# 或直接运行
|
||||
node scripts/stress-test.js
|
||||
```
|
||||
|
||||
### 3. 分布式测试 (distributed-test.js)
|
||||
|
||||
模拟多个不同IP地址的请求,测试基于IP的限速是否有效。
|
||||
|
||||
```bash
|
||||
npm run test:distributed
|
||||
# 或直接运行
|
||||
node scripts/distributed-test.js
|
||||
```
|
||||
|
||||
### 4. 运行所有测试 (run-all-tests.js)
|
||||
|
||||
按顺序运行所有测试,并在测试之间添加适当的延迟以重置限速计数器。
|
||||
|
||||
```bash
|
||||
npm run test:all-limits
|
||||
# 或直接运行
|
||||
node scripts/run-all-tests.js
|
||||
```
|
||||
|
||||
## 配置测试参数
|
||||
|
||||
每个测试脚本的开头都有配置参数,可以根据需要进行调整:
|
||||
|
||||
- `BASE_URL`: API服务器的基础URL(默认为 http://localhost:3000)
|
||||
- `CONCURRENT_REQUESTS`: 并发请求数(仅适用于压力测试)
|
||||
- `TOTAL_REQUESTS`: 总请求数
|
||||
- `SIMULATED_IPS`: 模拟的IP数量(仅适用于分布式测试)
|
||||
- `REQUESTS_PER_IP`: 每个IP的请求数(仅适用于分布式测试)
|
||||
- `TEST_ENDPOINT`: 测试的API端点
|
||||
|
||||
## 测试结果说明
|
||||
|
||||
测试脚本会输出彩色的测试进度和结果统计信息:
|
||||
|
||||
- 绿色点(`.`): 成功的请求
|
||||
- 黄色L(`L`): 被限速的请求
|
||||
- 红色E(`E`): 错误的请求
|
||||
- 红色X(`X`): 请求异常
|
||||
|
||||
测试完成后,会显示总体统计信息,包括总请求数、成功请求数、被限速请求数、错误请求数和限速比例。
|
182
scripts/distributed-test.js
Normal file
182
scripts/distributed-test.js
Normal file
@ -0,0 +1,182 @@
|
||||
import axios from 'axios';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
// 配置
|
||||
const BASE_URL = 'http://localhost:3000'; // 修改为你的服务器地址和端口
|
||||
const TEST_NAMESPACE = uuidv4(); // 生成随机命名空间用于测试
|
||||
const SIMULATED_IPS = 10; // 模拟的IP数量
|
||||
const REQUESTS_PER_IP = 30; // 每个IP的请求数
|
||||
const TEST_ENDPOINT = '/check'; // 测试端点
|
||||
const DELAY_BETWEEN_BATCHES = 500; // 批次间延迟(毫秒)
|
||||
|
||||
// 测试结果统计
|
||||
const stats = {
|
||||
totalRequests: 0,
|
||||
success: 0,
|
||||
rateLimited: 0,
|
||||
errors: 0,
|
||||
ipStats: {} // 每个IP的统计信息
|
||||
};
|
||||
|
||||
// 颜色输出
|
||||
const colors = {
|
||||
reset: '\x1b[0m',
|
||||
red: '\x1b[31m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
blue: '\x1b[34m',
|
||||
magenta: '\x1b[35m',
|
||||
cyan: '\x1b[36m'
|
||||
};
|
||||
|
||||
/**
|
||||
* 延迟函数
|
||||
* @param {number} ms 延迟毫秒数
|
||||
*/
|
||||
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
/**
|
||||
* 生成随机IP地址
|
||||
*/
|
||||
function generateRandomIP() {
|
||||
const ip = [];
|
||||
for (let i = 0; i < 4; i++) {
|
||||
ip.push(Math.floor(Math.random() * 256));
|
||||
}
|
||||
return ip.join('.');
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化IP统计
|
||||
*/
|
||||
function initIPStats() {
|
||||
const ips = [];
|
||||
for (let i = 0; i < SIMULATED_IPS; i++) {
|
||||
const ip = generateRandomIP();
|
||||
ips.push(ip);
|
||||
stats.ipStats[ip] = {
|
||||
success: 0,
|
||||
rateLimited: 0,
|
||||
errors: 0,
|
||||
totalRequests: 0
|
||||
};
|
||||
}
|
||||
return ips;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送单个请求
|
||||
* @param {string} ip 模拟的IP地址
|
||||
* @param {number} index 请求索引
|
||||
*/
|
||||
async function sendRequest(ip, index) {
|
||||
stats.totalRequests++;
|
||||
stats.ipStats[ip].totalRequests++;
|
||||
|
||||
const url = `${BASE_URL}${TEST_ENDPOINT}`;
|
||||
|
||||
try {
|
||||
const response = await axios({
|
||||
method: 'GET',
|
||||
url,
|
||||
headers: {
|
||||
'X-Forwarded-For': ip, // 模拟不同的IP地址
|
||||
'User-Agent': `TestBot/${ip}` // 模拟不同的用户代理
|
||||
},
|
||||
validateStatus: () => true // 不抛出HTTP错误
|
||||
});
|
||||
|
||||
// 处理响应
|
||||
if (response.status === 429) { // 请求被限速
|
||||
stats.rateLimited++;
|
||||
stats.ipStats[ip].rateLimited++;
|
||||
process.stdout.write(`${colors.yellow}L${colors.reset}`);
|
||||
} else if (response.status >= 200 && response.status < 300) { // 成功
|
||||
stats.success++;
|
||||
stats.ipStats[ip].success++;
|
||||
process.stdout.write(`${colors.green}.${colors.reset}`);
|
||||
} else { // 其他错误
|
||||
stats.errors++;
|
||||
stats.ipStats[ip].errors++;
|
||||
process.stdout.write(`${colors.red}E${colors.reset}`);
|
||||
}
|
||||
|
||||
return response.status;
|
||||
} catch (error) {
|
||||
stats.errors++;
|
||||
stats.ipStats[ip].errors++;
|
||||
process.stdout.write(`${colors.red}X${colors.reset}`);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打印统计信息
|
||||
* @param {Array<string>} ips IP地址列表
|
||||
*/
|
||||
function printStats(ips) {
|
||||
console.log(`\n\n${colors.magenta}分布式测试结果:${colors.reset}`);
|
||||
console.log(`总请求数: ${stats.totalRequests}`);
|
||||
console.log(`成功请求: ${stats.success}`);
|
||||
console.log(`被限速请求: ${stats.rateLimited}`);
|
||||
console.log(`错误请求: ${stats.errors}`);
|
||||
console.log(`总限速比例: ${Math.round((stats.rateLimited / stats.totalRequests) * 100)}%`);
|
||||
|
||||
console.log(`\n${colors.magenta}各IP测试结果:${colors.reset}`);
|
||||
ips.forEach((ip, index) => {
|
||||
const ipStat = stats.ipStats[ip];
|
||||
const limitedPercent = Math.round((ipStat.rateLimited / ipStat.totalRequests) * 100);
|
||||
console.log(`IP-${index+1} (${ip}): 总请求=${ipStat.totalRequests}, 成功=${ipStat.success}, 限速=${ipStat.rateLimited} (${limitedPercent}%), 错误=${ipStat.errors}`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 为单个IP发送多个请求
|
||||
* @param {string} ip IP地址
|
||||
*/
|
||||
async function sendRequestsForIP(ip) {
|
||||
const promises = [];
|
||||
for (let i = 0; i < REQUESTS_PER_IP; i++) {
|
||||
promises.push(sendRequest(ip, i));
|
||||
}
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* 主函数
|
||||
*/
|
||||
async function main() {
|
||||
console.log(`${colors.cyan}开始分布式测试限速功能...${colors.reset}`);
|
||||
console.log(`目标端点: ${TEST_ENDPOINT}`);
|
||||
console.log(`模拟IP数量: ${SIMULATED_IPS}`);
|
||||
console.log(`每个IP请求数: ${REQUESTS_PER_IP}`);
|
||||
console.log(`总请求数: ${SIMULATED_IPS * REQUESTS_PER_IP}`);
|
||||
|
||||
try {
|
||||
// 初始化IP统计
|
||||
const ips = initIPStats();
|
||||
|
||||
console.log(`\n${colors.cyan}测试进度:${colors.reset}`);
|
||||
|
||||
// 为每个IP发送请求
|
||||
for (let i = 0; i < ips.length; i++) {
|
||||
const ip = ips[i];
|
||||
console.log(`\n${colors.blue}测试IP-${i+1} (${ip}):${colors.reset} `);
|
||||
await sendRequestsForIP(ip);
|
||||
|
||||
// 在IP批次之间添加延迟
|
||||
if (i < ips.length - 1) {
|
||||
await delay(DELAY_BETWEEN_BATCHES);
|
||||
}
|
||||
}
|
||||
|
||||
printStats(ips);
|
||||
|
||||
console.log(`\n${colors.green}分布式测试完成!${colors.reset}`);
|
||||
} catch (error) {
|
||||
console.error(`\n${colors.red}测试过程中发生错误: ${error.message}${colors.reset}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 执行主函数
|
||||
main().catch(console.error);
|
83
scripts/run-all-tests.js
Normal file
83
scripts/run-all-tests.js
Normal file
@ -0,0 +1,83 @@
|
||||
import { spawn } from 'child_process';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
|
||||
// 获取当前文件的目录路径
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
// 颜色输出
|
||||
const colors = {
|
||||
reset: '\x1b[0m',
|
||||
red: '\x1b[31m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
blue: '\x1b[34m',
|
||||
magenta: '\x1b[35m',
|
||||
cyan: '\x1b[36m'
|
||||
};
|
||||
|
||||
/**
|
||||
* 运行脚本
|
||||
* @param {string} scriptName 脚本文件名
|
||||
* @param {string} description 测试描述
|
||||
*/
|
||||
function runScript(scriptName, description) {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log(`\n${colors.cyan}=======================================${colors.reset}`);
|
||||
console.log(`${colors.cyan}运行: ${description}${colors.reset}`);
|
||||
console.log(`${colors.cyan}=======================================${colors.reset}\n`);
|
||||
|
||||
const scriptPath = join(__dirname, scriptName);
|
||||
const child = spawn('node', [scriptPath], { stdio: 'inherit' });
|
||||
|
||||
child.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
console.log(`\n${colors.green}${description}完成,退出码: ${code}${colors.reset}`);
|
||||
resolve();
|
||||
} else {
|
||||
console.error(`\n${colors.red}${description}失败,退出码: ${code}${colors.reset}`);
|
||||
reject(new Error(`脚本退出码: ${code}`));
|
||||
}
|
||||
});
|
||||
|
||||
child.on('error', (error) => {
|
||||
console.error(`\n${colors.red}启动脚本时出错: ${error.message}${colors.reset}`);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 主函数
|
||||
*/
|
||||
async function main() {
|
||||
console.log(`${colors.magenta}开始运行所有限速测试...${colors.reset}`);
|
||||
|
||||
try {
|
||||
// 运行功能测试
|
||||
await runScript('test-rate-limit.js', '功能测试');
|
||||
|
||||
// 等待一段时间以确保限速计数器重置
|
||||
console.log(`\n${colors.yellow}等待30秒以确保限速计数器重置...${colors.reset}`);
|
||||
await new Promise(resolve => setTimeout(resolve, 30000));
|
||||
|
||||
// 运行压力测试
|
||||
await runScript('stress-test.js', '压力测试');
|
||||
|
||||
// 等待一段时间以确保限速计数器重置
|
||||
console.log(`\n${colors.yellow}等待30秒以确保限速计数器重置...${colors.reset}`);
|
||||
await new Promise(resolve => setTimeout(resolve, 30000));
|
||||
|
||||
// 运行分布式测试
|
||||
await runScript('distributed-test.js', '分布式测试');
|
||||
|
||||
console.log(`\n${colors.green}所有测试已完成!${colors.reset}`);
|
||||
} catch (error) {
|
||||
console.error(`\n${colors.red}测试过程中发生错误: ${error.message}${colors.reset}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 执行主函数
|
||||
main().catch(console.error);
|
135
scripts/stress-test.js
Normal file
135
scripts/stress-test.js
Normal file
@ -0,0 +1,135 @@
|
||||
import axios from 'axios';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
// 配置
|
||||
const BASE_URL = 'http://localhost:3000'; // 修改为你的服务器地址和端口
|
||||
const TEST_NAMESPACE = uuidv4(); // 生成随机命名空间用于测试
|
||||
const CONCURRENT_REQUESTS = 50; // 并发请求数
|
||||
const TOTAL_REQUESTS = 500; // 总请求数
|
||||
const TEST_ENDPOINT = '/check'; // 测试端点
|
||||
|
||||
// 测试结果统计
|
||||
const stats = {
|
||||
success: 0,
|
||||
rateLimited: 0,
|
||||
errors: 0,
|
||||
totalRequests: 0,
|
||||
startTime: 0,
|
||||
endTime: 0
|
||||
};
|
||||
|
||||
// 颜色输出
|
||||
const colors = {
|
||||
reset: '\x1b[0m',
|
||||
red: '\x1b[31m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
blue: '\x1b[34m',
|
||||
magenta: '\x1b[35m',
|
||||
cyan: '\x1b[36m'
|
||||
};
|
||||
|
||||
/**
|
||||
* 发送单个请求
|
||||
* @param {number} index 请求索引
|
||||
*/
|
||||
async function sendRequest(index) {
|
||||
stats.totalRequests++;
|
||||
const url = `${BASE_URL}${TEST_ENDPOINT}`;
|
||||
|
||||
try {
|
||||
const response = await axios({
|
||||
method: 'GET',
|
||||
url,
|
||||
validateStatus: () => true // 不抛出HTTP错误
|
||||
});
|
||||
|
||||
// 处理响应
|
||||
if (response.status === 429) { // 请求被限速
|
||||
stats.rateLimited++;
|
||||
process.stdout.write(`${colors.yellow}L${colors.reset}`);
|
||||
} else if (response.status >= 200 && response.status < 300) { // 成功
|
||||
stats.success++;
|
||||
process.stdout.write(`${colors.green}.${colors.reset}`);
|
||||
} else { // 其他错误
|
||||
stats.errors++;
|
||||
process.stdout.write(`${colors.red}E${colors.reset}`);
|
||||
}
|
||||
|
||||
// 每50个请求换行
|
||||
if (index % 50 === 0 && index > 0) {
|
||||
process.stdout.write('\n');
|
||||
}
|
||||
|
||||
return response.status;
|
||||
} catch (error) {
|
||||
stats.errors++;
|
||||
process.stdout.write(`${colors.red}X${colors.reset}`);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打印统计信息
|
||||
*/
|
||||
function printStats() {
|
||||
const duration = (stats.endTime - stats.startTime) / 1000;
|
||||
const rps = Math.round(stats.totalRequests / duration);
|
||||
|
||||
console.log(`\n\n${colors.magenta}压力测试结果:${colors.reset}`);
|
||||
console.log(`总请求数: ${stats.totalRequests}`);
|
||||
console.log(`成功请求: ${stats.success}`);
|
||||
console.log(`被限速请求: ${stats.rateLimited}`);
|
||||
console.log(`错误请求: ${stats.errors}`);
|
||||
console.log(`限速比例: ${Math.round((stats.rateLimited / stats.totalRequests) * 100)}%`);
|
||||
console.log(`测试持续时间: ${duration.toFixed(2)}秒`);
|
||||
console.log(`平均请求速率: ${rps} 请求/秒`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量发送请求
|
||||
* @param {number} batchSize 批次大小
|
||||
* @param {number} startIndex 起始索引
|
||||
*/
|
||||
async function sendBatch(batchSize, startIndex) {
|
||||
const promises = [];
|
||||
for (let i = 0; i < batchSize; i++) {
|
||||
const index = startIndex + i;
|
||||
if (index < TOTAL_REQUESTS) {
|
||||
promises.push(sendRequest(index));
|
||||
}
|
||||
}
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* 主函数
|
||||
*/
|
||||
async function main() {
|
||||
console.log(`${colors.cyan}开始压力测试限速功能...${colors.reset}`);
|
||||
console.log(`目标端点: ${TEST_ENDPOINT}`);
|
||||
console.log(`并发请求数: ${CONCURRENT_REQUESTS}`);
|
||||
console.log(`总请求数: ${TOTAL_REQUESTS}`);
|
||||
console.log(`\n${colors.cyan}测试进度:${colors.reset}`);
|
||||
|
||||
try {
|
||||
stats.startTime = Date.now();
|
||||
|
||||
// 分批发送请求
|
||||
const batches = Math.ceil(TOTAL_REQUESTS / CONCURRENT_REQUESTS);
|
||||
for (let i = 0; i < batches; i++) {
|
||||
const startIndex = i * CONCURRENT_REQUESTS;
|
||||
await sendBatch(CONCURRENT_REQUESTS, startIndex);
|
||||
}
|
||||
|
||||
stats.endTime = Date.now();
|
||||
printStats();
|
||||
|
||||
console.log(`\n${colors.green}压力测试完成!${colors.reset}`);
|
||||
} catch (error) {
|
||||
console.error(`\n${colors.red}测试过程中发生错误: ${error.message}${colors.reset}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 执行主函数
|
||||
main().catch(console.error);
|
250
scripts/test-rate-limit.js
Normal file
250
scripts/test-rate-limit.js
Normal file
@ -0,0 +1,250 @@
|
||||
import axios from 'axios';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
// 配置
|
||||
const BASE_URL = 'http://localhost:3030'; // 修改为你的服务器地址和端口
|
||||
const TEST_NAMESPACE = uuidv4(); // 生成随机命名空间用于测试
|
||||
const DELAY_BETWEEN_REQUESTS = 100; // 请求间隔时间(毫秒)
|
||||
|
||||
// 测试结果统计
|
||||
const stats = {
|
||||
success: 0,
|
||||
rateLimited: 0,
|
||||
errors: 0,
|
||||
totalRequests: 0
|
||||
};
|
||||
|
||||
// 颜色输出
|
||||
const colors = {
|
||||
reset: '\x1b[0m',
|
||||
red: '\x1b[31m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
blue: '\x1b[34m',
|
||||
magenta: '\x1b[35m',
|
||||
cyan: '\x1b[36m'
|
||||
};
|
||||
|
||||
/**
|
||||
* 延迟函数
|
||||
* @param {number} ms 延迟毫秒数
|
||||
*/
|
||||
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
/**
|
||||
* 发送请求并处理响应
|
||||
* @param {string} method HTTP方法
|
||||
* @param {string} endpoint 请求路径
|
||||
* @param {object} data 请求数据
|
||||
* @param {number} index 请求索引
|
||||
*/
|
||||
async function sendRequest(method, endpoint, data = null, index) {
|
||||
stats.totalRequests++;
|
||||
const url = `${BASE_URL}${endpoint}`;
|
||||
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
const response = await axios({
|
||||
method,
|
||||
url,
|
||||
data,
|
||||
validateStatus: () => true // 不抛出HTTP错误
|
||||
});
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
// 处理响应
|
||||
if (response.status === 429) { // 请求被限速
|
||||
stats.rateLimited++;
|
||||
console.log(`${colors.yellow}[${index}] ${method} ${endpoint} - 被限速 (${duration}ms)${colors.reset}`);
|
||||
return { limited: true, status: response.status };
|
||||
} else if (response.status >= 200 && response.status < 300) { // 成功
|
||||
stats.success++;
|
||||
console.log(`${colors.green}[${index}] ${method} ${endpoint} - 成功: ${response.status} (${duration}ms)${colors.reset}`);
|
||||
return { limited: false, status: response.status, data: response.data };
|
||||
} else { // 其他错误
|
||||
stats.errors++;
|
||||
console.log(`${colors.red}[${index}] ${method} ${endpoint} - 错误: ${response.status} (${duration}ms)${colors.reset}`);
|
||||
return { limited: false, status: response.status, error: response.data };
|
||||
}
|
||||
} catch (error) {
|
||||
stats.errors++;
|
||||
console.log(`${colors.red}[${index}] ${method} ${endpoint} - 异常: ${error.message}${colors.reset}`);
|
||||
return { limited: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试全局限速
|
||||
*/
|
||||
async function testGlobalRateLimit() {
|
||||
console.log(`\n${colors.cyan}===== 测试全局限速 (200/15分钟) =====${colors.reset}`);
|
||||
const requests = [];
|
||||
|
||||
// 发送250个请求 (应该有50个被限速)
|
||||
for (let i = 0; i < 250; i++) {
|
||||
requests.push(sendRequest('GET', '/check', null, i));
|
||||
await delay(DELAY_BETWEEN_REQUESTS);
|
||||
}
|
||||
|
||||
await Promise.all(requests);
|
||||
printStats('全局限速测试');
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试API限速
|
||||
*/
|
||||
async function testApiRateLimit() {
|
||||
console.log(`\n${colors.cyan}===== 测试API限速 (50/5分钟) =====${colors.reset}`);
|
||||
resetStats();
|
||||
const requests = [];
|
||||
|
||||
// 发送60个请求 (应该有10个被限速)
|
||||
for (let i = 0; i < 60; i++) {
|
||||
requests.push(sendRequest('GET', '/check', null, i));
|
||||
await delay(DELAY_BETWEEN_REQUESTS);
|
||||
}
|
||||
|
||||
await Promise.all(requests);
|
||||
printStats('API限速测试');
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试写操作限速
|
||||
*/
|
||||
async function testWriteRateLimit() {
|
||||
console.log(`\n${colors.cyan}===== 测试写操作限速 (10/分钟) =====${colors.reset}`);
|
||||
resetStats();
|
||||
const requests = [];
|
||||
|
||||
// 发送15个写请求 (应该有5个被限速)
|
||||
for (let i = 0; i < 15; i++) {
|
||||
const key = `test-key-${i}`;
|
||||
const data = { value: `test-value-${i}`, timestamp: Date.now() };
|
||||
requests.push(sendRequest('POST', `/${TEST_NAMESPACE}/${key}`, data, i));
|
||||
await delay(DELAY_BETWEEN_REQUESTS);
|
||||
}
|
||||
|
||||
await Promise.all(requests);
|
||||
printStats('写操作限速测试');
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试删除操作限速
|
||||
*/
|
||||
async function testDeleteRateLimit() {
|
||||
console.log(`\n${colors.cyan}===== 测试删除操作限速 (5/5分钟) =====${colors.reset}`);
|
||||
resetStats();
|
||||
|
||||
// 先创建几个键值对
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const key = `delete-key-${i}`;
|
||||
const data = { value: `delete-value-${i}` };
|
||||
await sendRequest('POST', `/${TEST_NAMESPACE}/${key}`, data, `创建-${i}`);
|
||||
await delay(DELAY_BETWEEN_REQUESTS);
|
||||
}
|
||||
|
||||
resetStats();
|
||||
const requests = [];
|
||||
|
||||
// 发送8个删除请求 (应该有3个被限速)
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const key = `delete-key-${i}`;
|
||||
requests.push(sendRequest('DELETE', `/${TEST_NAMESPACE}/${key}`, null, i));
|
||||
await delay(DELAY_BETWEEN_REQUESTS);
|
||||
}
|
||||
|
||||
await Promise.all(requests);
|
||||
printStats('删除操作限速测试');
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试认证限速
|
||||
*/
|
||||
async function testAuthRateLimit() {
|
||||
console.log(`\n${colors.cyan}===== 测试认证限速 (5/30分钟) =====${colors.reset}`);
|
||||
resetStats();
|
||||
const requests = [];
|
||||
|
||||
// 发送8个认证请求 (应该有3个被限速)
|
||||
for (let i = 0; i < 8; i++) {
|
||||
requests.push(sendRequest('POST', '/auth', { username: 'test', password: 'wrong' }, i));
|
||||
await delay(DELAY_BETWEEN_REQUESTS);
|
||||
}
|
||||
|
||||
await Promise.all(requests);
|
||||
printStats('认证限速测试');
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置统计数据
|
||||
*/
|
||||
function resetStats() {
|
||||
stats.success = 0;
|
||||
stats.rateLimited = 0;
|
||||
stats.errors = 0;
|
||||
stats.totalRequests = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 打印统计信息
|
||||
*/
|
||||
function printStats(testName) {
|
||||
console.log(`\n${colors.magenta}${testName}结果:${colors.reset}`);
|
||||
console.log(`总请求数: ${stats.totalRequests}`);
|
||||
console.log(`成功请求: ${stats.success}`);
|
||||
console.log(`被限速请求: ${stats.rateLimited}`);
|
||||
console.log(`错误请求: ${stats.errors}`);
|
||||
console.log(`限速比例: ${Math.round((stats.rateLimited / stats.totalRequests) * 100)}%`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 主函数
|
||||
*/
|
||||
async function main() {
|
||||
console.log(`${colors.cyan}开始测试限速功能...${colors.reset}`);
|
||||
console.log(`使用测试命名空间: ${TEST_NAMESPACE}`);
|
||||
|
||||
try {
|
||||
// 测试全局限速
|
||||
await testGlobalRateLimit();
|
||||
|
||||
// 重置统计并等待一段时间
|
||||
resetStats();
|
||||
await delay(2000);
|
||||
|
||||
// 测试API限速
|
||||
await testApiRateLimit();
|
||||
|
||||
// 重置统计并等待一段时间
|
||||
resetStats();
|
||||
await delay(2000);
|
||||
|
||||
// 测试写操作限速
|
||||
await testWriteRateLimit();
|
||||
|
||||
// 重置统计并等待一段时间
|
||||
resetStats();
|
||||
await delay(2000);
|
||||
|
||||
// 测试删除操作限速
|
||||
await testDeleteRateLimit();
|
||||
|
||||
// 重置统计并等待一段时间
|
||||
resetStats();
|
||||
await delay(2000);
|
||||
|
||||
// 测试认证限速
|
||||
await testAuthRateLimit();
|
||||
|
||||
// 清理测试数据
|
||||
console.log(`\n${colors.cyan}清理测试数据...${colors.reset}`);
|
||||
await sendRequest('DELETE', `/${TEST_NAMESPACE}`, null, 'cleanup');
|
||||
|
||||
console.log(`\n${colors.green}所有测试完成!${colors.reset}`);
|
||||
} catch (error) {
|
||||
console.error(`${colors.red}测试过程中发生错误: ${error.message}${colors.reset}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 执行主函数
|
||||
main().catch(console.error);
|
Loading…
x
Reference in New Issue
Block a user