日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時(shí)間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營(yíng)銷解決方案
「NodeJs進(jìn)階」超全面的Node.js性能優(yōu)化相關(guān)知識(shí)梳理

相信對(duì)于前端同學(xué)而言,我們?nèi)ラ_發(fā)一個(gè)自己的簡(jiǎn)單后端程序可以借助很多的??nodeJs??的框架去進(jìn)行快速搭建,但是從前端面向后端之后,我們會(huì)在很多方面會(huì)稍顯的有些陌生,比如性能分析性能測(cè)試,內(nèi)存管理,內(nèi)存查看,使用C++插件,子進(jìn)程,多線程,Cluster模塊,進(jìn)程守護(hù)管理等等??NodeJs??后端的知識(shí),在這里為大家來(lái)分析一下這些場(chǎng)景與具體實(shí)現(xiàn)。

創(chuàng)新互聯(lián)建站網(wǎng)站建設(shè)提供從項(xiàng)目策劃、軟件開發(fā),軟件安全維護(hù)、網(wǎng)站優(yōu)化(SEO)、網(wǎng)站分析、效果評(píng)估等整套的建站服務(wù),主營(yíng)業(yè)務(wù)為成都網(wǎng)站制作、成都網(wǎng)站設(shè)計(jì),成都app軟件開發(fā)以傳統(tǒng)方式定制建設(shè)網(wǎng)站,并提供域名空間備案等一條龍服務(wù),秉承以專業(yè)、用心的態(tài)度為用戶提供真誠(chéng)的服務(wù)。創(chuàng)新互聯(lián)建站深信只要達(dá)到每一位用戶的要求,就會(huì)得到認(rèn)可,從而選擇與我們長(zhǎng)期合作。這樣,我們也可以走得更遠(yuǎn)!

搭建基礎(chǔ)服務(wù)

首先我們先來(lái)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的??Http服務(wù)器??,為了演示方便這里我們使用??express??,代碼如下:

const fs = require('fs')
const express = require('express')
const app = express()

app.get('/', (req, res) => {
res.end('hello world')
})

app.get('/index', (req, res) => {
const file = fs.readFileSync(__dirname + '/index.html', 'utf-8')
/* return buffer */
res.end(file)
/* return stream */
// fs.createReadStream(__dirname + '/index.html').pipe(res)
})

app.listen(3000)

正常情況我們大部分的后端服務(wù)是聯(lián)合??db??最終返回一些列的接口信息的,但是為了后面的一些測(cè)試,這里我們返回了一個(gè)文件,因?yàn)榇笠稽c(diǎn)的返回信息可以直觀的感受我們的服務(wù)性能與瓶頸。

額外一點(diǎn),在上面可以看到我們?cè)谧⑨尩牡胤揭彩褂昧艘粋€(gè)??stream流??的形式進(jìn)行了返回,如果我們返回的是文件,第一種的同步讀取其實(shí)相對(duì)更耗時(shí),如果是個(gè)大的文件,會(huì)在內(nèi)存空間先去存儲(chǔ),拿到全部的文件之后才會(huì)一次返回,這樣的性能包括內(nèi)存占用在文件較大的時(shí)候更為明顯。

所以如果我們做的是??ssr??或者文件下載之類的東西我們都可以以這樣流的形式去做更加高效,至此,我們已經(jīng)有了一個(gè)簡(jiǎn)單的??http服務(wù)??了,接下來(lái)我們對(duì)齊進(jìn)行擴(kuò)展。

性能測(cè)試、壓測(cè)

首先我們需要借助測(cè)試工具模擬在高并發(fā)情況下的狀態(tài),這里我推薦兩種壓測(cè)工具。

  • ab 官方文檔
  • webbench
  • autocannon

本次我們使用??ab??壓測(cè)工具來(lái)進(jìn)行接下來(lái)的操作,所以這里為大家介紹一下??ab??。那么??ab??呢是??apache??公司的一款工具,??mac??系統(tǒng)是自帶這個(gè)工具的,安裝教程呢大家就自行去查看,當(dāng)然??mac??自帶的??ab??是有并發(fā)限制的。

然后我們先隨便來(lái)一條簡(jiǎn)單的命令再為大家分析一下具體的參數(shù)

ab -c200 -n1600 http://127.0.0.1:3000/index

上面這條命令的意思呢就是測(cè)試接口地址??http://127.0.0.1:3000/index??對(duì)齊每秒200個(gè)請(qǐng)求,并請(qǐng)求總數(shù)1600次這樣的一個(gè)壓測(cè),然后我們看看這個(gè)工具的其他參數(shù)吧

參數(shù)

解釋

??-c concurrency??

設(shè)定并發(fā)數(shù),默認(rèn)并發(fā)數(shù)是 1

??-n requests??

