如何用Node.js编写内存效率高的应用程序


Posted in Javascript onApril 30, 2021

前言

软件应用程序在计算机的主存储器中运行,我们称之为随机存取存储器(RAM)。JavaScript,尤其是 Nodejs(服务端js)允许我们为终端用户编写从小型到大型的软件项目。处理程序的内存总是一个棘手的问题,因为糟糕的实现可能会阻塞在给定服务器或系统上运行的所有其他应用程序。C 和 C++程序员确实关心内存管理,因为隐藏在代码的每个角落都有可能出现可怕的内存泄漏。但是对于 JS 开发者来说,你真的有关心过这个问题吗?

由于 JS 开发人员通常在专用的高容量服务器上进行 web 服务器编程,他们可能不会察觉多任务处理的延迟。比方说在开发 web 服务器的情况下,我们也会运行多个应用程序,如数据库服务器( MySQL )、缓存服务器( Redis )和其他需要的应用。我们需要知道它们也会消耗可用的主内存。如果我们随意地编写应用程序,很可能会降低其他进程的性能,甚至让内存完全拒绝对它们的分配。在本文中,我们通过解决一个问题来了解 NodeJS 的流、缓冲区和管道等结构,并了解它们分别如何支持编写内存有效的应用程序。

问题:大文件复制

如果有人被要求用 NodeJS 写一段文件复制的程序,那么他会迅速写出下面这段代码:

const fs = require('fs');

let fileName = process.argv[2];
let destPath = process.argv[3];

fs.readFile(fileName, (err, data) => {
    if (err) throw err;

    fs.writeFile(destPath || 'output', data, (err) => {
        if (err) throw err;
    });
    
    console.log('New file has been created!');
});

这段代码简单地根据输入的文件名和路径,在尝试对文件读取后把它写入目标路径,这对于小文件来说是不成问题的。

现在假设我们有一个大文件(大于4 GB)需要用这段程序来进行备份。就以我的一个达 7.4G 的超高清4K 电影为例子好了,我用上述的程序代码把它从当前目录复制到别的目录。

$ node basic_copy.js cartoonMovie.mkv ~/Documents/bigMovie.mkv

然后在 Ubuntu(Linux )系统下我得到了这段报错:

/home/shobarani/Workspace/basic_copy.js:7

    if (err) throw err;

             ^

RangeError: File size is greater than possible Buffer: 0x7fffffff bytes

    at FSReqWrap.readFileAfterStat [as oncomplete] (fs.js:453:11)

正如你看到的那样,由于 NodeJS 最大只允许写入 2GB 的数据到它的缓冲区,导致了错误发生在读取文件的过程中。为了解决这个问题,当你在进行 I/O 密集操作的时候(复制、处理、压缩等),最好考虑一下内存的情况。

NodeJS 中的 Streams 和 Buffers

为了解决上述问题,我们需要一个办法把大文件切成许多文件块,同时需要一个数据结构去存放这些文件块。一个 buffer 就是用来存储二进制数据的结构。接下来,我们需要一个读写文件块的方法,而 Streams 则提供了这部分能力。

Buffers(缓冲区)

我们能够利用 Buffer 对象轻松地创建一个 buffer。

let buffer = new Buffer(10); # 10 为 buffer 的体积
console.log(buffer); # prints <Buffer 00 00 00 00 00 00 00 00 00 00>

在新版本的 NodeJS (>8)中,你也可以这样写。

let buffer = new Buffer.alloc(10);
console.log(buffer); # prints <Buffer 00 00 00 00 00 00 00 00 00 00>

如果我们已经有了一些数据,比如数组或者别的数据集,我们可以为它们创建一个 buffer。

let name = 'Node JS DEV';
let buffer = Buffer.from(name);
console.log(buffer) # prints <Buffer 4e 6f 64 65 20 4a 53 20 44 45 5>

Buffers 有一些如buffer.toString()和buffer.toJSON()之类的重要方法,能够深入到其所存储的数据当中去。

