日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

你不知道的Node.js性能优化,读了之后水平直线上升

發(fā)布時(shí)間:2025/3/14 编程问答 35 豆豆
生活随笔 收集整理的這篇文章主要介紹了 你不知道的Node.js性能优化,读了之后水平直线上升 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

本文由云+社區(qū)發(fā)表

“當(dāng)我第一次知道要這篇文章的時(shí)候,其實(shí)我是拒絕的,因?yàn)槲矣X得,你不能叫我寫馬上就寫,我要有干貨才行,寫一些老生常談的然后加上好多特技,那個(gè) Node.js 性能啊好像 Duang~ 的一下就上去了,那讀者一定會(huì)罵我,Node.js 根本沒有這樣搞性能優(yōu)化的,都是假的。” ------ 斯塔克·成龍·王


1、使用最新版本的 Node.js

僅僅是簡單的升級 Node.js 版本就可以輕松地獲得性能提升,因?yàn)閹缀跞魏涡掳姹镜?Node.js 都會(huì)比老版本性能更好,為什么?

Node.js 每個(gè)版本的性能提升主要來自于兩個(gè)方面:

  • V8 的版本更新;
  • Node.js 內(nèi)部代碼的更新優(yōu)化。

例如最新的 V8 7.1 中,就優(yōu)化了某些情形下閉包的逃逸分析,讓 Array 的一些方法得到了性能提升:

Node.js 的內(nèi)部代碼,隨著版本的升級,也會(huì)有明顯的優(yōu)化,比如下面這個(gè)圖就是 require 的性能隨著 Node.js 版本升級的變化:

每個(gè)提交到 Node.js 的 PR 都會(huì)在 review 的時(shí)候考慮會(huì)不會(huì)對當(dāng)前性能造成衰退。同時(shí)也有專門的 benchmarking 團(tuán)隊(duì)來監(jiān)控性能變化,你可以在這里看到 Node.js 的每個(gè)版本的性能變化:

https://benchmarking.nodejs.org/

所以,你可以完全對新版本 Node.js 的性能放心,如果發(fā)現(xiàn)了任何在新版本下的性能衰退,歡迎提交一個(gè) issue。

如何選擇 Node.js 的版本?

這里就要科普一下 Node.js 的版本策略:

  • Node.js 的版本主要分為 Current 和 LTS;
  • Current 就是當(dāng)前最新的、依然處于開發(fā)中的 Node.js 版本;
  • LTS 就是穩(wěn)定的、會(huì)長期維護(hù)的版本;
  • Node.js 每六個(gè)月(每年的四月和十月)會(huì)發(fā)布一次大版本升級,大版本會(huì)帶來一些不兼容的升級;
  • 每年四月發(fā)布的版本(版本號為偶數(shù),如 v10)是 LTS 版本,即長期支持的版本,社區(qū)會(huì)從發(fā)布當(dāng)年的十月開始,繼續(xù)維護(hù) 18 + 12 個(gè)月(Active LTS + Maintaince LTS);
  • 每年十月發(fā)布的版本(版本號為奇數(shù),例如現(xiàn)在的 v11)只有 8 個(gè)月的維護(hù)期。

舉個(gè)例子,現(xiàn)在(2018年11月),Node.js Current 的版本是 v11,LTS 版本是 v10 和 v8。更老的 v6 處于 Maintenace LTS,從明年四月起就不再維護(hù)了。去年十月發(fā)布的 v9 版本在今年六月結(jié)束了維護(hù)。

對于生產(chǎn)環(huán)境而言,Node.js 官方推薦使用最新的 LTS 版本,現(xiàn)在是 v10.13.0。


2、使用 fast-json-stringify 加速 JSON 序列化

在 JavaScript 中,生成 JSON 字符串是非常方便的:

const json = JSON.stringify(obj)

但很少人會(huì)想到這里竟然也存在性能優(yōu)化的空間,那就是使用 JSON Schema 來加速序列化。

在 JSON 序列化時(shí),我們需要識別大量的字段類型,比如對于 string 類型,我們就需要在兩邊加上 ",對于數(shù)組類型,我們需要遍歷數(shù)組,把每個(gè)對象序列化后,用 , 隔開,然后在兩邊加上 [ 和 ],諸如此類等等。

如果已經(jīng)提前通過 Schema 知道每個(gè)字段的類型,那么就不需要遍歷、識別字段類型,而可以直接用序列化對應(yīng)的字段,這就大大減少了計(jì)算開銷,這就是 fast-json-stringfy 的原理。

