54 lines
2.3 KiB
JavaScript
54 lines
2.3 KiB
JavaScript
|
|
'use strict';
|
|||
|
|
|
|||
|
|
// ==================== 请求级并发闸门(大文件上传/下载) ====================
|
|||
|
|
// 背景(2026-08-11 压测结论):
|
|||
|
|
// 场景「无带宽限制 + 单 worker + 极端并发」(大量考生同时上传大文件 +
|
|||
|
|
// 管理后台同时下载大文件)下,单 worker 即使有文件 IO 护栏(file_io_guard),
|
|||
|
|
// 仍会在「被接受的并发重请求数」过高时被海量大文件流压垮(OOM 被系统杀掉,
|
|||
|
|
// 无任何 Node 异常日志)。原因是 file_io_guard 只限制「同时进行的文件流」,
|
|||
|
|
// 而 90 个并发请求的连接已先被 TCP 接受、请求体已在传输中。
|
|||
|
|
//
|
|||
|
|
// 本闸门在「读取请求体之前」对重 IO 接口做全局并发上限:
|
|||
|
|
// - 命中重路由且 active < MAX:放行(active++),请求结束(finally)后 active--;
|
|||
|
|
// - 命中重路由且 active >= MAX:立即回 429(连接级背压),请求体不会被解析/缓冲,
|
|||
|
|
// 由前端 uploadWithRetry / 下载侧重试指数退避重试 —— 保证后端存活而非雪崩。
|
|||
|
|
// - 非重路由:直接放行,不影响登录/统计等轻接口。
|
|||
|
|
//
|
|||
|
|
// 与 file_io_guard 互补:本闸门限制「被接受的并发重请求数」(防连接/内存被打满),
|
|||
|
|
// file_io_guard 限制「同时进行的文件流数(按流生命周期释放)」(防 fd/事件循环耗尽)。
|
|||
|
|
// 双保险,使单 worker 在极端并发下「降级(429)而非崩溃」。
|
|||
|
|
|
|||
|
|
const HEAVY_PATTERNS = [
|
|||
|
|
/^\/api\/upload\/[^/]+\/chunk$/i,
|
|||
|
|
/^\/api\/upload\/[^/]+\/merge$/i,
|
|||
|
|
/^\/api\/exam\/[^/]+\/word$/i,
|
|||
|
|
/^\/api\/admin\/submission-file\//i,
|
|||
|
|
/^\/api\/admin\/submissions\/[^/]+\/[^/]+\/word$/i,
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
module.exports = (options, app) => {
|
|||
|
|
const MAX = parseInt(process.env.HEAVY_REQ_MAX || (options && options.max) || '20', 10);
|
|||
|
|
|
|||
|
|
let active = 0;
|
|||
|
|
|
|||
|
|
return async function concurrencyGuard(ctx, next) {
|
|||
|
|
if (!HEAVY_PATTERNS.some((re) => re.test(ctx.path))) {
|
|||
|
|
return next();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (active >= MAX) {
|
|||
|
|
// 🔴 连接级背压:直接拒绝,避免请求体被解析/缓冲占用内存
|
|||
|
|
ctx.status = 429;
|
|||
|
|
ctx.body = ctx.helper.error(429, '服务器繁忙,请稍后重试');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
active++;
|
|||
|
|
try {
|
|||
|
|
await next();
|
|||
|
|
} finally {
|
|||
|
|
active--;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
};
|