<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Node.js內(nèi)存泄漏的原因竟然是……?

          共 6186字,需瀏覽 13分鐘

           ·

          2021-10-29 09:12


          導(dǎo)語(yǔ)?|?Node.js內(nèi)存泄漏的問(wèn)題經(jīng)常讓開(kāi)發(fā)者頭疼,我們應(yīng)該怎么樣解決這類(lèi)問(wèn)題呢?本文通過(guò)一個(gè)V8引擎自身Bug導(dǎo)致Generator內(nèi)存泄漏案例,來(lái)介紹常用的應(yīng)對(duì)手段。


          一、背景


          最近新開(kāi)發(fā)了一個(gè)Node.js服務(wù),卻發(fā)現(xiàn)上線(xiàn)之后內(nèi)存一直持續(xù)上漲。相信很多使用Node.js做過(guò)服務(wù)端開(kāi)發(fā)的同學(xué),也遇到過(guò)這樣的問(wèn)題,這種情況就是典型的內(nèi)存泄漏。內(nèi)存泄漏雖然不會(huì)馬上讓?xiě)?yīng)用停止服務(wù),但是如果不處理的話(huà),輕則會(huì)導(dǎo)致你的應(yīng)用越來(lái)越慢,重則會(huì)導(dǎo)致應(yīng)用Crash。所以對(duì)于這種情況,我們不能掉以輕心。




          二、為什么會(huì)內(nèi)存泄露


          (一)C語(yǔ)言中的內(nèi)存管理(手動(dòng)管理)


          在C語(yǔ)言中,我們?nèi)绻枰褂靡粋€(gè)變量來(lái)存儲(chǔ)某些值,需要開(kāi)發(fā)者先手動(dòng)調(diào)用malloc函數(shù),向系統(tǒng)申請(qǐng)一塊內(nèi)存,然后才能將相關(guān)信息保存到這塊內(nèi)存中。并且使用完之后,開(kāi)發(fā)者還要手動(dòng)調(diào)用free函數(shù)將這塊內(nèi)存給釋放掉:


          # include # include int main(void){    int *p = malloc(sizeof*p); // 申請(qǐng)一塊內(nèi)存    *p = 10; // 將int類(lèi)型的10寫(xiě)入這塊內(nèi)存中    printf("*p = %d\n", *p); // 輸出 *p = 10    free(p); // 釋放內(nèi)存    return 0;}


          這種讓開(kāi)發(fā)者手動(dòng)管理內(nèi)存的方式,嚴(yán)重拖慢了開(kāi)發(fā)效率。而且開(kāi)發(fā)者忘記free的內(nèi)存塊,會(huì)一直無(wú)法釋放。這樣也會(huì)導(dǎo)致內(nèi)存泄漏。



          (二)Node.js中的內(nèi)存管理(自動(dòng)管理)


          為了解決手動(dòng)管理內(nèi)存帶來(lái)的問(wèn)題,V8在內(nèi)存管理方面做了改進(jìn):


          • 開(kāi)發(fā)者在創(chuàng)建數(shù)據(jù)時(shí),V8會(huì)自動(dòng)分配對(duì)應(yīng)的內(nèi)存空間,無(wú)需再調(diào)用malloc。


          • V8引入了GC機(jī)制,自動(dòng)找到程序中不再需要使用的內(nèi)存,并將其釋放


          這種方式雖然給我們解決了很大的麻煩,但是也留下了新的問(wèn)題:開(kāi)發(fā)者習(xí)慣于V8幫助我們進(jìn)行內(nèi)存管理,從而產(chǎn)生一種不需要關(guān)注應(yīng)用內(nèi)存的錯(cuò)覺(jué)


          實(shí)際上GC機(jī)制并不能完全幫我們回收所有“不需要的內(nèi)存”(開(kāi)發(fā)者認(rèn)為不需要的內(nèi)存,如果沒(méi)有妥善處理,GC還是不會(huì)去回收)



          三、問(wèn)題排查


          內(nèi)存泄漏問(wèn)題排查起來(lái)一般都會(huì)比較困難,最常用的方式是通過(guò)分析內(nèi)存泄漏前后的內(nèi)存快照,對(duì)比找出持續(xù)增長(zhǎng)的內(nèi)容。


          (一)對(duì)比內(nèi)存快照


          對(duì)比內(nèi)存快照的方式分為4步


          • 程序啟動(dòng)之后,生成堆快照A。


          • 執(zhí)行可能導(dǎo)致內(nèi)存泄漏的操作。


          • 內(nèi)存上漲后,生成堆快照B。


          • 在Chrome Dev Tool中對(duì)比兩次快照,找出這段時(shí)間內(nèi)一直增長(zhǎng)的內(nèi)容。


          • 原理


          class Person {  constructor(name) {    this.name = name  }}
          let persons = []
          function leak() { const bob = new Person('bob') persons.push(bob)}
          genHeapSnapshot() // 偽碼: 執(zhí)行l(wèi)eak函數(shù)前, 生成堆快照Aleak()genHeapSnapshot() // 偽碼: 執(zhí)行l(wèi)eak函數(shù)后, 生成堆快照B


          內(nèi)存快照A中的信息:


          • 1個(gè)array, 變量名為persons。


          • 其他系統(tǒng)對(duì)象。


          內(nèi)存快照B的信息:


          • 1個(gè)array,變量名為persons。


          • 1個(gè)Person,變量名為bob;被persons.0所引用;被leak函數(shù)的Context引用(在leak函數(shù)中定義)


          • 1個(gè)string;被bob中的name屬性引用。


          把2個(gè)快照做對(duì)比之后就能發(fā)現(xiàn):leak函數(shù)執(zhí)行完之后,內(nèi)存中多了1個(gè)Person對(duì)象和1個(gè)string


          當(dāng)leak函數(shù)執(zhí)行10000次后,內(nèi)存中就會(huì)增加10000個(gè)Person和string,我們只需要找到這些新增的對(duì)象,就能找到內(nèi)存增長(zhǎng)的原因。



          • 實(shí)踐


          獲取內(nèi)存快照的方式有很多,常用的有heapdump、v8-profiler等模塊。還可以通過(guò)啟用Inspector模式,在Chrome Dev Tool中采集Node.js應(yīng)用的堆內(nèi)存。


          將快照加載到Chrome Dev Tool之后,我們看到增長(zhǎng)最多的對(duì)象是(system)、(array)、(string)、(compiled code)等。



          但是當(dāng)試圖從(system)里邊找出問(wèn)題對(duì)象時(shí),就會(huì)發(fā)現(xiàn)事情沒(méi)有想象中那么簡(jiǎn)單。


          兩次內(nèi)存快照之間,system新創(chuàng)建了39822個(gè),銷(xiāo)毀了39078個(gè),沒(méi)能正常銷(xiāo)毀的只占了1.8%。要找到這1.8%的問(wèn)題對(duì)象,需要耗費(fèi)不少時(shí)間。


          雖然對(duì)比內(nèi)存快照的方式,大部分情況下都能幫我們解決問(wèn)題,但是這次的情況卻不太適用。當(dāng)然,除了快照對(duì)比,還有其他的一些方法,比如MAT。



          (二)MAT


          MAT(Memory Analizer Tool)是Eclipse中的一個(gè)插件,經(jīng)常被用來(lái)定位Java中的內(nèi)存泄漏問(wèn)題。MAT的思路是:如果發(fā)生了內(nèi)存泄漏,那么這些導(dǎo)致內(nèi)存泄漏的對(duì)象會(huì)在內(nèi)存占很大比重


          • 原理


          class Person {}
          let persons = []let women = []
          function leak() { const bob = new Person() const steve = new Person() const lily = new Person() persons.push(bob, steve, lily) women.push(lily)}
          leak()genHeapSnapshot() // 偽碼: 執(zhí)行l(wèi)eak函數(shù)后, 生成堆快照


          這個(gè)例子生成的內(nèi)存快照,其中的對(duì)象引用關(guān)系,如圖中所示(簡(jiǎn)化版,去掉了各種內(nèi)置對(duì)象):



          支配樹(shù)中的每個(gè)節(jié)點(diǎn)都有一個(gè)Retained Size屬性,表示該節(jié)點(diǎn)所支配的內(nèi)存大小,節(jié)點(diǎn)自身的Retained Size=所有子節(jié)點(diǎn)的Retained Size+節(jié)點(diǎn)的Self Size(自己占用的內(nèi)存大小)


          MAT的工作原理是將內(nèi)存快照轉(zhuǎn)換成一個(gè)支配樹(shù),將支配樹(shù)中所支配內(nèi)存超過(guò)一定閾值的對(duì)象認(rèn)為是可疑對(duì)象,找到這些對(duì)象的支配鏈,和鏈上的內(nèi)存積累點(diǎn)


          在我們的例子中,當(dāng)越來(lái)越多的Person被放進(jìn)persons數(shù)組時(shí),persons的Retained Size會(huì)變得越來(lái)越大。當(dāng)對(duì)象的Retained Size達(dá)到一達(dá)閾值(可自定義,默認(rèn)是占總內(nèi)存的20%),就認(rèn)為該對(duì)象是可疑對(duì)象。開(kāi)發(fā)者可以根據(jù)對(duì)象的支配鏈路,快速找到問(wèn)題所在。



          • 實(shí)踐


          可以使用v8-mat這個(gè)npm包,把內(nèi)存快照轉(zhuǎn)換成支配樹(shù),并找到內(nèi)存中的可疑對(duì)象。也可以使用Chrome Dev Tool對(duì)快照中的對(duì)象,按Retained Size進(jìn)行排序,自行判斷。


          在服務(wù)運(yùn)行一天后,我們采集了內(nèi)存快照進(jìn)行分析,發(fā)現(xiàn)了一個(gè)內(nèi)存泄漏可疑點(diǎn):內(nèi)存中有一個(gè)Generator支配了73%的內(nèi)存!



          雖然找到了可疑的支配鏈,但是支配鏈下的對(duì)象卻是些和業(yè)務(wù)代碼無(wú)關(guān)的內(nèi)置對(duì)象。



          看到這里時(shí),已經(jīng)有點(diǎn)懷疑是否是Node.js本身存在的Bug。



          (三)問(wèn)題解決


          這時(shí)在網(wǎng)上發(fā)現(xiàn)了一個(gè)相似的案例:由于TS將async/await編譯成Generator,導(dǎo)致內(nèi)存泄漏。

          (https://github.com/apollographql/apollo-server/issues/3730)


          發(fā)現(xiàn)是V8引擎存在一個(gè)Bug,導(dǎo)致了在11.0.0-12.15.x,使用Generator時(shí),都會(huì)出現(xiàn)內(nèi)存泄漏!


          解決方式有2個(gè):去除代碼中的Generator,將Node.js將級(jí)到12.16以上。


          查看了tsconfig.json及編譯后的代碼,發(fā)現(xiàn)并無(wú)異常。再到node_modules中查找是否存在yield關(guān)鍵詞,結(jié)果卻搜出來(lái)幾十個(gè)使用了Generator的庫(kù)。改代碼是改不動(dòng)了,只能?chē)L試升級(jí)Node.js到14,看看內(nèi)存占用是否恢復(fù)正常。



          可以看到升級(jí)之后,Node.js應(yīng)用的內(nèi)存消耗已經(jīng)下降了很多,并且保存在穩(wěn)定的狀態(tài),沒(méi)有再出現(xiàn)之前持續(xù)增長(zhǎng)的情況。至此,內(nèi)存泄漏的問(wèn)題已經(jīng)解決。



          四、常見(jiàn)的內(nèi)存泄露場(chǎng)景


          最后列舉一些常見(jiàn)的內(nèi)存泄漏場(chǎng)景,在開(kāi)發(fā)過(guò)程中,對(duì)這些情況稍加注意,能幫助我們避免大部分的內(nèi)存泄漏問(wèn)題。


          (一)隱式全局變量


          沒(méi)有使用var/let/const聲明的變量會(huì)直接綁定在Global對(duì)象上(Node.js中)或者Windows對(duì)象上(瀏覽器中),哪怕不再使用,仍不會(huì)被自動(dòng)回收:


          function test() {  x = new Array(100000);}test();console.log(x); // 輸出 [ <100000 empty items> ]



          (二)沒(méi)釋放的無(wú)用對(duì)象(監(jiān)聽(tīng)器、緩存)


          沒(méi)有釋放的監(jiān)聽(tīng)器,會(huì)一直保存在內(nèi)存中,導(dǎo)致內(nèi)存無(wú)法釋放:


          class Test {  constructor() {    this.init()  }  init() {    emitter.addListener('message', function() {      // 相關(guān)操作    });  }  destroy() {    // 沒(méi)有removeListener  }}


          使用內(nèi)存作為緩存時(shí),沒(méi)有釋放過(guò)期的緩存也是常見(jiàn)的情況:


          const app = require('express')()const cache = {};// 設(shè)置緩存app.post('/data', (req, res) => {  cache[req.body.key] = req.body.value  res.send('succ')})// 獲取緩存app.get('/data', (req, res) => {  res.send(cache[req.params.key])})


          (三)閉包


          閉包也是導(dǎo)致內(nèi)存泄漏的常見(jiàn)原因。


          const func = function () {  const data = 'inner variable'  return () => {    return data  }}const getData = func()console.log(getData()) // 此時(shí)func函數(shù)內(nèi)部的data變量無(wú)法釋放



          五、相關(guān)工具介紹


          (一)heapdump

          (https://github.com/bnoordhuis/node-heapdump)


          老牌內(nèi)存快照生成庫(kù),可以通過(guò)API或者系統(tǒng)信號(hào)的形式,生成內(nèi)存快照。缺點(diǎn)是只支持內(nèi)存快照生成,不支持生成CPU Profile文件。


          使用API生成快照:


          var heapdump = require('heapdump');heapdump.writeSnapshot('/var/local/' + Date.now() + '.heapsnapshot');


          使用系統(tǒng)信號(hào)生成快照:


          kill -USR2 



          (二)v8-profiler

          (https://github.com/hyj1991/v8-profiler-next)


          支持生成CPU Profile/堆快照/Allocation Profile,缺點(diǎn)是需要登陸機(jī)器將生成的文件下載后,使用其他工具進(jìn)行分析。


          生成CPU Profile文件:


          const v8Profiler = require('v8-profiler-next');const title = 'good-name';v8Profiler.startProfiling(title, true);setTimeout(() => {  const profile = v8Profiler.stopProfiling(title);  profile.export(function (error, result) {    fs.writeFileSync(`${title}.cpuprofile`, result);    profile.delete();  });}, 5 * 60 * 1000);


          生成堆內(nèi)存快照:


          const v8Profiler = require('v8-profiler-next');const snapshot = v8Profiler.takeSnapshot();const transform = snapshot.export();transform.pipe(process.stdout);transform.on('finish', snapshot.delete.bind(snapshot))


          生成Allocation Profile:


          const v8Profiler = require('v8-profiler-next');const arraytest = [];setInterval(() => {  arraytest.push(new Array(1e2).fill('*').join());}, 20);
          v8Profiler.startSamplingHeapProfiling();setTimeout(() => { const profile = v8Profiler.stopSamplingHeapProfiling(); require('fs').writeFileSync('./shf.heapprofile', JSON.stringify(profile));}, 60 * 1000);



          (三)Chrome Inspector


          使用--inspect參數(shù)啟動(dòng)服務(wù),會(huì)默認(rèn)在9229端口啟動(dòng)一個(gè)websocket server,Chrome DevTool連接該端口后,可以對(duì)Node.js程序進(jìn)行Debug。Chrome DevTool功能齊全,缺點(diǎn)是線(xiàn)上機(jī)房網(wǎng)絡(luò)與本地開(kāi)發(fā)網(wǎng)絡(luò)不通,使用不便,通常只在DevCloud開(kāi)發(fā)機(jī)中使用。


          開(kāi)啟inspect模式:


          node --inspect=0.0.0.0:9229 app.js


          訪問(wèn)chrome://inspect/可以對(duì)指定進(jìn)程進(jìn)行調(diào)試,采集CPU Profile、堆快照等。



          六、結(jié)語(yǔ)


          雖然JavaScript、Java等語(yǔ)言能幫我們自動(dòng)回收內(nèi)存,提高了開(kāi)發(fā)效率,但是這并不意味著不會(huì)出現(xiàn)內(nèi)存泄漏的情況。作為開(kāi)發(fā)者,在開(kāi)發(fā)過(guò)程中也需要對(duì)可能的內(nèi)存泄漏,保持敏銳的嗅覺(jué)。同時(shí)還需要了解相關(guān)的問(wèn)題排查方法,即便是應(yīng)用上線(xiàn)之后才發(fā)現(xiàn)問(wèn)題,我們也能夠快速將它解決。



          ?作者簡(jiǎn)介


          王思鴻

          騰訊高級(jí)前端工程師

          騰訊高級(jí)前端工程師,畢業(yè)于華中科技大學(xué),目前負(fù)責(zé)騰訊教育企鵝輔導(dǎo)業(yè)務(wù)的開(kāi)發(fā)工作。專(zhuān)注于前端性能優(yōu)化與全棧開(kāi)發(fā),在Node.js監(jiān)控領(lǐng)域有深入研究。



          ?推薦閱讀


          超詳細(xì)教程!手把手帶你使用Raft分布式共識(shí)性算法

          Pulsar與Rocketmq、Kafka、Inlong-TubeMQ,誰(shuí)才是消息中間件的王者?

          gRPC如何在Golang和PHP中進(jìn)行實(shí)戰(zhàn)?7步教你上手!

          詳細(xì)解答!從C++轉(zhuǎn)向Rust需要注意哪些問(wèn)題?






          瀏覽 93
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  AV天堂电影在线 | 国产精品1区2区 | 免费操逼视频网站 | 性无码一区二区三区无码免费 | 亚洲一级操逼片 |