設(shè)定壓測(cè)的請(qǐng)求總數(shù)

??-t timelimit??

設(shè)定壓測(cè)的時(shí)長(zhǎng),單位是秒

??-p POST-file??

設(shè)定 POST 文件路徑,注意設(shè)定匹配的 ??-T?? 參數(shù)

??-T content-type??

設(shè)定 ??POST/PUT??? 的數(shù)據(jù)格式,默認(rèn)為 ??text/plain??

??-V??

查看版本信息

??-w??

以Html表格形式輸出

參數(shù)并不多很簡(jiǎn)單,當(dāng)然我們需要看看壓測(cè)之后的結(jié)果,這才是我們需要的東西。

上面的東西呢其實(shí)已經(jīng)很直觀了,最開頭的部分就是每秒請(qǐng)求成功了多少個(gè),其次就是請(qǐng)求地址、端口、路徑、大小、這些其實(shí)不是很重要,我們?cè)跒g覽器中自己也可以看到,我們主要需要注意的性能指標(biāo)是下面這些參數(shù):

Complete requests:      1600 # 請(qǐng)求完成成功數(shù) 這里判斷的依據(jù)是返回碼為200代表成功
Failed requests: 0 # 請(qǐng)求完成失敗數(shù)
Total transferred: 8142400 bytes # 本次測(cè)試傳輸?shù)目倲?shù)據(jù)
HTML transferred: 7985600 bytes
Requests per second: 2188.47 [#/sec] (mean) # QPS 每秒能夠處理的并發(fā)量
Time per request: 91.388 [ms] (mean) # 每次請(qǐng)求花費(fèi)的平均時(shí)常
Time per request: 0.457 [ms] # 多久一個(gè)并發(fā)可以得到結(jié)果
Transfer rate: 10876.09 [Kbytes/sec] received # 吞吐量 每秒服務(wù)器可以接受多少數(shù)據(jù)傳輸量

一般而言我們只需要注意最后四條即可,首先可以直觀知道當(dāng)前服務(wù)器能承受的并發(fā),同時(shí)我們可以知道服務(wù)器的瓶頸來(lái)自于哪里,如何分析呢?如果這里的吞吐量剛好是我們服務(wù)器的網(wǎng)卡帶寬一樣高,說(shuō)明瓶頸來(lái)自于我們的帶寬,而不是來(lái)自于其他例如cpu,內(nèi)存,硬盤等等,那么我們其他的如何查看呢,我們可以借助這兩個(gè)命令:

  • top 監(jiān)控計(jì)算機(jī)cpu和內(nèi)存使用情況
  • iostat 檢測(cè)io設(shè)備的帶寬的

我們就可以在使用??ab壓測(cè)??的過程中實(shí)時(shí)查看服務(wù)器的狀態(tài),看看瓶頸來(lái)自于cpu、內(nèi)存帶寬等等對(duì)癥下藥。

當(dāng)然存在一種特殊情況,很多場(chǎng)景下NodeJs只是作為BFF這個(gè)時(shí)候假如我們的Node層能處理600的qps但是后端只支持300,那么這個(gè)時(shí)候的瓶頸來(lái)自于后端。

在某些情況下,負(fù)載滿了可能也會(huì)是NodeJs的計(jì)算性能達(dá)到了瓶頸,可能是某一處的代碼所導(dǎo)致的,我們?nèi)绾稳フ业?strong>NodeJs的性能瓶頸呢,這一點(diǎn)我們接下來(lái)說(shuō)說(shuō)。

Nodejs性能分析工具

profile

NodeJs自帶了profile工具,如何使用呢,就是在啟動(dòng)的時(shí)候加上**--prof**即可,??node --prof index.js??,當(dāng)我們啟動(dòng)服務(wù)器的時(shí)候,目錄下會(huì)立馬生成一個(gè)文件??isolate-0x104a0a000-25750-v8.log??,我們先不用關(guān)注這個(gè)文件,我們重新進(jìn)行一次15秒的壓測(cè):

ab -c50 -t15 http://127.0.0.1:3000/index

等待壓測(cè)結(jié)束后,我們的這個(gè)文件就發(fā)生了變化,但是里面的數(shù)據(jù)很長(zhǎng)我們還需要進(jìn)行解析。

使用NodeJs自帶的命令 ??node --prof-process isolate-0x104a0a000-25750-v8.log > profile.txt??

這個(gè)命令呢就是把我們生成的日志文件轉(zhuǎn)為txt格式存在當(dāng)前目錄下,并且更為直觀可以看到,但是這種文字類型的對(duì)我來(lái)說(shuō)也不是足夠方便,我們大致說(shuō)說(shuō)里面的內(nèi)容吧,就不上圖了,里面包含了,里面有js,c++,gc等等的各種調(diào)用次數(shù),占用時(shí)間,還有各種的調(diào)用棧信息等等,這里你可以手動(dòng)實(shí)現(xiàn)之后看看。