根據(jù)項(xiàng)目中的跑分,在某些情況下甚至可以比 JSON.stringify 快接近 10 倍!

一個(gè)簡單的示例:

const fastJson = require('fast-json-stringify') const stringify = fastJson({title: 'Example Schema',type: 'object',properties: {name: { type: 'string' },age: { type: 'integer' },books: {type: 'array',items: {type: 'string',uniqueItems: true}}} })console.log(stringify({name: 'Starkwang',age: 23,books: ['C++ Primier', '響け!ユーフォニアム~'] })) //=> {"name":"Starkwang","age":23,"books":["C++ Primier","響け!ユーフォニアム~"]}

在 Node.js 的中間件業(yè)務(wù)中,通常會(huì)有很多數(shù)據(jù)使用 JSON 進(jìn)行,并且這些 JSON 的結(jié)構(gòu)是非常相似的(如果你使用了 TypeScript,更是這樣),這種場景就非常適合使用 JSON Schema 來優(yōu)化。


3、提升 Promise 的性能

Promise 是解決回調(diào)嵌套地獄的靈丹妙藥,特別是當(dāng)自從 async/await 全面普及之后,它們的組合無疑成為了 JavaScript 異步編程的終極解決方案,現(xiàn)在大量的項(xiàng)目都已經(jīng)開始使用這種模式。

但是優(yōu)雅的語法后面也隱藏著性能損耗,我們可以使用 github 上一個(gè)已有的跑分項(xiàng)目進(jìn)行測試,以下是測試結(jié)果:

file time(ms) memory(MB) callbacks-baseline.js 380 70.83 promises-bluebird.js 554 97.23 promises-bluebird-generator.js 585 97.05 async-bluebird.js 593 105.43 promises-es2015-util.promisify.js 1203 219.04 promises-es2015-native.js 1257 227.03 async-es2017-native.js 1312 231.08 async-es2017-util.promisify.js 1550 228.74Platform info: Darwin 18.0.0 x64 Node.JS 11.1.0 V8 7.0.276.32-node.7 Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz × 4

我們可以從結(jié)果中看到,原生 async/await + Promise 的性能比 callback 要差很多,并且內(nèi)存占用也高得多。對于大量異步邏輯的中間件項(xiàng)目而言,這里的性能開銷還是不能忽視的。

通過對比可以發(fā)現(xiàn),性能損耗主要來自于 Promise 對象自身的實(shí)現(xiàn),V8 原生實(shí)現(xiàn)的 Promise 比 bluebird 這樣第三方實(shí)現(xiàn)的 Promise 庫要慢很多。而 async/await 語法并不會(huì)帶來太多的性能損失。

所以對于大量異步邏輯、輕量計(jì)算的中間件項(xiàng)目而言,可以在代碼中把全局的 Promise 換為 bluebird 的實(shí)現(xiàn):

global.Promise = require('bluebird');

4、正確地編寫異步代碼

使用 async/await 之后,項(xiàng)目的異步代碼會(huì)非常好看:

const foo = await doSomethingAsync(); const bar = await doSomethingElseAsync();

但因此,有時(shí)我們也會(huì)忘記使用 Promise 給我們帶來的其它能力,比如 Promise.all() 的并行能力:

// bad async function getUserInfo(id) {const profile = await getUserProfile(id);const repo = await getUserRepo(id)return { profile, repo } }// good async function getUserInfo(id) {const [profile, repo] = await Promise.all([getUserProfile(id),getUserRepo(id)])return { profile, repo } }

還有比如 Promise.any()(此方法不在ES6 Promise標(biāo)準(zhǔn)中,也可以使用標(biāo)準(zhǔn)的 Promise.race() 代替),我們可以用它輕松實(shí)現(xiàn)更加可靠快速的調(diào)用:

async function getServiceIP(name) {// 從 DNS 和 ZooKeeper 獲取服務(wù) IP,哪個(gè)先成功返回用哪個(gè)// 與 Promise.race 不同的是,這里只有當(dāng)兩個(gè)調(diào)用都 reject 時(shí),才會(huì)拋出錯(cuò)誤return await Promise.any([getIPFromDNS(name),getIPFromZooKeeper(name)]) }

5、優(yōu)化 V8 GC

關(guān)于 V8 的垃圾回收機(jī)制,已經(jīng)有很多類似的文章了,這里就不再重復(fù)介紹。推薦兩篇文章:

  • 解讀 V8 GC Log(一): Node.js 應(yīng)用背景與 GC 基礎(chǔ)知識
  • 解讀 V8 GC Log(二): 堆內(nèi)外內(nèi)存的劃分與 GC 算法

我們在日常開發(fā)代碼的時(shí)候,比較容易踩到下面幾個(gè)坑:

坑一:使用大對象作為緩存,導(dǎo)致老生代(Old Space)的垃圾回收變慢

示例:

const cache = {} async function getUserInfo(id) {if (!cache[id]) {cache[id] = await getUserInfoFromDatabase(id)}return cache[id] }

這里我們使用了一個(gè)變量 cache 作為緩存,加速用戶信息的查詢,進(jìn)行了很多次查詢后,cache 對象會(huì)進(jìn)入老生代,并且會(huì)變得無比龐大,而老生代是使用三色標(biāo)記 + DFS 的方式進(jìn)行 GC 的,一個(gè)大對象會(huì)直接導(dǎo)致 GC 花費(fèi)的時(shí)間增長(而且也有內(nèi)存泄漏的風(fēng)險(xiǎn))。

解決方法就是:

  • 使用 Redis 這樣的外部緩存,實(shí)際上像 Redis 這樣的內(nèi)存型數(shù)據(jù)庫非常適合這種場景;
  • 限制本地緩存對象的大小,比如使用 FIFO、TTL 之類的機(jī)制來清理對象中的緩存。

坑二:新生代空間不足,導(dǎo)致頻繁 GC

這個(gè)坑會(huì)比較隱蔽。

Node.js 默認(rèn)給新生代分配的內(nèi)存是 64MB(64位的機(jī)器,后同),但因?yàn)樾律?GC 使用的是 Scavenge 算法,所以實(shí)際能使用的內(nèi)存只有一半,即 32MB。

當(dāng)業(yè)務(wù)代碼頻繁地產(chǎn)生大量的小對象時(shí),這個(gè)空間很容易就會(huì)被占滿,從而觸發(fā) GC。雖然新生代的 GC 比老生代要快得多,但頻繁的 GC 依然會(huì)很大地影響性能。極端的情況下,GC 甚至可以占用全部計(jì)算時(shí)間的 30% 左右。

解決方法就是,在啟動(dòng) Node.js 時(shí),修改新生代的內(nèi)存上限,減少 GC 的次數(shù):

node --max-semi-space-size=128 app.js

當(dāng)然有人肯定會(huì)問,新生代的內(nèi)存是不是越大越好呢?

隨著內(nèi)存的增大,GC 的次數(shù)減少,但每次 GC 所需要的時(shí)間也會(huì)增加,所以并不是越大越好,具體數(shù)值需要對業(yè)務(wù)進(jìn)行壓測 profile 才能確定分配多少新生代內(nèi)存最好。

但一般根據(jù)經(jīng)驗(yàn)而言,分配 64MB 或者 128MB 是比較合理的


6、正確地使用 Stream

Stream 是 Node.js 最基本的概念之一,Node.js 內(nèi)部的大部分與 IO 相關(guān)的模塊,比如 http、net、fs、repl,都是建立在各種 Stream 之上的。

下面這個(gè)經(jīng)典的例子應(yīng)該大部分人都知道,對于大文件,我們不需要把它完全讀入內(nèi)存,而是使用 Stream 流式地把它發(fā)送出去:

const http = require('http'); const fs = require('fs');// bad http.createServer(function (req, res) {fs.readFile(__dirname + '/data.txt', function (err, data) {res.end(data);}); });// good http.createServer(function (req, res) {const stream = fs.createReadStream(__dirname + '/data.txt');stream.pipe(res); });

在業(yè)務(wù)代碼中合理地使用 Stream 能很大程度地提升性能,當(dāng)然是但實(shí)際的業(yè)務(wù)中我們很可能會(huì)忽略這一點(diǎn),比如采用 React 服務(wù)器端渲染的項(xiàng)目,我們就可以用 renderToNodeStream:

const ReactDOMServer require('react-dom/server') const http = require('http') const fs = require('fs') const app = require('./app')// bad const server = http.createServer((req, res) => {const body = ReactDOMServer.renderToString(app)res.end(body) });// good const server = http.createServer(function (req, res) {const stream = ReactDOMServer.renderToNodeStream(app)stream.pipe(res) })server.listen(8000)

使用 pipeline 管理 stream

