Node.js中的高效内存处理

程序在工作过程中会使用计算机的RAM。 在JavaScript中的Node.js环境中,您可以编写各种规模的服务器项目。 用记忆组织工作始终是一项艰巨而负责的任务。 同时,如果在诸如C和C ++之类的语言中,程序员紧密地参与了内存管理,那么JS具有自动机制,这似乎完全消除了程序员高效处理内存的责任。 但是,实际上并非如此。 Node.js的代码编写不当可能会干扰运行该服务器的整个服务器的正常运行。



我们今天发布的翻译材料将重点讨论Node.js环境中内存的有效工作。 特别是,这里将讨论诸如流,缓冲区和pipe()流方法之类的概念。 实验中将使用Node.js v8.12.0。 在此处可以找到带有示例代码的存储库。

任务:复制一个大文件


如果有人要求创建一个用于在Node.js中复制文件的程序,那么很可能他会立即写出下面显示的内容。 我们将包含此代码的文件命名为basic_copy.js

 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.4 GB的视频文件,使用上述程序,我将尝试将其从当前目录复制到Documents目录。 这是开始复制的命令:

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

在Ubuntu中,执行此命令后,显示与缓冲区溢出有关的错误消息:

 /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) 

如您所见,由于Node.js仅允许将2 GB的数据读入缓冲区,因此文件读取操作失败。 如何克服这个限制? 在执行大量使用I / O子系统的操作(复制文件,处理,压缩它们)时,有必要考虑系统的功能以及与内存相关的限制。

Node.js中的流和缓冲区


为了解决上述问题,我们需要一种可以将大量数据分解为小片段的机制。 我们还将需要数据结构来存储这些片段并使用它们。 缓冲区是一种允许您存储二进制数据的数据结构。 接下来,我们需要能够从磁盘读取数据并将其写入磁盘。 这个机会可以给我们带来流动。 让我们谈谈缓冲区和线程。

▍缓冲区


可以通过初始化Buffer对象来创建Buffer

 let buffer = new Buffer(10); // 10 -    console.log(buffer); //  <Buffer 00 00 00 00 00 00 00 00 00 00> 

在比第8版更高的Node.js版本中,最好使用以下构造来创建缓冲区:

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

如果我们已经有一些数据,例如数组或类似数据,则可以基于此数据创建缓冲区。

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

缓冲区具有允许您“查看”它们并找出其中包含什么数据的方法-这些是toString()toJSON()方法。

我们在优化代码的过程中不会自己创建缓冲区。 当使用流或网络套接字时,Node.js自动创建这些数据结构。

▍流


如果我们转向科幻小说的语言,可以将流与通往其他世界的门户进行比较。 流有四种类型:

  • 用于读取的流(可以从中读取数据)。
  • 用于记录的流(可以将数据发送到该流)。
  • 双工流(开放用于从中读取数据和向其发送数据)。
  • 转换流(一种特殊的双工流,允许您处理数据,例如,对其进行压缩或检查其正确性)。

我们需要流,因为Node.js中的流API stream.pipe()尤其是stream.pipe()方法stream.pipe()的重要目标是将数据缓冲限制在可接受的水平。 这样做是为了使处理不同处理速度的数据源和接收器不会溢出可用内存。

换句话说,要解决复制大文件的问题,我们需要某种允许我们不使系统过载的机制。


流和缓冲区(基于Node.js文档)

上图显示了两种类型的流-可读流和可写流。 pipe()方法是一种非常简单的机制,它允许您将读取线程附加到写入线程。 如果上述方案对您来说不是特别清楚,那就可以了。 在分析了以下示例之后,您可以轻松地对其进行处理。 特别地,现在我们将考虑使用pipe()方法进行数据处理的示例。

解决方案1.使用流复制文件


考虑一下我们上面谈到的复制大文件问题的解决方案。 该解决方案可以基于两个线程,并且将如下所示:

  • 我们希望下一个数据出现在流中以供读取。
  • 我们将接收到的数据写入流中进行记录。
  • 我们监视复制操作的进度。

