Promise.all批量发起大量fetch请求致服务器异常的解决方案咨询
背景
将115个fetch请求放入Promise.all同时触发时,出现脚本冻结或服务器返回500错误;目前通过添加50ms延迟避免同时请求,总加载时间达5.75秒。使用Apache+PHP+MySQL搭建的API,希望找到更高效的方案,而非依赖猜测服务器承受能力的延迟设置,同时保留单个请求缓存的优势。
当前延迟实现代码:
let offset = 100 let delay = 0 let requests = ['apiRequest0','apiRequest1','apiRequest2','apiRequest3'...] let data = {} await (async() => { promises = [] requests.forEach(function(item){ promises.push( (async() => { await new Promise(function(resolve) { setTimeout(resolve, delay); }); data[item] = await fetch(`/api/${item}`).then(response => response.json()) })() ) delay += offset }) return await Promise.all(promises) })();
已尝试方案:
- fetch中使用
keep-alive,无效果 - 提高PHP执行时间和内存限制,无变化
疑问解答
1. 是否需要创建批量API端点?
如果想保留单个请求缓存的优势,不一定要强制做批量端点,但可以做折中方案:
- 提供可选的批量接口,允许客户端按需选择:比如支持
/api/batch?ids=id1,id2,id3,返回对应多个资源的键值对,同时单个请求/api/id仍保留,这样既可以在需要时批量加载,又不丢失单请求缓存的灵活性。 - 但如果服务器端的瓶颈是MySQL连接数(115个请求同时建立115个数据库连接),批量接口能大幅减少数据库连接开销,这时候即使要保留单请求缓存,也建议补充批量接口作为优化选项。
2. 能否用一个连接发起所有请求,让服务器识别为非独立请求?
不行。HTTP/1.1的keep-alive是复用TCP连接来发送多个请求,但每个请求在服务器端依然是独立的请求实例,PHP会为每个请求单独处理(除非用Swoole/Workerman这类常驻内存框架,但这属于架构改造)。
HTTP/2的多路复用可以在一个TCP连接上同时发送多个请求,但服务器端还是会把它们当作独立请求处理,依然会占用相同的PHP进程/数据库连接资源,无法解决服务器端并发过载的问题。
3. 能否让服务器排队处理请求,而非同时处理?
对于Apache+PHP的传统架构(比如prefork模式),服务器的并发能力由MaxRequestWorkers等配置决定,超过上限的请求会被Apache排队等待空闲进程。但如果115个请求同时到达,瞬间占满所有工作进程,后续请求排队,但这时候可能已经导致服务器资源耗尽(比如MySQL连接池被占满),依然会出现500错误。
可以通过以下方式优化服务器端排队/限流:
- 在PHP层实现简单的请求队列:比如用Redis做分布式队列,每个API请求先入队,然后由后台进程消费队列处理请求并返回结果,但这需要改造API为异步模式,客户端需要轮询或WebSocket接收结果,复杂度较高。
- 配置Apache的限流模块(比如
mod_ratelimit)或使用Nginx作为反向代理做限流,控制单个IP的请求速率,避免瞬间大量请求涌入。
浏览器如何处理大量并发资源请求?
浏览器会对同一域名的并发请求数做限制(Chrome默认是6个),超过限制的请求会排队,形成开发者工具中看到的「瀑布流」——一批6个请求发送,其中某个请求完成后,队列中的下一个请求才会发起。
这种机制就是为了避免同时发送大量请求压垮服务器,你可以模仿这个逻辑,实现并发请求数控制,而不是固定延迟:比如同时最多发送6个请求,一个请求完成后立即发起下一个,这样总耗时会比固定延迟更短,且更适配服务器的实际承受能力。
实现并发控制的代码示例
const requests = ['apiRequest0','apiRequest1','apiRequest2',...]; const data = {}; const maxConcurrent = 6; // 匹配浏览器默认并发数 async function fetchWithConcurrency() { let index = 0; async function fetchNext() { if (index >= requests.length) return; const item = requests[index++]; try { data[item] = await fetch(`/api/${item}`).then(res => res.json()); } catch (err) { console.error(`请求${item}失败:`, err); } await fetchNext(); // 当前请求完成后,发起下一个 } // 初始化并发请求池 const pool = Array(maxConcurrent).fill().map(fetchNext); await Promise.all(pool); } await fetchWithConcurrency();
这种方式的优势:
- 不会瞬间发送所有请求,而是保持固定的并发数,避免压垮服务器
- 总耗时更短:比如每个请求平均耗时100ms,115个请求用6并发的话,总耗时约
(115/6)*100ms ≈ 1917ms,远低于固定50ms延迟的5.75秒 - 自动适配请求耗时:如果某个请求慢,其他并发请求依然会继续处理,不会被固定延迟拖累
内容的提问来源于stack exchange,提问作者Timothée Billiet