在過去的 Node.js 中,處理 stream 是非常麻煩的,舉個(gè)例子:

source.pipe(a).pipe(b).pipe(c).pipe(dest)

一旦其中 source、a、b、c、dest 中,有任何一個(gè) stream 出錯(cuò)或者關(guān)閉,會(huì)導(dǎo)致整個(gè)管道停止,此時(shí)我們需要手工銷毀所有的 stream,在代碼層面這是非常麻煩的。

所以社區(qū)出現(xiàn)了 pump 這樣的庫來自動(dòng)控制 stream 的銷毀。而 Node.js v10.0 加入了一個(gè)新的特性:stream.pipeline,可以替代 pump 幫助我們更好的管理 stream。

一個(gè)官方的例子:

const { pipeline } = require('stream'); const fs = require('fs'); const zlib = require('zlib');pipeline(fs.createReadStream('archive.tar'),zlib.createGzip(),fs.createWriteStream('archive.tar.gz'),(err) => {if (err) {console.error('Pipeline failed', err);} else {console.log('Pipeline succeeded');}} );

實(shí)現(xiàn)自己的高性能 Stream

在業(yè)務(wù)中你可能也會(huì)自己實(shí)現(xiàn)一個(gè) Stream,可讀、可寫、或者雙向流,可以參考文檔:

  • implementing Readable streams
  • implementing Writable streams

Stream 雖然很神奇,但自己實(shí)現(xiàn) Stream 也可能會(huì)存在隱藏的性能問題,比如:

class MyReadable extends Readable {_read(size) {while (null !== (chunk = getNextChunk())) {this.push(chunk);}} }

當(dāng)我們調(diào)用 new MyReadable().pipe(xxx) 時(shí),會(huì)把 getNextChunk() 所得到的 chunk 都 push 出去,直到讀取結(jié)束。但如果此時(shí)管道的下一步處理速度較慢,就會(huì)導(dǎo)致數(shù)據(jù)堆積在內(nèi)存中,導(dǎo)致內(nèi)存占用變大,GC 速度降低。

而正確的做法應(yīng)該是,根據(jù) this.push() 返回值選擇正確的行為,當(dāng)返回值為 false 時(shí),說明此時(shí)堆積的 chunk 已經(jīng)滿了,應(yīng)該停止讀入。

class MyReadable extends Readable {_read(size) {while (null !== (chunk = getNextChunk())) {if (!this.push(chunk)) {return false }}} }

這個(gè)問題在 Node.js 官方的一篇文章中有詳細(xì)的介紹:Backpressuring in Streams


7、C++ 擴(kuò)展一定比 JavaScript 快嗎?

Node.js 非常適合 IO 密集型的應(yīng)用,而對于計(jì)算密集的業(yè)務(wù),很多人都會(huì)想到用編寫 C++ Addon 的方式來優(yōu)化性能。但實(shí)際上 C++ 擴(kuò)展并不是靈丹妙藥,V8 的性能也沒有想象的那么差。

比如,我在今年九月份的時(shí)候把 Node.js 的 net.isIPv6() 從 C++ 遷移到了 JS 的實(shí)現(xiàn),讓大多數(shù)的測試用例都獲得了 10%- 250% 不等的性能提升(具體PR可以看這里)。

JavaScript 在 V8 上跑得比 C++ 擴(kuò)展還快,這種情況多半發(fā)生在與字符串、正則表達(dá)式相關(guān)的場景,因?yàn)?V8 內(nèi)部使用的正則表達(dá)式引擎是 irregexp,這個(gè)正則表達(dá)式引擎比 boost 中自帶的引擎(boost::regex)要快得多。

還有一處值得注意的就是,Node.js 的 C++ 擴(kuò)展在進(jìn)行類型轉(zhuǎn)換的時(shí)候,可能會(huì)消耗非常多的性能,如果不注意 C++ 代碼的細(xì)節(jié),性能會(huì)很大地下降。

這里有一篇文章對比了相同算法下 C++ 和 JS 的性能(需翻墻):How to get a performance boost using Node.js native addons。其中值得注意的結(jié)論就是,C++ 代碼在對參數(shù)中的字符串進(jìn)行轉(zhuǎn)換后(String::Utf8Value轉(zhuǎn)為std::string),性能甚至不如 JS 實(shí)現(xiàn)的一半。只有在使用 NAN 提供的類型封裝后,才獲得了比 JS 更高的性能。