我们不会为了优化代码而去直接创建原始 buffer。NodeJS 和 V8 引擎在处理 streams 和网络 socket 的时候就已经在创建内部缓冲区(队列)中实现了这一点。

Streams(流)

简单来说,流就像 NodeJS 对象上的任意门。在计算机网络中,入口是一个输入动作,出口是一个输出动作。我们接下来将继续使用这些术语。

流的类型总共有四种:

  • 可读流(用于读取数据)
  • 可写流(用于写入数据)
  • 双工流(同时可用于读写)
  • 转换流(一种用于处理数据的自定义双工流,如压缩,检查数据等)

下面这句话可以清晰地阐述为什么我们应该使用流。

Stream API (尤其是stream.pipe()方法)的一个重要目标是将数据缓冲限制在可接受的水平,这样不同速度的源和目标就不会阻塞可用内存。

我们需要一些办法去完成任务而不至于压垮系统。这也是我们在文章开头就已经提到过的。

如何用Node.js编写内存效率高的应用程序

上面的示意图中我们有两个类型的流,分别是可读流和可写流。.pipe()方法是一个非常基本的方法,用于连接可读流和可写流。如果你不明白上面的示意图,也没关系,在看完我们的例子以后,你可以回到示意图这里来,那个时候一切都会显得理所当然。管道是一种引人注目的机制,下面我们用两个例子来说明它。

解法1(简单地使用流来复制文件)

让我们设计一种解法来解决前文中大文件复制的问题。首先我们要创建两个流,然后执行接下来的几个步骤。

1.监听来自可读流的数据块

2.把数据块写进可写流

3.跟踪文件复制的进度

我们把这段代码命名为streams_copy_basic.js

/*
    A file copy with streams and events - Author: Naren Arya
*/

const stream = require('stream');
const fs = require('fs');

let fileName = process.argv[2];
let destPath = process.argv[3];

const readabale = fs.createReadStream(fileName);
const writeable = fs.createWriteStream(destPath || "output");

fs.stat(fileName, (err, stats) => {
    this.fileSize = stats.size;
    this.counter = 1;
    this.fileArray = fileName.split('.');
    
    try {
        this.duplicate = destPath + "/" + this.fileArray[0] + '_Copy.' + this.fileArray[1];
    } catch(e) {
        console.exception('File name is invalid! please pass the proper one');
    }
    
    process.stdout.write(`File: ${this.duplicate} is being created:`);
    
    readabale.on('data', (chunk)=> {
        let percentageCopied = ((chunk.length * this.counter) / this.fileSize) * 100;
        process.stdout.clearLine();  // clear current text
        process.stdout.cursorTo(0);
        process.stdout.write(`${Math.round(percentageCopied)}%`);
        writeable.write(chunk);
        this.counter += 1;
    });
    
    readabale.on('end', (e) => {
        process.stdout.clearLine();  // clear current text
        process.stdout.cursorTo(0);
        process.stdout.write("Successfully finished the operation");
        return;
    });
    
    readabale.on('error', (e) => {
        console.log("Some error occured: ", e);
    });
    
    writeable.on('finish', () => {
        console.log("Successfully created the file copy!");
    });
    
});

在这段程序中,我们接收用户传入的两个文件路径(源文件和目标文件),然后创建了两个流,用于把数据块从可读流运到可写流。然后我们定义了一些变量去追踪文件复制的进度,然后输出到控制台(此处为 console)。与此同时我们还订阅了一些事件:

data:当一个数据块被读取时触发

end:当一个数据块被可读流所读取完的时候触发

error:当读取数据块的时候出错时触发

运行这段程序,我们可以成功地完成一个大文件(此处为7.4 G)的复制任务。

$ time node streams_copy_basic.js cartoonMovie.mkv ~/Documents/4kdemo.mkv

然而,当我们通过任务管理器观察程序在运行过程中的内存状况时,依旧有一个问题。

如何用Node.js编写内存效率高的应用程序

4.6GB?我们的程序在运行时所消耗的内存,在这里是讲不通的,以及它很有可能会卡死其他的应用程序。

发生了什么?

如果你有仔细观察上图中的读写率,你会发现一些端倪。