總體來(lái)說(shuō)還是不方便查看,所以我們采用另一種方式。

chrome devtools

因?yàn)槲覀冎?strong>NodeJs是基礎(chǔ)chrome v8引擎javascript運(yùn)行環(huán)境,所以我們調(diào)試NodeJs也是可以對(duì)NodeJs進(jìn)行調(diào)試的。這里我們要使用新的參數(shù)??--inspect??, ??-brk??代表啟動(dòng)調(diào)試的同時(shí)暫停程序運(yùn)行,只有我們進(jìn)入的時(shí)候才往下走。

??node --inspect-brk index.js??

(base) xiaojiu@192 node-share % node --inspect-brk index.js
Debugger listening on ws://127.0.0.1:9229/e9f0d9b5-cdfd-45f1-9d0e-d77dfbf6e765
For help, see: https://nodejs.org/en/docs/inspector

運(yùn)行之后我們看到他就告訴我們監(jiān)聽了一個(gè)??websocket??,我們就可以通過這個(gè)ws進(jìn)行調(diào)試了。

我們進(jìn)入到chrome瀏覽器然后在地址欄輸入??chrome://inspect??

然后我們可以看到other中有一個(gè)Target,上面輸出了版本,我們只需要點(diǎn)擊最后一行的那個(gè)inspect就可以進(jìn)入調(diào)試了。進(jìn)入之后我們發(fā)現(xiàn),上面就可以完完整整看到我們寫的源代碼了。

并且我們進(jìn)入的時(shí)候已經(jīng)是暫停狀態(tài)了,需要我們手動(dòng)下去,這里和前端調(diào)試都大同小異了,相信這里大家都不陌生了。

除此之外,我們可以看到其他幾個(gè)面板,Console:控制臺(tái)、Memory:內(nèi)存監(jiān)控Profile:CPU監(jiān)控,

CPU監(jiān)控

我們可以進(jìn)入到Memory面板,點(diǎn)擊左上角的原點(diǎn)表示開始監(jiān)控,這個(gè)時(shí)候進(jìn)行一輪例如上面的15s壓測(cè),壓測(cè)結(jié)束后我們點(diǎn)擊stop按鈕,這個(gè)時(shí)候就可以生成這個(gè)時(shí)間段的詳細(xì)數(shù)據(jù)了,結(jié)果如下:

我們也可點(diǎn)擊??hHeavy按鈕??切換這個(gè)數(shù)據(jù)展現(xiàn)形式為圖表等其他方式,大家自己試試,那么從這個(gè)數(shù)據(jù)中,我們可以得到什么呢?在這其中記錄了所有的調(diào)用棧,調(diào)用時(shí)間,耗時(shí)等等,我們可以詳細(xì)的知道,我們代碼中每一行或者每一步的花費(fèi)時(shí)間,這樣再對(duì)代碼優(yōu)化的話是完全有跡可循的,同時(shí)我們使用圖表的形式也可以更為直觀的查看的,當(dāng)然這里不僅僅可以調(diào)試本地的,也可以通過服務(wù)器ip在設(shè)置中去調(diào)試遠(yuǎn)端服務(wù)器的,當(dāng)然可能速度會(huì)相對(duì)慢一點(diǎn),可以自己去嘗試。同時(shí)我們也可以借助一些其他的三方包,比如clinic,有興趣的各位可以自己去查看一下。

我們看他的意義是什么呢,當(dāng)然是分析各個(gè)動(dòng)作的耗時(shí)然后對(duì)齊進(jìn)行代碼優(yōu)化了,接下來(lái)怎么優(yōu)化呢?

代碼性能優(yōu)化

通過上面的分析,我們可以看到花費(fèi)時(shí)間最長(zhǎng)的是readFileSync,很明顯是讀取代碼,那么我們對(duì)最最初的代碼進(jìn)行分析,可以看到當(dāng)我們每次訪問/indexd路徑的時(shí)候都會(huì)去重新讀取文件,那么很明顯這一步就是我們優(yōu)化的點(diǎn),我們稍加改造:

const fs = require('fs')
const express = require('express')
const app = express()
app.get('/', (req, res) => {
res.end('hello world')
})
/* 提取到外部每次程序只會(huì)讀取一次 提高性能 */
const file = fs.readFileSync(__dirname + '/index.html', 'utf-8')
app.get('/index', (req, res) => {
/* return buffer */
res.end(file)
/* return stream */
// fs.createReadStream(__dirname + '/index.html').pipe(res)
})
app.listen(3000)