換句話說,C++ 是否比 JavaScript 更加高效需要具體問題具體分析,某些情況下,C++ 擴(kuò)展不一定就會(huì)比原生 JavaScript 更高效。如果你對自己的 C++ 水平不是那么有信心,其實(shí)還是建議用 JavaScript 來實(shí)現(xiàn),因?yàn)?V8 的性能比你想象的要好得多。


8、使用 node-clinic 快速定位性能問題

說了這么多,有沒有什么可以開箱即用,五分鐘見效的呢?當(dāng)然有。

node-clinic 是 NearForm 開源的一款 Node.js 性能診斷工具,可以非常快速地定位性能問題。

npm i -g clinic npm i -g autocannon

使用的時(shí)候,先開啟服務(wù)進(jìn)程:

clinic doctor -- node server.js

然后我們可以用任何壓測工具跑一次壓測,比如使用同一個(gè)作者的 autocannon(當(dāng)然你也可以使用 ab、curl 這樣的工具來進(jìn)行壓測。):

autocannon http://localhost:3000

壓測完畢后,我們 ctrl + c 關(guān)閉 clinic 開啟的進(jìn)程,就會(huì)自動(dòng)生成報(bào)告。比如下面就是我們一個(gè)中間件服務(wù)的性能報(bào)告:

我們可以從 CPU 的使用曲線看出,這個(gè)中間件服務(wù)的性能瓶頸不在自身內(nèi)部的計(jì)算,而在于 I/O 速度太慢。clinic 也在上面告訴我們檢測到了潛在的 I/O 問題。

下面我們使用 clinic bubbleprof 來檢測 I/O 問題:

clinic bubbleprof -- node server.js

再次進(jìn)行壓測后,我們得到了新的報(bào)告:

這個(gè)報(bào)告中,我們可以看到,http.Server 在整個(gè)程序運(yùn)行期間,96% 的時(shí)間都處于 pending 狀態(tài),點(diǎn)開后,我們會(huì)發(fā)現(xiàn)調(diào)用棧中存在大量的 empty frame,也就是說,由于網(wǎng)絡(luò) I/O 的限制,CPU 存在大量的空轉(zhuǎn),這在中間件業(yè)務(wù)中非常常見,也為我們指明了優(yōu)化方向不在服務(wù)內(nèi)部,而在服務(wù)器的網(wǎng)關(guān)和依賴的服務(wù)相應(yīng)速度上。

想知道如何讀懂 clinic bubbleprof 生成的報(bào)告,可以看這里:https://clinicjs.org/bubbleprof/walkthrough/

同樣,clinic 也可以檢測到服務(wù)內(nèi)部的計(jì)算性能問題,下面我們做一些“破壞”,讓這個(gè)服務(wù)的性能瓶頸出現(xiàn)在 CPU 計(jì)算上。

我們在某個(gè)中間件中加入了空轉(zhuǎn)一億次這樣非常消耗 CPU 的“破壞性”代碼:

function sleep() {let n = 0while (n++ < 10e7) {empty()} } function empty() { }module.exports = (ctx, next) => {sleep()// ......return next() }

然后使用 clinic doctor,重復(fù)上面的步驟,生成性能報(bào)告:

這就是一個(gè)非常典型的同步計(jì)算阻塞了異步隊(duì)列的“病例”,即主線程上進(jìn)行了大量的計(jì)算,導(dǎo)致 JavaScript 的異步回調(diào)沒法及時(shí)觸發(fā),Event Loop 的延遲極高。

對于這樣的應(yīng)用,我們可以繼續(xù)使用 clinic flame 來確定到底是哪里出現(xiàn)了密集計(jì)算:

clinic flame -- node app.js

壓測后,我們得到了火焰圖(這里把空轉(zhuǎn)次數(shù)減少到了100萬次,讓火焰圖看起來不至于那么極端):

從這張圖里,我們可以明顯看到頂部的那個(gè)大白條,它代表了 sleep 函數(shù)空轉(zhuǎn)所消耗的 CPU 時(shí)間。根據(jù)這樣的火焰圖,我們可以非常輕松地看出 CPU 資源的消耗情況,從而定位代碼中哪里有密集的計(jì)算,找到性能瓶頸。

此文已由作者授權(quán)騰訊云+社區(qū)發(fā)布


轉(zhuǎn)載于:https://www.cnblogs.com/qcloud1001/p/10084223.html

總結(jié)

以上是生活随笔為你收集整理的你不知道的Node.js性能优化,读了之后水平直线上升的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。