Files
competition-website/app/middleware/concurrency_guard.js
WorkBuddy a303cf2d53 chore: 后端首次基线提交
运行必须源码:app/(controller/model/service/middleware/lib/utils/validate/router)、config/、database/(migrations/seeders/sql)、启动脚本、app/public/templates 模板。
已忽略运行非必须:node_modules/、logs/、run/、app/public 运行时上传数据、.env*、构建/缓存产物。
2026-08-17 12:25:42 +08:00

54 lines
2.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
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.
'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--;
}
};
};