Disk Read: 53.4 MiB/s

Disk Write: 14.8 MiB/s

这意味着生产者正在以更快的速度生产,而消费者无法跟上这个速度。计算机为了保存读取的数据块,将多余的数据存储到机器的RAM中。这就是RAM出现峰值的原因。

上述代码在我的机器上运行了3分16秒……

17.16s user 25.06s system 21% cpu 3:16.61 total

解法2(基于流和自动背压的文件复制)

为了克服上述问题,我们可以修改程序来自动调整磁盘的读写速度。这个机制就是背压。我们不需要做太多,只需将可读流导入可写流即可,NodeJS 会负责背压的工作。

让我们将这个程序命名为streams_copy_efficient.js

/*
    A file copy with streams and piping - Author: Naren Arya
*/

const stream = require('stream');
const fs = require('fs');

let fileName = process.argv[2];
let destPath = process.argv[3];

const readabale = fs.createReadStream(fileName);
const writeable = fs.createWriteStream(destPath || "output");

fs.stat(fileName, (err, stats) => {
    this.fileSize = stats.size;
    this.counter = 1;
    this.fileArray = fileName.split('.');
    
    try {
        this.duplicate = destPath + "/" + this.fileArray[0] + '_Copy.' + this.fileArray[1];
    } catch(e) {
        console.exception('File name is invalid! please pass the proper one');
    }
    
    process.stdout.write(`File: ${this.duplicate} is being created:`);
    
    readabale.on('data', (chunk) => {
        let percentageCopied = ((chunk.length * this.counter) / this.fileSize) * 100;
        process.stdout.clearLine();  // clear current text
        process.stdout.cursorTo(0);
        process.stdout.write(`${Math.round(percentageCopied)}%`);
        this.counter += 1;
    });
    
    readabale.pipe(writeable); // Auto pilot ON!
    
    // In case if we have an interruption while copying
    writeable.on('unpipe', (e) => {
        process.stdout.write("Copy has failed!");
    });
    
});

在这个例子中,我们用一句代码替换了之前的数据块写入操作。

readabale.pipe(writeable); // Auto pilot ON!

这里的pipe就是所有魔法发生的原因。它控制了磁盘读写的速度以至于不会阻塞内存(RAM)。

运行一下。

$ time node streams_copy_efficient.js cartoonMovie.mkv ~/Documents/4kdemo.mkv

我们复制了同一个大文件(7.4 GB),让我们来看看内存利用率。

如何用Node.js编写内存效率高的应用程序

震惊!现在 Node 程序仅仅占用了61.9 MiB 的内存。如果你观察到读写速率的话:

Disk Read: 35.5 MiB/s

Disk Write: 35.5 MiB/s

在任意给定的时间内,因为背压的存在,读写速率得以保持一致。更让人惊喜的是,这段优化后的程序代码整整比之前的快了13秒。

12.13s user 28.50s system 22% cpu 3:03.35 total

由于 NodeJS 流和管道,内存负载减少了98.68%,执行时间也减少了。这就是为什么管道是一个强大的存在。

61.9 MiB 是由可读流创建的缓冲区大小。我们还可以使用可读流上的 read 方法为缓冲块分配自定义大小。

const readabale = fs.createReadStream(fileName);
readable.read(no_of_bytes_size);

除了本地文件的复制以外,这个技术还可以用于优化许多 I/O 操作的问题:

  • 处理从卡夫卡到数据库的数据流
  • 处理来自文件系统的数据流,动态压缩并写入磁盘
  • 更多……

结论

我写这篇文章的动机,主要是为了说明即使 NodeJS 提供了很好的 API,我们也可能会一不留神就写出性能很差的代码。如果我们能更多地关注其内置的工具,我们便可以更好地优化程序的运行方式。

以上就是如何用Node.js编写内存效率高的应用程序的详细内容,更多关于Node.js的资料请关注三水点靠木其它相关文章!

