NodeJS createReadStream 踩坑记
2023-11-26
| 2023-11-26
0  |  0 分钟
type
status
date
slug
summary
tags
category
icon
password

背景

上传文件超过 2GB 时候,Nodejs 给报错了…
定位一下代码后,发现是下面这句有问题:
filepath 指向的文件超过 2GB 后,就有了刚才的异常了。
 
其实挺好理解的,因为上面的代码尝试将文件加载到内存中,超出了 v8 的内存限制了。这也引出了今天的问题:Node.js 中要如何处理大文件?

在 Node.js 中处理大文件

Node.js 中有个很核心的东西叫 Stream 也就是「数据流」。这个挺好理解:一次性无法完成的任务,例如加载2GB的文件到内容,就将其拆分成子任务来进行,例如一次处理1M数据,分成2048次处理就能处理2GB的文件了。这里的子任务无限进行细分,就变成了「数据流」了,也就是 Stream 最核心的概念。
 
现在我们带着 Stream 这把利器后,重新来审视刚才的代码,就发现确实没有必要将文件一次性加载到内存中,这么做纯粹是当初写的时候偷懒了,现在问题暴露出来了。
 
那么我们审视一下拿到文件数据后,看看我都干了啥吧:
 

操作1:计算文件 MD5

计算一段数据的 MD5 是一个很常见的需求,这里直接怼数据进行计算,不符合 Stream 的概念,下面我们来进行改造:
改造后,引入了一个新的 API:createReadStream
这里建议提前看一下文档,等会就知道踩坑点在哪里了。设计这个 API 的人,估计脑袋有点进水了,呵呵。
 
在引入 stream.Readable 概念后,我们计算 MD5 就不再是梭哈了,而是根据「data」事件累计计算所有的分配(chunk),最后当「end」触发时候,表示没有数据了,这个时候才对数据汇总,输出最后的 MD5。
 
进过此番改造后,再次计算大文件的MD5即可正常运行,唯一的区别就是要稍微多等待那么一段时间。

操作2:文件分片&计算分片MD5

改造前的分配,就是暴力的对整个 Buffer 进行分片,虽然效率很高,但是招架不住内存问题,也因此需要改造。
这次改造依旧借助 createReadStream 能力,只不过这次我们通过指定 startend 参数来指定读取的范围,以实现「分片」的效果。唯一的缺点就是重复打开关闭文件,占用较多的 io 资源,也不知道操作系统是否会对频繁读取的文件进行优化。
 
就当我庆幸自己如此聪明的时候,我发现改造后的分片MD5计算结果不符合预期,但是审查代码又无法发现问题,这导致我一度非常的困惑,怀疑是不是MD5计算的做法不正确,为此我在错误的道路上做了不少弯路,甚至会向他们的妈妈问好。
 
直到我突然翻阅到了这段内容:
`options` can include `start` and `end` values to read a range of bytes from the file instead of the entire file. Both `start` and `end` are inclusive and start counting at 0, ...
 
「inclusive」不是「include」的变种吗?include, emm.., 包含?等一下,什么?包含?也就说说包括「end」指向的那个字节?WTF?我居然多算了一个字节,难怪MD5一直算不对…
 
如晴天霹雳一般,我第一次对这个世界有了新的认知。为什么不能包含「end」字节呢?所以我之前咋想的呢?对啊,我咋没想到可以包含「end」字节呢?
notion image
这是哪个 GENIUS 设计的 API 啊,我必须给上一个双手双脚的赞同…
 
所以正确的写法应该是这样的:
 
至此,重新跑一下代码,bingo,总于算对了… 我的心也累了…

总结

整体改造还是非常成功的,程序能够支持处理 2GB 以上的「大文件」了,只是中间出现的小插曲让我一度怀疑人生,之前用的好好的「经验」或者「约定俗成」的东西居然不管用了,这对我的打击不小,尤其是它出现在我每天睁眼闭眼都能用到的 Node.js 之中,实在被秀到了。
 
我现在依旧没有想明白「end」这点字节「inclusive」的意义何在呢?为了突出这个 API 是如此的与众不同吗?如果有想明白的也恳请大家的指教。
 
碎片杂文
  • 开发
  • 入坑!我的第一辆公路车给服务器添加交换内存(Swap)
    目录