'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--; } }; };