我们将调用实现此想法的程序streams_copy_basic.js 。 这是她的代码:

 /*         . : Naren Arya */ const stream = require('stream'); const fs = require('fs'); let fileName = process.argv[2]; let destPath = process.argv[3]; const readable = 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:`);     readable.on('data', (chunk)=> {       let percentageCopied = ((chunk.length * this.counter) / this.fileSize) * 100;       process.stdout.clearLine();  //          process.stdout.cursorTo(0);       process.stdout.write(`${Math.round(percentageCopied)}%`);       writeable.write(chunk);       this.counter += 1;   });     readable.on('end', (e) => {       process.stdout.clearLine();  //          process.stdout.cursorTo(0);       process.stdout.write("Successfully finished the operation");       return;   });     readable.on('error', (e) => {       console.log("Some error occurred: ", e);   });     writeable.on('finish', () => {       console.log("Successfully created the file copy!");   });  }); 

我们希望用户运行该程序为她提供两个文件名。 第一个是源文件,第二个是其将来副本的名称。 我们创建两个流-用于读取的流和用于写入的流,将数据从第一个传输到第二个。 还有一些辅助机制。 它们用于监视复制过程并将相应的信息输出到控制台。

我们在这里使用事件机制,尤其是我们在谈论订阅以下事件:

  • data -读取一条data调用。
  • end-从读取流中读取数据时调用。
  • error -如果在读取数据时发生error则调用。

使用此程序,将复制7.4 GB的文件,而不会出现错误消息。

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

但是,有一个问题。 可以通过查看有关各种过程使用系统资源的数据来识别它。


系统资源使用情况数据

请注意, node进程在复制了88%的文件后会占用4.6 GB的内存。 这很多,这样的内存处理可能会干扰其他程序的工作。

memory过度消耗内存的原因


请注意上图( Disk ReadDisk Write列)中从磁盘读取数据并将数据写入磁盘的速度。 即,在这里您可以看到以下指示器:

 Disk Read: 53.4 MiB/s Disk Write: 14.8 MiB/s 

从数据记录读取速度的这种差异意味着数据源生成它们的速度比接收器可以接收和处理它们的速度快得多。 计算机必须将读取的数据片段存储在内存中,直到将它们写入磁盘为止。 结果,我们看到了这样的内存使用指标。

在我的计算机上,该程序运行了3分16秒。 以下是有关其实施进度的信息:

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

解决方案2.使用流复制文件并自动调整读写数据的速度


为了解决上述问题,我们可以修改程序,以便在文件复制期间自动配置读写速度。 这种机制称为背压。 为了使用它,我们不需要做任何特殊的事情。 使用pipe()方法将读取流连接到写入流就足够了,Node.js将自动调整数据传输速度。

将此程序streams_copy_efficient.js 。 这是她的代码:

 /*          pipe(). : Naren Arya */ const stream = require('stream'); const fs = require('fs'); let fileName = process.argv[2]; let destPath = process.argv[3]; const readable = 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:`);     readable.on('data', (chunk) => {       let percentageCopied = ((chunk.length * this.counter) / this.fileSize) * 100;       process.stdout.clearLine();  //          process.stdout.cursorTo(0);       process.stdout.write(`${Math.round(percentageCopied)}%`);       this.counter += 1;   });   readable.on('error', (e) => {       console.log("Some error occurred: ", e);   });     writeable.on('finish', () => {       process.stdout.clearLine();  //          process.stdout.cursorTo(0);       process.stdout.write("Successfully created the file copy!");   });     readable.pipe(writeable); //  !  }); 

该程序与上一个程序的主要区别在于,用于复制数据片段的代码被替换为以下行:

 readable.pipe(writeable); //  ! 

在此发生的所有事情的核心是pipe()方法。 它控制读写速度,从而导致内存不再过载的事实。

运行程序。

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

我们正在复制相同的大文件。 现在让我们看一下内存和磁盘的工作方式。


通过使用竖线(),可以自动配置读写速度

现在我们看到node进程仅消耗61.9 MB的内存。 如果查看有关磁盘使用情况的数据,则可以看到以下内容:

 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 

通过使用pipe()方法,我们可以减少程序执行时间,并减少98.68%的内存消耗。

在这种情况下,61.9 MB是由数据读取流创建的缓冲区的大小。 我们可以使用流的read()方法自行读取数据,从而很好地设置此大小:

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

在这里,我们将文件复制到本地文件系统中,但是可以使用相同的方法来优化许多其他数据输入输出任务。 例如,这正在处理数据流,数据流的源是Kafka,接收器是数据库。 根据相同的方案,可以组织从磁盘读取数据,按照他们所说的“即时”压缩数据,然后将其以压缩形式写回到磁盘。 实际上,此处描述的技术还有许多其他用途。

总结


本文的目标之一是演示即使在该平台为开发人员提供了出色的API的情况下,在Node.js上编写不良程序也是多么容易。 对此API有所注意,可以提高服务器端软件项目的质量。

亲爱的读者们! 您如何在Node.js中使用缓冲区和线程?

Source: https://habr.com/ru/post/zh-CN433408/


All Articles