為了直觀感受,我們?cè)诟脑烨昂蠓謩e壓測(cè)一次看看,這里呢就不上圖了,大家可以自己動(dòng)手,會(huì)發(fā)現(xiàn)這樣的操作可以讓你的qps可以直接翻倍,可以看到,這樣分析處出來(lái)的結(jié)果,再對(duì)代碼改造可以提高非常大的效率。

同時(shí)除此之外,還有一個(gè)地方可以優(yōu)化,我們發(fā)現(xiàn)上圖我點(diǎn)開的箭頭部分有一個(gè)byteLengthUtf8這樣的一個(gè)步驟,可以看出他是獲取我們文件的一個(gè)長(zhǎng)度,因?yàn)槲覀冎付松戏降墨@取格式是utf-8,那么我們想想獲取長(zhǎng)度是為了什么呢?因?yàn)?strong>NodeJs的底層是基于C++,最終識(shí)別的數(shù)據(jù)結(jié)構(gòu)還是buffer,所以思路就來(lái)了,我們直接為其傳遞一個(gè)buffer是不是就更快了呢?

事實(shí)確實(shí)如此,readFileSync不指定格式的時(shí)候默認(rèn)就是Buffer,當(dāng)我們?nèi)サ糁付愋偷臅r(shí)候,再去壓測(cè),發(fā)現(xiàn)qps再次增加了,所以在這里我們明白,在很多操作中使用buffer的形式可以提高代碼的效率與性能。

當(dāng)然還有許多其他的點(diǎn),那些地方的優(yōu)化可能就不太容易了,但是我們只需要去處理這些占用大頭的點(diǎn)就已經(jīng)足夠了,我們只需要知道去優(yōu)化的手段與思路,剛剛這個(gè)的優(yōu)化就是把一些需要計(jì)算啊或者讀取這種需要時(shí)間的操作移動(dòng)到服務(wù)啟動(dòng)之前去完成就可以做到一個(gè)比較好的性能思想,那么我們性能優(yōu)化需要考慮哪些點(diǎn)呢?

性能優(yōu)化的準(zhǔn)則

  • 減少不必要的計(jì)算:NodeJs中計(jì)算會(huì)占用相當(dāng)大的一部分cpu,包括一些文件的編解碼等等,盡量要避免這些操作。
  • 空間換時(shí)間:比如上面這種讀取,或者一些計(jì)算,我們可以緩存起來(lái),下次讀取的時(shí)候直接調(diào)用。

掌握這兩點(diǎn),我們?cè)诰幋a過程中要盡量思考某些計(jì)算是否可以提前,盡量做到在服務(wù)啟動(dòng)階段去進(jìn)行處理,把在服務(wù)階段的計(jì)算提前到啟動(dòng)階段就可以做到不錯(cuò)的提升效果。

內(nèi)存管理

垃圾回收機(jī)制

我們都知道javascript的內(nèi)存管理都是由語(yǔ)言自己來(lái)做,不需要開發(fā)者來(lái)做,我們也知道其是通過GC垃圾回收機(jī)制實(shí)現(xiàn)的,我們粗略聊一下,一般來(lái)說(shuō)呢,垃圾回收機(jī)制分為,新生代和老生代兩部分,所有新創(chuàng)建的變量都會(huì)先進(jìn)入新生代部分,當(dāng)新生代內(nèi)存區(qū)域快要分配滿的時(shí)候,就會(huì)進(jìn)行一次垃圾回收,把無(wú)用的變量清楚出去給新的變量使用,同時(shí),如果一個(gè)變量在多次垃圾回收之后依然存在,那么則認(rèn)為其是一個(gè)常用且不會(huì)輕易移除的變量,就會(huì)將其放入老生代區(qū)域,這樣一個(gè)循環(huán),同時(shí),老生代區(qū)域容量更大,垃圾回收相對(duì)更慢一些。

  • 新生代:容量小、垃圾回收更快
  • 老生代:容量大,垃圾回收更慢

所以減少內(nèi)存的使用也是提高服務(wù)性能的手段之一,如果有內(nèi)存泄漏,會(huì)導(dǎo)致服務(wù)器性能大大降低。

內(nèi)存泄漏問題處理與修復(fù)

剛剛我們上面介紹過Memory面板,可以檢測(cè),如何使用呢,點(diǎn)擊面板之后點(diǎn)擊右上角遠(yuǎn)點(diǎn)會(huì)產(chǎn)生一個(gè)快照,顯示當(dāng)前使用了多少內(nèi)存空間,正常狀態(tài)呢,我就不為大家演示了,一般如何檢測(cè)呢,就是在服務(wù)啟動(dòng)時(shí)截取一個(gè)快照,在壓測(cè)結(jié)束后再截取一個(gè)看看雙方差異,你也可以在壓測(cè)的過程中截取快照查看,我們先去修改一些代碼制造一個(gè)內(nèi)存泄漏的現(xiàn)場(chǎng),改動(dòng)如下:

const fs = require('fs')
const express = require('express')
const app = express()
app.get('/', (req, res) => {
res.end('hello world')
})

const cache = []
/* 提取到外部每次程序只會(huì)讀取一次 提高性能 */
const file = fs.readFileSync(__dirname + '/index.html', 'utf-8')
app.get('/index', (req, res) => {
/* return buffer */
cache.push(file)
res.end(file)
/* return stream */
// fs.createReadStream(__dirname + '/index.html').pipe(res)
})
app.listen(3000)

我們每次請(qǐng)求都把讀取的這個(gè)文件添加到cache數(shù)組,那么意味著請(qǐng)求越多,這個(gè)數(shù)組將會(huì)越大,我們和之前一樣 ,先打開調(diào)試,同時(shí)截取一份快照,然后開始?jí)簻y(cè),壓測(cè)結(jié)束再截圖一份,也可以在壓測(cè)過程中多次截圖,得到如下:

我們?cè)趬簻y(cè)過程中不斷截取快照發(fā)現(xiàn)內(nèi)存一直在加大,這就是很直觀的可以看到內(nèi)存泄漏,而且因?yàn)槲覀兊奈募淮?,如果是一個(gè)更大的文件,會(huì)看起來(lái)差異更懸殊,然后我們點(diǎn)擊Comparsion按鈕位置,選擇完快照之后進(jìn)行比較,然后點(diǎn)擊占用最大的那一列,點(diǎn)擊之后我們就能看到詳細(xì)信息了,此次泄漏就是cache變量所導(dǎo)致的,對(duì)齊進(jìn)行修復(fù)即可,在我們知道如何修復(fù)和檢測(cè)內(nèi)存泄漏之后,我們就應(yīng)該明白,減少內(nèi)存的使用是提高性能的一大助力,那么我們?nèi)绾螠p少內(nèi)存的使用呢?

控制內(nèi)存使用

在此之前我們聊聊NodeJsBuffer的內(nèi)存分配策略,他會(huì)分為兩種情況,一種是小于8kb的文件,一種是大于8kb的文件,小于8kb的文件NodeJs認(rèn)為頻繁的去創(chuàng)建沒有必要,所以每次都會(huì)先創(chuàng)建一個(gè)8kb的空間,然后得到空間之后的去計(jì)算buffer的占用空間,如果小于8kb就在8kb中給它切一部分使用,依次內(nèi)推,如果遇到一個(gè)小于8kb的buffer使余下的空間不夠使用的時(shí)候就會(huì)去開辟新的一份8kb空間,在這期間,如何有任何變量被銷毀,則這個(gè)空間就會(huì)被釋放,讓后面的使用,這就是NodeJsBuffer的空間分配機(jī)制,這種算法類似于一種的概覽。如果在我們的編碼中也會(huì)遇到內(nèi)存緊張的問題,那么我們也可以采取這種策略。

至此我們對(duì)于內(nèi)存監(jiān)控已經(jīng)查找已經(jīng)學(xué)會(huì)了,接下來(lái)我們來(lái)看看多進(jìn)程如何使用與優(yōu)化。

Node多進(jìn)程使用優(yōu)化

現(xiàn)在的計(jì)算機(jī)一般呢都搭載了多核的cpu,所以我們?cè)诰幊痰臅r(shí)候可以考慮怎么去使用多進(jìn)程或者多線程來(lái)盡量利用這些多核cpu來(lái)提高我們的性能。

在此之前,我們要先了解一下進(jìn)程和線程的概覽:

  • 進(jìn)程:擁有系統(tǒng)掛載運(yùn)行程序的單元 擁有一些獨(dú)立的資源,比如內(nèi)存空間
  • 線程:進(jìn)行運(yùn)算調(diào)度的單元 進(jìn)程內(nèi)的線程共享進(jìn)程內(nèi)的資源 一個(gè)進(jìn)程是可以擁有多個(gè)線程的

NodeJs中一般啟動(dòng)一個(gè)服務(wù)會(huì)有一個(gè)主線程和四個(gè)子線程,我們簡(jiǎn)單來(lái)理解其概覽呢,可以把進(jìn)程當(dāng)做一個(gè)公司,線程當(dāng)做公司的職工,職工共享公司的資源來(lái)進(jìn)行工作。

NodeJs中,主線程運(yùn)行v8javascript,主線程相當(dāng)于公司老板負(fù)責(zé)主要流程和下發(fā)各種工作,通過時(shí)間循環(huán)機(jī)制、LibUv再由四個(gè)子線程去進(jìn)行工作。

