Node/ExpressJs无法同时处理1000请求的问题排查求助
Node.js Express 请求批量延迟问题分析与解决方案
场景复现
部署了两个Express服务器,代码如下:
Server A 代码
const express = require('express') const axios = require('axios'); const app = express() const port = 3000 function deepSleep (duration) { const start = new Date().getTime(); while (true) { const current = new Date().getTime(); if (current - start > duration) break; } } app.get('/:id', async (req, res) => { deepSleep(30); // 测试时便于观察 await axios.get('http://localhost:3001/' + req.params.id); // 调用Server B res.send('Hello World!'); }) app.listen(port, () => { console.log(`Example app listening on port ${port}`) })
Server B 代码
const express = require('express') const app = express() const port = 3001 app.get('/:id', (req, res) => { console.log(new Date().getTime()); console.log(req.params); res.sendStatus(200); }) const server = app.listen(port, () => { console.log(`Example app listening on port ${port}`) })
调用端代码
const axios = require('axios'); for (let i = 0; i < 1000; i+= 1) { axios.get(`http://localhost:3000/${i}`); }
问题现象
向Server A发送1000个请求时,预期每个请求会立即触发对Server B的调用,但实际是所有请求在Server A处理完成后,才批量打向Server B,导致Server B过载、请求耗时剧增。即使移除deepSleep的阻塞影响,axios的异步请求依然没有按预期立即触发Server B的连接。
原因分析
1. 同步阻塞直接卡死事件循环
deepSleep是同步阻塞代码,会完全占用Node.js的单线程事件循环。每个请求进入路由处理后,先执行30ms的while循环,这段时间事件循环无法处理任何其他任务——包括其他请求的处理、异步IO的回调。1000个请求就需要30*1000=30秒才能完成所有同步阻塞逻辑,之后才会批量执行所有axios请求,直接导致Server B瞬间被打满。
2. 异步场景下的隐性限制
即使移除deepSleep,问题依然存在,核心原因有两个:
- TCP连接队列与请求处理顺序:Node.js的HTTP服务器会将到来的请求缓存到TCP队列中,Express会依次取出请求处理。调用端一次性发起1000个请求,会把Server A的TCP队列占满,Express只能逐个处理请求。
- HTTP客户端并发限制:axios底层依赖Node.js的
http模块,旧版本中maxSockets(最大并发连接数)默认是5,新版本虽改为Infinity,但操作系统的端口限制、TCP连接建立的开销,会导致大量axios请求被客户端队列缓存,最终集中到达Server B。
优化方案
1. 彻底移除同步阻塞代码
用异步延迟替代同步deepSleep,避免卡死事件循环:
// 替换为异步sleep函数 async function sleep(duration) { return new Promise(resolve => setTimeout(resolve, duration)); } // 路由中使用 app.get('/:id', async (req, res) => { await sleep(30); await axios.get(`http://localhost:3001/${req.params.id}`); res.send('Hello World!'); })
2. 给Server A添加请求限流
控制Server A同时处理的请求数量,避免被打垮:
const rateLimit = require('express-rate-limit'); // 1秒内最多处理50个请求 const limiter = rateLimit({ windowMs: 1000, max: 50, standardHeaders: true, legacyHeaders: false, }); app.use(limiter);
3. 限制向Server B的并发请求数
用p-limit控制axios的并发量,避免瞬间压垮Server B:
const pLimit = require('p-limit'); // 最多同时发起20个请求到Server B const limit = pLimit(20); app.get('/:id', async (req, res) => { await sleep(30); // 用limit包裹axios请求,控制并发 await limit(() => axios.get(`http://localhost:3001/${req.params.id}`)); res.send('Hello World!'); });
4. 调用端控制并发
不要一次性发起1000个请求,分批限制并发:
const pLimit = require('p-limit'); const limit = pLimit(50); const requestTasks = []; for (let i = 0; i < 1000; i++) { requestTasks.push(limit(() => axios.get(`http://localhost:3000/${i}`))); } // 等待所有请求完成 await Promise.all(requestTasks);
5. Server B增强抗过载能力
给Server B添加限流和超时处理,提升稳定性:
const rateLimit = require('express-rate-limit'); const limiter = rateLimit({ windowMs: 1000, max: 100, // 1秒内最多处理100个请求 }); app.use(limiter); // 添加请求超时处理 app.use((req, res, next) => { res.setTimeout(5000, () => { res.status(504).send('Request Timeout'); }); next(); });
内容的提问来源于stack exchange,提问作者Brian Phan
相关产品推荐
相关产品推荐

