如何用Node.js編寫(xiě)內(nèi)存效率高的應(yīng)用程序
前言
軟件應(yīng)用程序在計(jì)算機(jī)的主存儲(chǔ)器中運(yùn)行,我們稱之為隨機(jī)存取存儲(chǔ)器(RAM)。JavaScript,尤其是 Nodejs(服務(wù)端js)允許我們?yōu)榻K端用戶編寫(xiě)從小型到大型的軟件項(xiàng)目。處理程序的內(nèi)存總是一個(gè)棘手的問(wèn)題,因?yàn)樵愀獾膶?shí)現(xiàn)可能會(huì)阻塞在給定服務(wù)器或系統(tǒng)上運(yùn)行的所有其他應(yīng)用程序。C 和 C++程序員確實(shí)關(guān)心內(nèi)存管理,因?yàn)殡[藏在代碼的每個(gè)角落都有可能出現(xiàn)可怕的內(nèi)存泄漏。但是對(duì)于 JS 開(kāi)發(fā)者來(lái)說(shuō),你真的有關(guān)心過(guò)這個(gè)問(wèn)題嗎?
由于 JS 開(kāi)發(fā)人員通常在專用的高容量服務(wù)器上進(jìn)行 web 服務(wù)器編程,他們可能不會(huì)察覺(jué)多任務(wù)處理的延遲。比方說(shuō)在開(kāi)發(fā) web 服務(wù)器的情況下,我們也會(huì)運(yùn)行多個(gè)應(yīng)用程序,如數(shù)據(jù)庫(kù)服務(wù)器( MySQL )、緩存服務(wù)器( Redis )和其他需要的應(yīng)用。我們需要知道它們也會(huì)消耗可用的主內(nèi)存。如果我們隨意地編寫(xiě)應(yīng)用程序,很可能會(huì)降低其他進(jìn)程的性能,甚至讓內(nèi)存完全拒絕對(duì)它們的分配。在本文中,我們通過(guò)解決一個(gè)問(wèn)題來(lái)了解 NodeJS 的流、緩沖區(qū)和管道等結(jié)構(gòu),并了解它們分別如何支持編寫(xiě)內(nèi)存有效的應(yīng)用程序。
問(wèn)題:大文件復(fù)制
如果有人被要求用 NodeJS 寫(xiě)一段文件復(fù)制的程序,那么他會(huì)迅速寫(xiě)出下面這段代碼:
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!'); });
這段代碼簡(jiǎn)單地根據(jù)輸入的文件名和路徑,在嘗試對(duì)文件讀取后把它寫(xiě)入目標(biāo)路徑,這對(duì)于小文件來(lái)說(shuō)是不成問(wèn)題的。
現(xiàn)在假設(shè)我們有一個(gè)大文件(大于4 GB)需要用這段程序來(lái)進(jìn)行備份。就以我的一個(gè)達(dá) 7.4G 的超高清4K 電影為例子好了,我用上述的程序代碼把它從當(dāng)前目錄復(fù)制到別的目錄。
$ node basic_copy.js cartoonMovie.mkv ~/Documents/bigMovie.mkv
然后在 Ubuntu(Linux )系統(tǒng)下我得到了這段報(bào)錯(cuò):
/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 最大只允許寫(xiě)入 2GB 的數(shù)據(jù)到它的緩沖區(qū),導(dǎo)致了錯(cuò)誤發(fā)生在讀取文件的過(guò)程中。為了解決這個(gè)問(wèn)題,當(dāng)你在進(jìn)行 I/O 密集操作的時(shí)候(復(fù)制、處理、壓縮等),最好考慮一下內(nèi)存的情況。
NodeJS 中的 Streams 和 Buffers
為了解決上述問(wèn)題,我們需要一個(gè)辦法把大文件切成許多文件塊,同時(shí)需要一個(gè)數(shù)據(jù)結(jié)構(gòu)去存放這些文件塊。一個(gè) buffer 就是用來(lái)存儲(chǔ)二進(jìn)制數(shù)據(jù)的結(jié)構(gòu)。接下來(lái),我們需要一個(gè)讀寫(xiě)文件塊的方法,而 Streams 則提供了這部分能力。
Buffers(緩沖區(qū))
我們能夠利用 Buffer 對(duì)象輕松地創(chuàng)建一個(gè) 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)中,你也可以這樣寫(xiě)。
let buffer = new Buffer.alloc(10); console.log(buffer); # prints <Buffer 00 00 00 00 00 00 00 00 00 00>
如果我們已經(jīng)有了一些數(shù)據(jù),比如數(shù)組或者別的數(shù)據(jù)集,我們可以為它們創(chuàng)建一個(gè) 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()之類的重要方法,能夠深入到其所存儲(chǔ)的數(shù)據(jù)當(dāng)中去。
我們不會(huì)為了優(yōu)化代碼而去直接創(chuàng)建原始 buffer。NodeJS 和 V8 引擎在處理 streams 和網(wǎng)絡(luò) socket 的時(shí)候就已經(jīng)在創(chuàng)建內(nèi)部緩沖區(qū)(隊(duì)列)中實(shí)現(xiàn)了這一點(diǎn)。
Streams(流)
簡(jiǎn)單來(lái)說(shuō),流就像 NodeJS 對(duì)象上的任意門(mén)。在計(jì)算機(jī)網(wǎng)絡(luò)中,入口是一個(gè)輸入動(dòng)作,出口是一個(gè)輸出動(dòng)作。我們接下來(lái)將繼續(xù)使用這些術(shù)語(yǔ)。
流的類型總共有四種:
- 可讀流(用于讀取數(shù)據(jù))
- 可寫(xiě)流(用于寫(xiě)入數(shù)據(jù))
- 雙工流(同時(shí)可用于讀寫(xiě))
- 轉(zhuǎn)換流(一種用于處理數(shù)據(jù)的自定義雙工流,如壓縮,檢查數(shù)據(jù)等)
下面這句話可以清晰地闡述為什么我們應(yīng)該使用流。
Stream API (尤其是stream.pipe()方法)的一個(gè)重要目標(biāo)是將數(shù)據(jù)緩沖限制在可接受的水平,這樣不同速度的源和目標(biāo)就不會(huì)阻塞可用內(nèi)存。
我們需要一些辦法去完成任務(wù)而不至于壓垮系統(tǒng)。這也是我們?cè)谖恼麻_(kāi)頭就已經(jīng)提到過(guò)的。
上面的示意圖中我們有兩個(gè)類型的流,分別是可讀流和可寫(xiě)流。.pipe()方法是一個(gè)非?;镜姆椒ǎ糜谶B接可讀流和可寫(xiě)流。如果你不明白上面的示意圖,也沒(méi)關(guān)系,在看完我們的例子以后,你可以回到示意圖這里來(lái),那個(gè)時(shí)候一切都會(huì)顯得理所當(dāng)然。管道是一種引人注目的機(jī)制,下面我們用兩個(gè)例子來(lái)說(shuō)明它。
解法1(簡(jiǎn)單地使用流來(lái)復(fù)制文件)
讓我們?cè)O(shè)計(jì)一種解法來(lái)解決前文中大文件復(fù)制的問(wèn)題。首先我們要?jiǎng)?chuàng)建兩個(gè)流,然后執(zhí)行接下來(lái)的幾個(gè)步驟。
1.監(jiān)聽(tīng)來(lái)自可讀流的數(shù)據(jù)塊
2.把數(shù)據(jù)塊寫(xiě)進(jìn)可寫(xiě)流
3.跟蹤文件復(fù)制的進(jìn)度
我們把這段代碼命名為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!"); }); });
在這段程序中,我們接收用戶傳入的兩個(gè)文件路徑(源文件和目標(biāo)文件),然后創(chuàng)建了兩個(gè)流,用于把數(shù)據(jù)塊從可讀流運(yùn)到可寫(xiě)流。然后我們定義了一些變量去追蹤文件復(fù)制的進(jìn)度,然后輸出到控制臺(tái)(此處為 console)。與此同時(shí)我們還訂閱了一些事件:
data:當(dāng)一個(gè)數(shù)據(jù)塊被讀取時(shí)觸發(fā)
end:當(dāng)一個(gè)數(shù)據(jù)塊被可讀流所讀取完的時(shí)候觸發(fā)
error:當(dāng)讀取數(shù)據(jù)塊的時(shí)候出錯(cuò)時(shí)觸發(fā)
運(yùn)行這段程序,我們可以成功地完成一個(gè)大文件(此處為7.4 G)的復(fù)制任務(wù)。
$ time node streams_copy_basic.js cartoonMovie.mkv ~/Documents/4kdemo.mkv
然而,當(dāng)我們通過(guò)任務(wù)管理器觀察程序在運(yùn)行過(guò)程中的內(nèi)存狀況時(shí),依舊有一個(gè)問(wèn)題。
4.6GB?我們的程序在運(yùn)行時(shí)所消耗的內(nèi)存,在這里是講不通的,以及它很有可能會(huì)卡死其他的應(yīng)用程序。
發(fā)生了什么?
如果你有仔細(xì)觀察上圖中的讀寫(xiě)率,你會(huì)發(fā)現(xiàn)一些端倪。
Disk Read: 53.4 MiB/s
Disk Write: 14.8 MiB/s
這意味著生產(chǎn)者正在以更快的速度生產(chǎn),而消費(fèi)者無(wú)法跟上這個(gè)速度。計(jì)算機(jī)為了保存讀取的數(shù)據(jù)塊,將多余的數(shù)據(jù)存儲(chǔ)到機(jī)器的RAM中。這就是RAM出現(xiàn)峰值的原因。
上述代碼在我的機(jī)器上運(yùn)行了3分16秒……
17.16s user 25.06s system 21% cpu 3:16.61 total
解法2(基于流和自動(dòng)背壓的文件復(fù)制)
為了克服上述問(wèn)題,我們可以修改程序來(lái)自動(dòng)調(diào)整磁盤(pán)的讀寫(xiě)速度。這個(gè)機(jī)制就是背壓。我們不需要做太多,只需將可讀流導(dǎo)入可寫(xiě)流即可,NodeJS 會(huì)負(fù)責(zé)背壓的工作。
讓我們將這個(gè)程序命名為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!"); }); });
在這個(gè)例子中,我們用一句代碼替換了之前的數(shù)據(jù)塊寫(xiě)入操作。
readabale.pipe(writeable); // Auto pilot ON!
這里的pipe就是所有魔法發(fā)生的原因。它控制了磁盤(pán)讀寫(xiě)的速度以至于不會(huì)阻塞內(nèi)存(RAM)。
運(yùn)行一下。
$ time node streams_copy_efficient.js cartoonMovie.mkv ~/Documents/4kdemo.mkv
我們復(fù)制了同一個(gè)大文件(7.4 GB),讓我們來(lái)看看內(nèi)存利用率。
震驚!現(xiàn)在 Node 程序僅僅占用了61.9 MiB 的內(nèi)存。如果你觀察到讀寫(xiě)速率的話:
Disk Read: 35.5 MiB/s
Disk Write: 35.5 MiB/s
在任意給定的時(shí)間內(nèi),因?yàn)楸硥旱拇嬖?,讀寫(xiě)速率得以保持一致。更讓人驚喜的是,這段優(yōu)化后的程序代碼整整比之前的快了13秒。
12.13s user 28.50s system 22% cpu 3:03.35 total
由于 NodeJS 流和管道,內(nèi)存負(fù)載減少了98.68%,執(zhí)行時(shí)間也減少了。這就是為什么管道是一個(gè)強(qiáng)大的存在。
61.9 MiB 是由可讀流創(chuàng)建的緩沖區(qū)大小。我們還可以使用可讀流上的 read 方法為緩沖塊分配自定義大小。
const readabale = fs.createReadStream(fileName); readable.read(no_of_bytes_size);
除了本地文件的復(fù)制以外,這個(gè)技術(shù)還可以用于優(yōu)化許多 I/O 操作的問(wèn)題:
- 處理從卡夫卡到數(shù)據(jù)庫(kù)的數(shù)據(jù)流
- 處理來(lái)自文件系統(tǒng)的數(shù)據(jù)流,動(dòng)態(tài)壓縮并寫(xiě)入磁盤(pán)
- 更多……
結(jié)論
我寫(xiě)這篇文章的動(dòng)機(jī),主要是為了說(shuō)明即使 NodeJS 提供了很好的 API,我們也可能會(huì)一不留神就寫(xiě)出性能很差的代碼。如果我們能更多地關(guān)注其內(nèi)置的工具,我們便可以更好地優(yōu)化程序的運(yùn)行方式。
以上就是如何用Node.js編寫(xiě)內(nèi)存效率高的應(yīng)用程序的詳細(xì)內(nèi)容,更多關(guān)于Node.js的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
簡(jiǎn)單了解node npm cnpm的具體使用方法
這篇文章主要介紹了簡(jiǎn)單了解node npm cnpm的具體使用方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-02-02詳解nodejs微信公眾號(hào)開(kāi)發(fā)——2.自動(dòng)回復(fù)
這篇文章主要介紹了詳解nodejs微信公眾號(hào)開(kāi)發(fā)——2.自動(dòng)回復(fù),非常具有實(shí)用價(jià)值,需要的朋友可以參考下2017-04-04npm?install安裝報(bào)錯(cuò):gyp?info?it?worked?if?it?ends?with?
今天新啟動(dòng)一個(gè)項(xiàng)目,在 npm install 安裝依賴項(xiàng)時(shí)出現(xiàn)報(bào)錯(cuò),所以下面這篇文章主要給大家介紹了關(guān)于npm?install安裝報(bào)錯(cuò):gyp?info?it?worked?if?it?ends?with?ok的解決方法,需要的朋友可以參考下2022-07-07基于node.js實(shí)現(xiàn)微信支付退款功能
在微信開(kāi)發(fā)中有有付款就會(huì)有退款,這樣的功能非常常見(jiàn),這篇文章主要介紹了node.js實(shí)現(xiàn)微信支付退款功能,需要的朋友可以參考下2017-12-12輕松創(chuàng)建nodejs服務(wù)器(3):代碼模塊化
這篇文章主要介紹了輕松創(chuàng)建nodejs服務(wù)器(3):代碼模塊化,本文是對(duì)第一節(jié)的例子作了封裝,需要的朋友可以參考下2014-12-12NodeJs+MySQL實(shí)現(xiàn)注冊(cè)登錄功能
這篇文章主要為大家詳細(xì)介紹了NodeJs+MySQL實(shí)現(xiàn)注冊(cè)登錄功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-04-04手把手教你更優(yōu)雅的修改node_modules里的代碼
這篇文章主要給大家介紹了關(guān)于如何更優(yōu)雅的修改node_modules里的代碼的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2023-02-02