因?yàn)?strong>js是一門單線程的語(yǔ)言,它正常情況下只能使用到一個(gè)cpu,不過其子線程在 底層也使用到了其他cpu,但是依然沒有完全解放多核的能力,當(dāng)計(jì)算任務(wù)過于繁重的時(shí)候,我們就可以也在其他的cpu上跑一個(gè)javascript的運(yùn)行環(huán)境,那么我么先來(lái)看看如何用子進(jìn)程來(lái)調(diào)用吧。

進(jìn)程的使用 child_process

我們創(chuàng)建兩個(gè)文件,??master.js??和??child.js??,并且寫入如下代碼,

/* master.js */
/* 自帶的子進(jìn)程模塊 */
const cp = require('child_process')
/* fork一個(gè)地址就是啟動(dòng)了一個(gè)子進(jìn)程 */
const child_process = cp.fork(__dirname + '/child.js')
/* 通過send方法給子進(jìn)程發(fā)送消息 */
child_process.send('主進(jìn)程發(fā)這個(gè)消息給子進(jìn)程')
/* 通過 on message響應(yīng)接收到子進(jìn)程的消息 */
child_process.on('message', (str) => {
console.log('主進(jìn)程: 接收到來(lái)自自進(jìn)程的消息', str);
})


/* chlid.js */
/* 通過on message 響應(yīng)父進(jìn)程傳遞的消息 */
process.on('message', (str) => {
console.log('子進(jìn)程, 收到消息', str)
/* process是全局變量 通過send發(fā)送給父進(jìn)程 */
process.send('子進(jìn)程發(fā)給主進(jìn)程的消息')
})

如上,就是一個(gè)使用子進(jìn)程的簡(jiǎn)單實(shí)現(xiàn)了,看起來(lái)和ws很像。每fork一次便可以開啟一個(gè)子進(jìn)程,我們可以fork多次,fork多少個(gè)合適呢,我們后邊再說(shuō)。

子線程 WOKer Threads

在v10版本之后,NodeJs也提供了子線程的能力,在官方文檔中解釋到,官方認(rèn)為自己的事件循環(huán)機(jī)制已經(jīng)做的夠好足夠使用了,就沒必要去為開發(fā)者提供這個(gè)接口,并且在文檔中寫到,他可以對(duì)計(jì)算有所幫助,但是對(duì)io操作是沒有任何變化的,有興趣可以去看看這個(gè)模塊,除此之外,我們可以有更簡(jiǎn)單的方式去使用多核的服務(wù),接下來(lái)我們聊聊內(nèi)置模塊cluster

Cluster模塊

在此之前我們來(lái)聊聊NodeJs的部署,熟悉NodeJs的同學(xué)應(yīng)該都使用過Pm2,利用其可以進(jìn)程提高不熟的性能,其實(shí)現(xiàn)原理就是基于這種模塊,如果我們可以在不同的核分別去跑一個(gè)http服務(wù)那么是不是類似于我們后端的集群,部署多套服務(wù)呢,當(dāng)客戶端發(fā)送一個(gè)Http請(qǐng)求的時(shí)候進(jìn)入到我們的master node,當(dāng)我們收到請(qǐng)求的時(shí)候,我們把其請(qǐng)求發(fā)送給子進(jìn)程,讓子進(jìn)程自己處理完之后返回給我,由主進(jìn)程將其發(fā)送回去,那么這樣我們是不是就可以利用服務(wù)器的多核呢?

答案是肯定的,同時(shí)這些都不需要我們做過多的東西,這個(gè)模塊就幫我們實(shí)現(xiàn)了,然后我們來(lái)實(shí)現(xiàn)一個(gè)這樣的服務(wù),我們創(chuàng)建兩個(gè)文件??app.js??,??cluster.js??,第一個(gè)文件呢就是我們?nèi)粘5膯?dòng)文件,我們來(lái)簡(jiǎn)單的,使用我們的最開始的那個(gè)服務(wù)即可:

/* cluster.js */
const cluster = require('cluster')

/* 判斷如果是主線程那么就啟動(dòng)三個(gè)子線程 */
if(cluster.isMaster){
cluster.fork()
cluster.fork()
cluster.fork()
} else {
/* 如果是子進(jìn)程就去加載啟動(dòng)文件 */
require('./index.js')
}

就這樣簡(jiǎn)單的代碼就可以讓我們的請(qǐng)求分發(fā)到不同的子進(jìn)程里面去,這一點(diǎn)類似于負(fù)載均衡,非常簡(jiǎn)單,同時(shí)我們?cè)趩⒂枚嗑€程和沒啟動(dòng)的前后分別壓測(cè),可以發(fā)現(xiàn)啟用后的qps是前者的2.5倍擁有很大的一個(gè)提升了,也可以知道進(jìn)程直接的通信是有損耗的,不然應(yīng)該就是三倍了,那么我們要開啟多少個(gè)子進(jìn)程比較合適呢。我們可以使用內(nèi)置模塊OS,來(lái)獲取到當(dāng)前計(jì)算機(jī)的cpu核數(shù)的,我們加一點(diǎn)簡(jiǎn)單改造:

