Node.js中读取10个50MB文件与1个500MB文件耗时相同的原因?
问题背景
示例1:单个5000ms Promise
// fulfill one promise 5000ms console.time('test'); (async function () { await new Promise((resolve) => setTimeout(resolve, 5000)); console.log('slept') })() .then(() => { console.timeEnd('test'); }) // Time ~5000 мс
示例2:并行10个500ms Promise
// fulfill 10 promises by 500ms console.time('test'); const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; let test = []; (async function () { test = await Promise.all( arr.map((elem) => new Promise(async (resolve) => setTimeout(resolve, 500))) ) console.log(test.length) console.log('raided') })() .then(() => { console.timeEnd('test'); }) // Time ~500 мс
示例3:同步读取单个500MB文件
// Read file 500mb const fs = require('node:fs'); console.time('test'); let test; (async function () { test = fs.readFileSync('./assets/500mb'); console.log('raided', test.length) })() .then(() => { console.timeEnd('test'); }) // Time ~300 мс
示例4:并行读取10个50MB文件
// Read 10 files by 50mb const fs = require('node:fs'); console.time('test'); const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; let test = []; (async function () { test = await Promise.all( arr.map((elem) => new Promise((resolve) => fs.readFile(`./assets/50mb_${elem}`, resolve))) ) console.log(test.length) console.log('raided') })() .then(() => { console.timeEnd('test'); }) // Time ~300ms
疑问
执行前两段代码时,我们会得到约10倍的执行速度差异,这一点不难理解。但将setTimeout替换为文件读取操作后,却无法获得这样的速度提升。我知晓文件读取并非由Node.js本身处理,但具体流程是怎样的?为何我们甚至无法获得至少2倍的速度提升?第三段与第四段代码的耗时完全相同,读取1个500MB文件和读取10个各50MB的文件耗时没有区别,这是为什么?
解答
1. setTimeout与文件读取的核心差异
setTimeout是CPU空闲等待任务:它只是告诉操作系统“指定时间后触发回调”,期间Node.js的事件循环完全可以处理其他任务。所以10个500ms的setTimeout并行执行时,总耗时只取决于单个任务的等待时间,和任务数量无关,自然能拿到10倍的效率提升。
而文件读取是IO密集型任务,核心瓶颈不在Node.js,而在存储设备的IO带宽:
- Node.js调用
fs.readFile时,会把读取请求交给操作系统的IO线程池处理,自己回到事件循环等待结果。 - 但存储设备(SSD/机械硬盘)的总带宽是固定的——比如你的SSD连续读取速度是1.7GB/s,那不管读1个500MB文件还是10个50MB文件,总数据量都是500MB,总耗时都接近
500MB / 1.7GB/s ≈ 300ms。
2. 并行读多文件没提速的原因
- 存储设备的连续读取优势:如果10个50MB文件是连续存储在磁盘上的,并行读取和单个大文件读取的效率几乎一致;即使文件分散,总数据量相同的情况下,耗时差异也不会明显。
- 操作系统IO调度优化:当同时发起多个文件读取请求时,操作系统会把这些请求合并成连续的IO操作(如果文件物理地址连续),避免机械硬盘磁头频繁寻道或SSD块寻址的额外开销,实际耗时和读单个大文件差不多。
- IO线程池的限制:Node.js默认IO线程池大小是4,即使调大线程池数量,存储带宽的瓶颈依然存在——线程再多,设备每秒能读取的数据量是固定的,总耗时不会减少。
3. 同步与异步读取的耗时一致原因
示例3用了fs.readFileSync,这是同步阻塞调用,会让Node.js主线程等待直到文件读取完成;示例4是异步并行读取,虽然执行方式不同,但两者的总数据量相同,且都受限于存储设备的带宽,所以最终耗时接近。
总结来说,setTimeout的并行是“等待时间重叠”,而文件读取的并行是“数据量叠加”,两者的瓶颈完全不同——前者是时间调度,后者是硬件IO带宽。
内容的提问来源于stack exchange,提问作者Alexander Cheprasov
相关产品推荐
相关产品推荐

