|
|
51CTO旗下网站
|
|
移步端
  • 用Node.js编纂内存效率高的使用程序

    在本文中,咱们通过解决一下问题来了解 NodeJS 的流、引黄灌区和管道等组织,并打听它们分别如何支持编写内存有效的使用程序。

    笔者:jrainlau 来源:segmentfault| 2020-01-15 14:20

     

    一座被设计为能避开气流的建造 (https://pixelz.cc)

    硬件应用程序在电脑的玉器中运行,咱们称之为随机存取存储器(RAM)。JavaScript,尤其是 NodeJS (劳务端 JS)兴许我们为终端用户编写从小型到大型的硬件项目。拍卖程序的内存总是一番老大难的题材,因为糟糕的贯彻可能会阻塞在给定服务器或体系上运行的一切其他应用程序。C 和 C++ 程序员确实关心内存管理,因为隐藏在代码的每股角落都有可能出现可怕的内存泄漏。但是对于 JS 开发者来说,你真的有关心过这个题目吗?

    出于 JS 付出人员通常在专用的高容量服务器上开展 web 传感器编程,她们可能不会发现多任务处理的延期。举例说在开发 web 传感器的情况下,咱们也会运行多个应用程序,如必发娱乐登录服务器( MySQL )、缓存服务器( Redis )和任何需要的使用。咱们需要了解它们也会消耗可用之主人翁内存。如果我们随意地编写应用程序,很可能会降低其他进程的性质,甚至让内存完全拒绝对它们的分配。在本文中,咱们通过解决一下问题来了解 NodeJS 的流、引黄灌区和管道等组织,并打听它们分别如何支持编写内存有效的使用程序。

    咱们采用 NodeJS v8.12.0 来运转这些程序,整整代码示例都放在这里:

    narenaryan/node-backpressure-internals

    原文链接:Writing memory efficient software applications in Node.js 

    题材:大文件复制

    如果有人口把要求用 NodeJS 写一段文件复制的顺序,这就是说他会很快写出下面这段代码:

          
    1. const fs = require('fs');  
    2. let fileName = process.argv[2];  
    3. let destPath = process.argv[3];  
    4. fs.readFile(fileName, (err, data) => {  
    5.     if (err) throw err;  
    6.     fs.writeFile(destPath || 'output', data, (err) => {  
    7.         if (err) throw err;  
    8.     });   
    9.     console.log('New file has been created!');  
    10. }); 

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

    如今假设我们有一度大文件(大于4 GB)要求用这段程序来展开小修。就以我之一个达 7.4G 的超高清4K 影视为例子好了,我用上述的顺序代码把他从目前目录复制到别的目录。

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

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

          
    1. /home/shobarani/Workspace/basic_copy.js:7  
    2.     if (err) throw err;  
    3.              ^  
    4. RangeError: File size is greater than possible Buffer: 0x7fffffff bytes  
    5.     at FSReqWrap.readFileAfterStat [as oncomplete] (fs.js:453:11) 

    正如你看到的那样,出于 NodeJS 最大只同意写入 2GB 的多寡到他的风景区,导致了错误发生在读取文件的经过中。为了消灭这个题目,顶你在开展 I/O 成群结队操作的时节(研制、拍卖、调减等),最好考虑一下内存的状况。

    NodeJS 中的 Streams 和 Buffers

    为了消灭上述问题,咱们需要一个艺术把大文件切成许多文件块,同时要求一个数目结构去存放这些文件块。一度 buffer 就是用来存储二进制数据的组织。然后,咱们需要一个读写文件块的主意,而 Streams 则提供了这部分能力。

    Buffers(引黄灌区)

    咱们能够利用 Buffer 目标轻松地创造一个 buffer。

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

    在新版本的 NodeJS (>8)官方,你也得以这样写。

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

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

          
    1. let name = 'Node JS DEV' 
    2. let buffer = Buffer.from(name);  
    3. 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() 办法)的一个重要目标是将数据缓冲限制在可接收的档次,这样不同速度的源和对象就不会阻塞可用内存。

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

    地方的示意图中我们有两个项目的流,离别是可读流和可写流。.pipe() 办法是一番奇异基本的主意,用于连接可读流和可写流。如果你不晓得上面的示意图,也不要紧,在看完我们的例证以后,你可以返回示意图这里来,其二时候一切都会显得理所当然。管道是一种引人注目的公有制,下我们用两个比喻来阐明他。

    书法1(大概地采取流来复制文件)

    让咱设计一种书法来解决前文中大文件复制的题材。第一我们要创造两个流,下一场执行接下来的几个步骤。

    1.  监听来自可读流的多寡块
    2.  把数据块写进可写流
    3.  钉住文件复制的速度

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

          
    1. /*  
    2.     A file copy with streams and events - Author: Naren Arya  
    3. */  
    4. const stream = require('stream');  
    5. const fs = require('fs');  
    6. let fileName = process.argv[2];  
    7. let destPath = process.argv[3];  
    8. const readabale = fs.createReadStream(fileName);  
    9. const writeable = fs.createWriteStream(destPath || "output");  
    10. fs.stat(fileName, (err, stats) => {  
    11.     this.fileSize = stats.size;  
    12.     this.counter = 1 
    13.     this.fileArray = fileName.split('.');     
    14.     try {  
    15.         this.duplicate = destPath + "/" + this.fileArray[0] + '_Copy.' + this.fileArray[1];  
    16.     } catch(e) {  
    17.         console.exception('File name is invalid! please pass the proper one');  
    18.     }    
    19.     process.stdout.write(`File: ${this.duplicate} is being created:`);    
    20.     readabale.on('data', (chunk)=> {  
    21.         let percentageCopied = ((chunk.length * this.counter) / this.fileSize) * 100;  
    22.         process.stdout.clearLine();  // clear current text  
    23.         process.stdout.cursorTo(0);  
    24.         process.stdout.write(`${Math.round(percentageCopied)}%`);  
    25.         writeable.write(chunk);  
    26.         this.counter += 1;  
    27.     });    
    28.     readabale.on('end', (e) => {  
    29.         process.stdout.clearLine();  // clear current text  
    30.         process.stdout.cursorTo(0);  
    31.         process.stdout.write("Successfully finished the operation");  
    32.         return;  
    33.     });    
    34.     readabale.on('error', (e) => {  
    35.         console.log("Some error occured: ", e);  
    36.     });     
    37.     writeable.on('finish', () => { 
    38.         console.log("Successfully created the file copy!");  
    39.     });     
    40. }); 

    在这段程序中,咱们接受用户传入的两个文件路径(源文件和对象文件),下一场创建了两个流,用于把数据块从可读流运到可写流。下一场我们定义了部分变量去追踪文件复制的速度,下一场输出到控制台(此地为 console)。下半时我们还订阅了部分事件:

    data:顶一个数目块被读取时接触

    end:顶一个数目块被可读流所读取完的时节触发

    error:顶读取数据块的时节出错时接触

    运作这段程序,咱们可以成功地做到一个大文件(此地为7.4 G)的摄制任务。

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

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

    4.6GB?咱们的顺序在运行时所消耗的内存,在此间是讲不通的,以及他很有可能会卡死其他的使用程序。

    发生了什么?

    如果你有仔细察看上图中的读写率,你会发现部分端倪。

    Disk Read: 53.4 MiB/s

    Disk Write: 14.8 MiB/s

    这意味着生产者正在以更快的进度生产,而消费者无法跟上这个速度。微机为了保存读取的多寡块,名将多余的多寡存储到机器的RAM官方。这就是RAM出现地价的由来。

    上述代码在我之机械上运行了3成分16秒……

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

    书法2(基于流和机关背压的公文复制)

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

    让咱将以此程序命名为 streams_copy_efficient.js

          
    1. /*  
    2.     A file copy with streams and piping - Author: Naren Arya  
    3. */  
    4. const stream = require('stream');  
    5. const fs = require('fs');  
    6. let fileName = process.argv[2];  
    7. let destPath = process.argv[3];  
    8. const readabale = fs.createReadStream(fileName);  
    9. const writeable = fs.createWriteStream(destPath || "output");  
    10. fs.stat(fileName, (err, stats) => {  
    11.     this.fileSize = stats.size;  
    12.     this.counter = 1 
    13.     this.fileArray = fileName.split('.');   
    14.     try {  
    15.         this.duplicate = destPath + "/" + this.fileArray[0] + '_Copy.' + this.fileArray[1];  
    16.     } catch(e) {  
    17.         console.exception('File name is invalid! please pass the proper one');  
    18.     }     
    19.     process.stdout.write(`File: ${this.duplicate} is being created:`);   
    20.     readabale.on('data', (chunk) => {  
    21.         let percentageCopied = ((chunk.length * this.counter) / this.fileSize) * 100;  
    22.         process.stdout.clearLine();  // clear current text  
    23.         process.stdout.cursorTo(0);  
    24.         process.stdout.write(`${Math.round(percentageCopied)}%`);  
    25.         this.counter += 1;  
    26.     });     
    27.     readabale.pipe(writeable); // Auto pilot ON!     
    28.     // In case if we have an interruption while copying  
    29.     writeable.on('unpipe', (e) => {  
    30.         process.stdout.write("Copy has failed!");  
    31.     });     
    32. }); 

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

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

    此地的 pipe 就是全部魔法发生之由来。他控制了磁盘读写的进度以至于不会阻塞内存(RAM)。

    运作一下。

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

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

    震惊!如今 Node 先后仅仅占用了61.9 MiB 的内存。如果你观察到读写速率的话:

    Disk Read: 35.5 MiB/s

    Disk Write: 35.5 MiB/s

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

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

    出于 NodeJS 流和管道,内存负载减少了98.68%,推行时间也减少了。这就是为什么管道是一番有力的生活。

    61.9 MiB 是由可读流创建的风景区大小。咱们还可以运用可读流上的 read 办法为缓冲块分配自定义大小。

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

    除了本地文件的摄制以外,其一艺术还可以用于优化许多 I/O 借鉴的题材:

  •  拍卖从卡夫卡到必发娱乐登录的数据流
  •  拍卖来自文件系统之数据流,动态压缩并写入磁盘
  •  更多……
  • 源码(Git)

    你可以在我之库房底下找到所有的例证并在协调之机械上测试。

    narenaryan/node-backpressure-internals

    总结

    我写这篇文章的想法,重点是为了说明即使 NodeJS 提供了很好的 API,咱们也可能会一不小心就写出性能很差的编码。如果我们能更多地关心其内置的工具,咱们便足以更好地优化程序的运作方式。

    你在此可以找到更多关于“背压”的素材:

    backpressuring-in-streams

    完。

    【义务编辑: 庞桂玉 TEL:(010)68476606】

    点赞 0
  • Node.js  使用程序  javascript
  • 分享:
    大家都在看
    猜你喜欢
  • 订阅专栏+更多

    Python使用场景实战手册

    Python使用场景实战手册

    Python使用场景实战手册
    共3章 | KaliArch

    118人口订阅学习

    一步到位玩儿透Ansible

    一步到位玩儿透Ansible

    Ansible
    共17章 | 骏马金龙1

    193人口订阅学习

    云架构师修炼手册

    云架构师修炼手册

    云架构师之必不可少技能
    共3章 | Allen在路上

    132人口订阅学习

    读 书 +更多

    基于Eclipse的正本求源框架技术与实战

    眼前,开源框架层出不穷,他为客户提供了通用的解决方案,同时也增加了他家之读书难度。开源是一把“双刃剑”,一边它共享了货源,提供了...

    订阅51CTO邮刊

    点击这里查看样刊

    订阅51CTO邮刊

    51CTO劳务号

    51CTO官微

    
       
       
       
    
       
       
       
        
       
  •