const cluster = require('cluster')
const os = require('os')

/* 判斷如果是主線程那么就啟動(dòng)三個(gè)子線程 */
if(cluster.isMaster){
/* 多少個(gè)cpu啟動(dòng)多少個(gè)子進(jìn)程 */
for (let i = 0; i < os.cpus().length; i++) cluster.fork()
} else {
/* 如果是子進(jìn)程就去加載啟動(dòng)文件 */
require('./index.js')
}

這樣我們就可以準(zhǔn)確知道計(jì)算機(jī)有多少個(gè)cpu我們最多可以啟動(dòng)多少個(gè)子進(jìn)程了,這時(shí)我們進(jìn)行壓測(cè)發(fā)現(xiàn)qps更多了,當(dāng)然并不是啟動(dòng)的越多就越好,前面我們說(shuō)到。NodeJs的底層是用到了其他cpu的所以,我們這里一般來(lái)說(shuō)只需要os.cpus().length / 2的數(shù)量最為合適,就這么簡(jiǎn)單我們就使用到了其他cpu實(shí)現(xiàn)了一個(gè)類似負(fù)載均衡概念的服務(wù)。

當(dāng)然這里有一個(gè)疑問,我們手動(dòng)啟動(dòng)多次node app.js為什么不行呢?很明顯會(huì)報(bào)錯(cuò)端口占用,我們知道,正常情況下計(jì)算機(jī)的一個(gè)端口只能被監(jiān)聽一次,我們這里監(jiān)聽了多次實(shí)際就是有NodeJs在其底層完成的,這里的實(shí)現(xiàn)呢就相對(duì)復(fù)雜需要看源碼了,這里就不過多了解了,有興趣的同學(xué)可以自己去研究一下。

如果你做完這些操作,相信你的服務(wù)性能已經(jīng)提高了很大一截了。接下來(lái)我們來(lái)聊聊關(guān)于其穩(wěn)定性的安全。

NodeJs進(jìn)程守護(hù)與管理

基本上各種NodeJs框架都會(huì)有全局捕獲錯(cuò)誤,但是一般自己去編碼的過程中沒有去做try catch的操作就可能導(dǎo)致你的服務(wù)直接因?yàn)橐粋€(gè)小錯(cuò)誤直接掛掉,為了提高其穩(wěn)定性,我們要去實(shí)現(xiàn)一個(gè)守護(hù),我們用原生的node來(lái)創(chuàng)建一個(gè)服務(wù),不做異常處理的情況下,如果是框架可能很多框架已經(jīng)幫你做過這部分東西了,所以我們自己來(lái)實(shí)現(xiàn)看看吧:

const fs = require('fs')
const http = require('http')

const app = http.createServer( function(req,res) {
res.writeHead(200, { 'content-type': 'text/html'})
console.log(window.xxx)
res.end(fs.readFileSync(__dirname + './index.html', 'utf-8'))
} )

app.listen(3000, () => {
console.log(`listen in 3000`);
})

我們?cè)谡?qǐng)求時(shí)去打印一個(gè)不存在的變量,我們?nèi)フ?qǐng)求的話就會(huì)進(jìn)行一個(gè)報(bào)錯(cuò),同時(shí)進(jìn)程直接退出,而我們?nèi)绻褂枚嗑€程啟動(dòng)的話,也會(huì)在我們請(qǐng)求多線程的個(gè)數(shù)之后,主線程退出,因?yàn)橹骶€程發(fā)現(xiàn)所有子線程全都掛掉了就會(huì)退出,基于這種文件我們希望不要發(fā)生,我們?cè)趺醋隹梢越鉀Q呢,

內(nèi)置了一個(gè)事件uncaughtException可以用來(lái)捕獲錯(cuò)誤,但是管方建議不要在這里組織塔退出程序,但是我們可以在退出程序前對(duì)其進(jìn)行錯(cuò)誤上報(bào),我們對(duì)cluster.js進(jìn)行輕微改造即可,同時(shí)我們也可以通過cluster模塊監(jiān)控,如果有的時(shí)候發(fā)生錯(cuò)誤導(dǎo)致現(xiàn)線程退出了,我們也可以進(jìn)行重啟,那么改造如下:

const cluster = require('cluster')
const os = require('os')