Javascript 相关文章推荐
js下判断 iframe 是否加载完成的完美方法
Oct 26 Javascript
JavaScript OOP面向对象介绍
Dec 02 Javascript
jQuery中replaceWith()方法用法实例
Dec 25 Javascript
动态加载js文件简单示例
Apr 21 Javascript
jQuery实现为LI列表前3行设置样式的方法【2种方法】
Sep 04 Javascript
JavaScript使用类似break机制中断forEach循环的方法
Nov 13 Javascript
详解vue挂载到dom上会发生什么
Jan 20 Javascript
react配置antd按需加载的使用
Feb 11 Javascript
利用Webpack实现小程序多项目管理的方法
Feb 25 Javascript
layui对工具条进行选择性的显示方法
Sep 19 Javascript
javascript操作向表格中动态加载数据
Aug 27 Javascript
详细介绍Next.js脚手架完整搭建封装
Apr 26 Javascript
用几道面试题来看JavaScript执行机制
Apr 30 #Javascript
详解前端任务构建利器Gulp.js使用指南
Apr 30 #Javascript
浅谈node.js中间件有哪些类型
Apr 29 #Javascript
JavaScript实现简单图片切换
何时使用Map来代替普通的JS对象
详解Js模块化的作用原理和方案
详解JavaScript中的执行上下文及调用堆栈
You might like
PHP字符串word末字符实现大小写互换的方法
2014/11/10 PHP
PHP实现一维数组转二维数组的方法
2015/02/25 PHP
PHP实现求两个字符串最长公共子串的方法示例
2017/11/17 PHP
解决laravel session失效的问题
2019/10/14 PHP
laravel框架使用FormRequest进行表单验证,验证异常返回JSON操作示例
2020/02/18 PHP
jQuery层次选择器选择元素使用介绍
2013/04/18 Javascript
php析构函数的具体用法小结
2014/03/11 Javascript
js无刷新操作table的行和列
2014/03/27 Javascript
javasctipt如何显示几分钟前、几天前等
2014/04/30 Javascript
node.js中的fs.exists方法使用说明
2014/12/17 Javascript
深入探寻javascript定时器
2015/01/02 Javascript
使用 TypeScript 重新编写的 JavaScript 坦克大战游戏代码
2015/04/07 Javascript
jquery实现一个简单的表单验证实例
2016/03/30 Javascript
AngularJS 指令的交互详解及实例代码
2016/09/14 Javascript
jQuery插件HighCharts实现气泡图效果示例【附demo源码】
2017/03/13 Javascript
Vue2.0基于vue-cli+webpack父子组件通信(实例讲解)
2017/09/14 Javascript
微信小程序使用Socket的实例
2017/09/19 Javascript
Express系列之multer上传的使用
2017/10/27 Javascript
详解React之父子组件传递和其它一些要点
2018/06/25 Javascript
layui插件表单验证提交触发提交的例子
2019/09/09 Javascript
100行代码实现vue表单校验功能(小白自编)
2019/11/19 Javascript
JavaScript动画实例之粒子文本的实现方法详解
2020/07/28 Javascript
[05:56]第十六期——新进3大C之小兔基
2014/06/24 DOTA
python和ruby,我选谁?
2017/09/13 Python
Django Admin实现三级联动的示例代码(省市区)
2018/06/22 Python
python从入门到精通 windows安装python图文教程
2019/05/18 Python
python multiprocessing模块用法及原理介绍
2019/08/20 Python
matplotlib之pyplot模块坐标轴标签设置使用(xlabel()、ylabel())
2021/02/22 Python
HTML5 LocalStorage 本地存储详细概括(多图)
2017/08/18 HTML / CSS
达拉斯牛仔官方商店:Dallas Cowboys Pro Shop
2018/02/10 全球购物
阳光体育活动总结
2014/04/30 职场文书
离职感谢信怎么写
2015/01/22 职场文书
学习型家庭事迹材料(2016精选版)
2016/02/29 职场文书
优秀员工演讲稿
2019/06/21 职场文书
Vue过滤器(filter)实现及应用场景详解
2021/06/15 Vue.js
关于SpringBoot 使用 Redis 分布式锁解决并发问题
2021/11/17 Redis