/* 判斷如果是主線程那么就啟動(dòng)三個(gè)子線程 */
if(cluster.isMaster){
/* 多少個(gè)cpu啟動(dòng)多少個(gè)子進(jìn)程 */
for (let i = 0; i < os.cpus().length; i++) cluster.fork()

/* 如果有線程退出了,我們重啟一個(gè) */
cluster.on('exit', () => {
setimeout(()=>{
cluster.fork()
}, 5000)
})
} else {
/* 如果是子進(jìn)程就去加載啟動(dòng)文件 */
require('./index.js')
process.on('uncaughtException', (err) => {
console.error(err)
/* 進(jìn)程錯(cuò)誤上報(bào) */
process.exit(1)
})
}

如上我們就可以在異常錯(cuò)誤的時(shí)候重啟線程并異常上報(bào),但是這樣會(huì)出現(xiàn)一個(gè)問題,那我如果重復(fù)銷毀創(chuàng)建線程可能會(huì)進(jìn)入死循環(huán),我們不確定這個(gè)線程的退出是不是可以挽救的情況,所以我們還需要對(duì)齊進(jìn)行完善,首先我們可以在全局監(jiān)控中判斷其內(nèi)存使用的數(shù)量,如果大于我們?cè)O(shè)置的限制就讓其退出程序。我們做如下改造防止內(nèi)存泄漏導(dǎo)致的無(wú)限重啟:

else {
/* 如果是子進(jìn)程就去加載啟動(dòng)文件 */
require('./index.js')
process.on('uncaughtException', (err) => {
console.error(err)
/* 進(jìn)程錯(cuò)誤上報(bào) */
/* 如果程序內(nèi)存大于xxxm了讓其退出 */
if(process.memoryUsage().rss > 734003200){
console.log('大于700m了,退出程序吧');
process.exit(1)
}
/* 退出程序 */
process.exit(1)
})
}

這樣呢我們就可以對(duì)內(nèi)存泄漏問題進(jìn)行處理了,同時(shí)我們還得考慮一種情況,如果子線程假死了怎么辦,僵尸進(jìn)程如何處理?

心跳檢測(cè),殺掉僵尸進(jìn)程

實(shí)現(xiàn)這個(gè)的思路并不負(fù)責(zé),和我們?nèi)粘W?strong>ws類似, 主進(jìn)程發(fā)心跳包,子進(jìn)程接收并回應(yīng)心跳包,我們分別改造兩個(gè)文件,

const cluster = require('cluster')
const os = require('os')

/* 判斷如果是主線程那么就啟動(dòng)三個(gè)子線程 */
if(cluster.isMaster){
/* 多少個(gè)cpu啟動(dòng)多少個(gè)子進(jìn)程 */
for (let i = 0; i < os.cpus().length; i++) {
let timer = null;
/* 記錄每一個(gè)woker */
const worker = cluster.fork()

/* 記錄心跳次數(shù) */
let missedPing = 0;

/* 每五秒發(fā)送一個(gè)心跳包 并記錄次數(shù)加1 */
timer = setInterval(() => {
missedPing++
worker.send('ping')

/* 如果大于5次都沒有得到響應(yīng)說(shuō)明可能掛掉了就退出 并清楚定時(shí)器 */
if(missedPing > 5 ){
process.kill(worker.process.pid)
worker.send('ping')
clearInterval(timer)
}
}, 5000);

/* 如果接收到心跳響應(yīng)就讓記錄值-1回去 */
worker.on('message', (msg) => {
msg === 'pong' && missedPing--
})
}

/* 如果有線程退出了,我們重啟一個(gè) */
cluster.on('exit', () => {
cluster.fork()
})
} else {
/* 如果是子進(jìn)程就去加載啟動(dòng)文件 */
require('./index.js')

/* 心跳回應(yīng) */
process.on('message', (msg) => {
msg === 'ping' && process.send('pong')
})

process.on('uncaughtException', (err) => {
console.error(err)
/* 進(jìn)程錯(cuò)誤上報(bào) */
/* 如果程序內(nèi)存大于xxxm了讓其退出 */
if(process.memoryUsage().rss > 734003200){
console.log('大于700m了,退出程序吧');
process.exit(1)
}
/* 退出程序 */
process.exit(1)
})
}

介紹一下流程

  • 主線程每隔五秒發(fā)送一個(gè)心跳包ping,同時(shí)記錄上發(fā)送次數(shù)+1,時(shí)間根據(jù)自己而定 這里五秒是測(cè)試方便
  • 子線程接收到了ping信號(hào)回復(fù)一個(gè)pong
  • 主線程接收到了子線程響應(yīng)讓計(jì)算數(shù)-1
  • 如果大于五次都還沒響應(yīng)可能是假死了,那么退出線程并清空定時(shí)器,

至此一個(gè)健壯的NodeJs服務(wù)已經(jīng)完成了。


當(dāng)前文章:「NodeJs進(jìn)階」超全面的Node.js性能優(yōu)化相關(guān)知識(shí)梳理
URL分享:http://www.5511xx.com/article/cosiodc.html