<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 服務(wù)內(nèi)存泄漏,沒(méi)想到竟是它?

          共 8807字,需瀏覽 18分鐘

           ·

          2020-10-12 01:11

          今日文章由作者 @林樂(lè)揚(yáng) 投稿分享。

          背景

          團(tuán)隊(duì)最近將兩個(gè)項(xiàng)目遷移至 degg 2.0 中,兩個(gè)項(xiàng)目均出現(xiàn)比較嚴(yán)重的內(nèi)存泄漏問(wèn)題,此處以本人維護(hù)的埋點(diǎn)服務(wù)為例進(jìn)行排查。服務(wù)上線后內(nèi)存增長(zhǎng)如下圖,其中紅框?yàn)?degg 2.0 線上運(yùn)行的時(shí)間窗口,在短短 36 小時(shí)內(nèi),內(nèi)存已經(jīng)增長(zhǎng)到 50%,而平時(shí)內(nèi)存穩(wěn)定在 20%-30%,可知十之八九出現(xiàn)了內(nèi)存泄漏。

          排查思路

          由于兩個(gè)接入 degg 2.0 的服務(wù)均出現(xiàn)內(nèi)存泄漏問(wèn)題,因此初步將排查范圍鎖定在 degg 2.0引入或重寫的基礎(chǔ)組件上,重點(diǎn)懷疑對(duì)象為 nodex-logger 組件;同時(shí)為了排查內(nèi)存泄漏,我們需要獲取服務(wù)運(yùn)行進(jìn)程的堆快照(heapsnapshot),獲取方式可參看文章 hyj1991:Node 案發(fā)現(xiàn)場(chǎng)揭秘 —— 快速定位線上內(nèi)存泄漏https://zhuanlan.zhihu.com/p/36340263 。

          排查過(guò)程

          一、獲取堆快照

          使用 alinode 獲取堆快照,服務(wù)啟動(dòng)后,使用小流量預(yù)熱一兩分鐘便記錄第1份堆快照(2020-4-16-16:52),接著設(shè)置 qps 為 125 對(duì)服務(wù)進(jìn)行施壓,經(jīng)過(guò)大約一個(gè)小時(shí)(2020-4-16-15:46)獲取第2份堆快照。使用 Chrome dev工具載入兩份堆快照,如下圖所示,發(fā)現(xiàn)服務(wù)僅短短運(yùn)行一小時(shí),其堆快照文件就增大了 45MB,而初始大小也不過(guò) 39.7MB;我們按 Retained Size 列進(jìn)行排序,很快就發(fā)現(xiàn)了一個(gè)『嫌疑犯』,即 generator;該項(xiàng)占用了 55% 的大小,同時(shí) Shallow Size 卻為 0%,一項(xiàng)一項(xiàng)地展開(kāi),鎖定到了圖中高亮的這行,但是繼續(xù)展開(kāi)卻提示 0%,線索突然斷了。

          盯著 generator 進(jìn)入思考,我的服務(wù)代碼并沒(méi)有generator 語(yǔ)法,為什么會(huì)出現(xiàn) generator 對(duì)象的內(nèi)存泄漏呢?此時(shí)我把注意力轉(zhuǎn)到 node_modules 目錄中,由于最近一直在優(yōu)化 nodex-kafka 組件,有時(shí)直接在 node_modules 目錄中修改該組件的代碼進(jìn)行調(diào)試,因此幾乎每個(gè)文件頭部都有的一段代碼引起了我的注意:

          "use?strict";
          var?__awaiter?=?(this?&&?this.__awaiter)?||?function?(thisArg,?_arguments,?P,?generator)?{
          ????function?adopt(value)?{?return?value?instanceof?P???value?:?new?P(function?(resolve)?{?resolve(value);?});?}
          ????return?new?(P?||?(P?=?Promise))(function?(resolve,?reject)?{
          ????????function?fulfilled(value)?{?try?{?step(generator.next(value));?}?catch?(e)?{?reject(e);?}?}
          ????????function?rejected(value)?{?try?{?step(generator["throw"](value));?}?catch?(e)?{?reject(e);?}?}
          ????????function?step(result)?{?result.done???resolve(result.value)?:?adopt(result.value).then(fulfilled,?rejected);?}
          ????????step((generator?=?generator.apply(thisArg,?_arguments?||?[])).next());
          ????});
          };

          這個(gè)代碼是 typescript 源碼編譯后的產(chǎn)出,由于代碼使用了 async/await 語(yǔ)法,因此都編譯成 __awaiter 的形式,在源碼中使用 async 函數(shù)的地方,在編譯后都使用 __awaiter 進(jìn)行包裹:

          //?編譯前
          (async?function()?{
          ??await?Promise.resolve(1);
          ??await?Promise.resolve(2);
          })()

          //?編譯后
          (function?()?{
          ??return?__awaiter(this,?void?0,?void?0,?function*?()?{
          ????yield?Promise.resolve(1);
          ????yield?Promise.resolve(2);
          ??});
          })();

          同時(shí)一個(gè)關(guān)于 generator 內(nèi)存泄漏的 #30753 generator functions - memory leak https://github.com/nodejs/node/issues/30753 也引起了我的注意,該 issue 遇到的問(wèn)題無(wú)論從 Node.js 的版本和內(nèi)存泄漏的表現(xiàn)都和我遇到的問(wèn)題十分相似。所以我在工程的 node_modules 中搜索所有 __awaiter 字符串,發(fā)現(xiàn)了 3 個(gè)模塊編譯出了上述代碼,分別是:

          1. nodex-logger
          2. nodex-kafka
          3. nodex-apollo

          由于模塊的 tsconfig.json 的 target 字段將目標(biāo)產(chǎn)出為es6,因此才會(huì)使用 generator 去模擬 async/await 語(yǔ)法,但是從 Node.js v8.10.0 開(kāi)始已經(jīng) 100% 支持了 ES2017 的所有特性,所以本不該編譯 async/await 語(yǔ)法,此處遂將這 3 個(gè)模塊的目標(biāo)產(chǎn)出配置改為 es2017,這樣 tsc 就不會(huì)編譯 async/await 語(yǔ)法。

          二、驗(yàn)證

          重復(fù)之前獲取堆快照的步驟,驚奇地發(fā)現(xiàn)即使過(guò)了一天,內(nèi)存也沒(méi)有增長(zhǎng),而且 generator 也沒(méi)有持有未釋放的內(nèi)存:

          至此,內(nèi)存泄漏問(wèn)題已經(jīng)解決!那么如何避免遇到這個(gè)問(wèn)題呢?

          如何避免

          一、解決步驟

          步驟一

          該問(wèn)題僅在特定的 Node.js 版本中存在,請(qǐng)使用版本區(qū)間 (v11.0.0 - v12.16.0) 之外的 Node.js,從而防止二方 npm 組件、三方 npm 組件的 generator 語(yǔ)法使你的服務(wù)出問(wèn)題

          步驟二將自己的 typescript 的目標(biāo)環(huán)境(target)編譯為 es2017 及以上,同時(shí)應(yīng)盡量使用 async/await 語(yǔ)法而不是 generator 語(yǔ)法,從而防止別人使用 (v11.0.0 - v12.16.0) 版本時(shí),引入你的 npm 組件而導(dǎo)致內(nèi)存泄漏

          二、詳細(xì)說(shuō)明

          前文說(shuō)了從 Node.js v8.10.0 開(kāi)始就已經(jīng)支持了 async/await 語(yǔ)法,經(jīng)查該版本于 2018-03-06 發(fā)布,由于所有服務(wù)也不可能一下全切換到新版本,因此為了兼容 Node.js v6 版本的環(huán)境,需要將代碼編譯到 es6。但是站在現(xiàn)在這個(gè) LTS 版本已經(jīng)是 v12 的時(shí)間節(jié)點(diǎn),完全可以排查現(xiàn)有使用 typescript 的 npm 組件是否都編譯到 es2017,甚至探討編譯到 es2019 的可能。

          此外這個(gè)內(nèi)存泄漏問(wèn)題是從哪個(gè)版本開(kāi)始有的,現(xiàn)在是否解決了呢?編寫可驗(yàn)證的內(nèi)存泄漏的代碼如下:

          //?no-leak.js
          const?heapdump?=?require('heapdump')

          class?Async?{
          ??async?run()?{
          ??????return?null;
          ??}
          }
          const?run?=?async?()?=>?{
          ??for?(let?index?=?0;?index?10000000;?index++)?{
          ??????if?(index?%?1000000?===?0)
          ??????????console.log(Math.floor(process.memoryUsage().heapUsed?/?10000),?index);
          ??????const?doer?=?new?Async();
          ??????await?doer.run();
          ??}
          ??heapdump.writeSnapshot((err,?filename)?=>?{
          ????console.log("Heap?dump?written?to",?filename);
          ??});
          };
          run();
          //?leak.js?由?no-leak.js?編譯得來(lái)
          var?__awaiter?=?(this?&&?this.__awaiter)?||?function?(thisArg,?_arguments,?P,?generator)?{
          ????function?adopt(value)?{?return?value?instanceof?P???value?:?new?P(function?(resolve)?{?resolve(value);?});?}
          ????return?new?(P?||?(P?=?Promise))(function?(resolve,?reject)?{
          ????????function?fulfilled(value)?{?try?{?step(generator.next(value));?}?catch?(e)?{?reject(e);?}?}
          ????????function?rejected(value)?{?try?{?step(generator["throw"](value));?}?catch?(e)?{?reject(e);?}?}
          ????????function?step(result)?{?result.done???resolve(result.value)?:?adopt(result.value).then(fulfilled,?rejected);?}
          ????????step((generator?=?generator.apply(thisArg,?_arguments?||?[])).next());
          ????});
          };
          class?Async?{
          ????run()?{
          ????????return?__awaiter(this,?void?0,?void?0,?function*?()?{
          ????????????return?null;
          ????????});
          ????}
          }
          const?run?=?()?=>?__awaiter(this,?void?0,?void?0,?function*?()?{
          ????const?now?=?Date.now();
          ????console.log('循環(huán)總次數(shù):?',?10000000);
          ?????for?(let?index?=?0;?index?10000000;?index++)?{
          ????????if?(index?%?1000000?===?0)?{
          ????????????console.log('第?%d?次循環(huán),此時(shí)內(nèi)存為?%d',?index,?Math.floor(process.memoryUsage().heapUsed?/?1000000));
          ????????}
          ????????const?instance?=?new?Async();
          ????????yield?instance.run();
          ????}
          ????console.log('總耗時(shí):?%d?秒',?(Date.now()?-?now)?/?1000);
          });
          run();

          經(jīng)過(guò)二分排查,發(fā)現(xiàn)該泄漏問(wèn)題從 v11.0.0 引入,在 v12.16.0 解決;內(nèi)存泄漏版本執(zhí)行腳本時(shí),內(nèi)存占用逐步遞增直到 crash,而未泄漏版本則會(huì)及時(shí)回收內(nèi)存。

          根本原因

          根本原因是 v8 的一個(gè) bug,相關(guān)鏈接:

          v8 issue: https://bugs.chromium.org/p/v8/issues/detail?id=10031

          v8 commit: https://chromium.googlesource.com/v8/v8.git/+/d3a1a5b6c4916f22e076e3349ed3619bfb014f29

          node issue: https://github.com/nodejs/node/issues/30753

          node commit: https://github.com/nodejs/node/pull/31005/files

          改進(jìn)后的代碼,在分配新增WeakArrayList 數(shù)組時(shí),即使返回沒(méi)有空閑數(shù)組的標(biāo)記( kNoEmptySlotsMarker ),仍需要調(diào)用 ScanForEmptySlots 方法重新掃描一次數(shù)組,因?yàn)樵摂?shù)組元素有可能有被 GC 回收,這些被回收的元素是可以重復(fù)使用的;僅當(dāng)返回 kNoEmptySlotsMarker 且數(shù)組中沒(méi)有被 GC 回收的元素,才真正執(zhí)行新增邏輯:

          //?https://github.com/targos/node/blob/cceb2a87295724b7aa843363460ffcd10cda05b5/deps/v8/src/objects/objects.cc#L4042
          //?static
          Handle?PrototypeUsers::Add(Isolate*?isolate,
          ??????????????????????????????????????????Handle?array,
          ??????????????????????????????????????????Handle<Map>?value,
          ??????????????????????????????????????????int*?assigned_index)?{
          ??int?length?=?array->length();
          ??if?(length?==?0)?{
          ????//?Uninitialized?WeakArrayList;?need?to?initialize?empty_slot_index.
          ????array?=?WeakArrayList::EnsureSpace(isolate,?array,?kFirstIndex?+?1);
          ????set_empty_slot_index(*array,?kNoEmptySlotsMarker);
          ????array->Set(kFirstIndex,?HeapObjectReference::Weak(*value));
          ????array->set_length(kFirstIndex?+?1);
          ????if?(assigned_index?!=?nullptr)?*assigned_index?=?kFirstIndex;
          ????return?array;
          ??}

          ??//?If?the?array?has?unfilled?space?at?the?end,?use?it.
          ??if?(!array->IsFull())?{
          ????array->Set(length,?HeapObjectReference::Weak(*value));
          ????array->set_length(length?+?1);
          ?????if?(assigned_index?!=?nullptr)?*assigned_index?=?length;
          ????return?array;
          ??}

          ??//?If?there?are?empty?slots,?use?one?of?them.
          ??int?empty_slot?=?Smi::ToInt(empty_slot_index(*array));

          ??if?(empty_slot?==?kNoEmptySlotsMarker)?{
          ????//?GCs?might?have?cleared?some?references,?rescan?the?array?for?empty?slots.
          ????PrototypeUsers::ScanForEmptySlots(*array);
          ????empty_slot?=?Smi::ToInt(empty_slot_index(*array));
          ??}
          ???if?(empty_slot?!=?kNoEmptySlotsMarker)?{
          ????DCHECK_GE(empty_slot,?kFirstIndex);
          ????CHECK_LT(empty_slot,?array->length());
          ????int?next_empty_slot?=?array->Get(empty_slot).ToSmi().value();

          ????array->Set(empty_slot,?HeapObjectReference::Weak(*value));
          ????if?(assigned_index?!=?nullptr)?*assigned_index?=?empty_slot;

          ????set_empty_slot_index(*array,?next_empty_slot);
          ????return?array;
          ??}?else?{
          ????DCHECK_EQ(empty_slot,?kNoEmptySlotsMarker);
          ??}
          ??
          ???//?Array?full?and?no?empty?slots.?Grow?the?array.
          ??array?=?WeakArrayList::EnsureSpace(isolate,?array,?length?+?1);
          ??array->Set(length,?HeapObjectReference::Weak(*value));
          ??array->set_length(length?+?1);
          ??if?(assigned_index?!=?nullptr)?*assigned_index?=?length;
          ??return?array;
          }

          //?static
          void?PrototypeUsers::ScanForEmptySlots(WeakArrayList?array)?{
          ??for?(int?i?=?kFirstIndex;?i?????if?(array.Get(i)->IsCleared())?{
          ??????PrototypeUsers::MarkSlotEmpty(array,?i);
          ????}
          ??}
          }


          不止內(nèi)存泄漏

          在我測(cè)試內(nèi)存泄漏時(shí),有一個(gè)發(fā)現(xiàn),執(zhí)行發(fā)生內(nèi)存泄漏時(shí)的代碼(前文的 leak.js)和未發(fā)生內(nèi)存泄漏時(shí)的代碼(前文的 no-leak.js)時(shí),即使在已經(jīng)修復(fù)該問(wèn)題的 Node.js v12.16.2 版本下,generator 語(yǔ)法仍然有兩個(gè)問(wèn)題:

          1. 內(nèi)存回收效率低,導(dǎo)致執(zhí)行完后,仍有相當(dāng)大的內(nèi)存占用;
          2. 執(zhí)行效率非常慢,async/await 版本僅需要 0.953 秒,而generator 卻需要 17.754 秒;

          這說(shuō)明,相比 generator 語(yǔ)法,async/await 語(yǔ)法無(wú)論從執(zhí)行效率還是內(nèi)存占用方面都有壓倒性優(yōu)勢(shì)。那么執(zhí)行效率對(duì)比如何呢?上 benchmark 工具比劃比劃:

          //?benchmark.js
          const?__awaiter?=?(this?&&?this.__awaiter)?||?function?(thisArg,?_arguments,?P,?generator)?{
          ??function?adopt(value)?{?return?value?instanceof?P???value?:?new?P(function?(resolve)?{?resolve(value);?});?}
          ??return?new?(P?||?(P?=?Promise))(function?(resolve,?reject)?{
          ??????function?fulfilled(value)?{?try?{?step(generator.next(value));?}?catch?(e)?{?reject(e);?}?}
          ??????function?rejected(value)?{?try?{?step(generator["throw"](value));?}?catch?(e)?{?reject(e);?}?}
          ??????function?step(result)?{?result.done???resolve(result.value)?:?adopt(result.value).then(fulfilled,?rejected);?}
          ??????step((generator?=?generator.apply(thisArg,?_arguments?||?[])).next());
          ??});
          };

          const?Benchmark?=?require('benchmark');
          const?suite?=?new?Benchmark.Suite;

          suite
          ??.add('generator',?{
          ????defer:?true,
          ????fn:?function?(deferred)?{
          ??????(function?()?{
          ????????return?__awaiter(this,?void?0,?void?0,?function*?()?{
          ????????????yield?Promise.resolve(1);
          ????????????yield?Promise.resolve(2);
          ????????????//?測(cè)試完成
          ????????????deferred.resolve();
          ????????});
          ??????})();
          ????}
          ??})
          ???.add('async/await',?{
          ????defer:?true,
          ????fn:?function(deferred)?{
          ??????(async?function()?{
          ????????await?Promise.resolve(1);
          ????????await?Promise.resolve(2);

          ????????//?測(cè)試完成
          ????????deferred.resolve();
          ??????})()
          ????}
          ??})
          ??.on('cycle',?function(event)?{
          ????console.log(String(event.target));
          ??})
          ??.run({
          ????'async':?false
          ??});

          Node.js v12.16.2 的結(jié)果:

          generator?x?443,891?ops/sec?±4.12%?(75?runs?sampled)
          async/await?x?4,567,163?ops/sec?±1.96%?(79?runs?sampled)

          generator 每秒執(zhí)行了 516,178 次操作,而 async/await 每秒執(zhí)行了 4,531,357 次操作,后者是前者的 10 倍多!我們看看其它 Node.js 版本表現(xiàn)如何:

          電腦配置:MacBook Pro (13-inch, 2017, Two Thunderbolt 3 ports)

          二者執(zhí)行效率和 Node.js 版本成正比,而 Node.js v12 來(lái)了一次大躍進(jìn),直接高了一個(gè)數(shù)量級(jí),這個(gè)得益于 v8 7.2 的一個(gè)新特性,官網(wǎng)用了整整一篇文章 https://v8.dev/blog/fast-async#await-under-the-hood 說(shuō)明,有興趣的可以看看。

          Chrome 也中招了嗎?

          目前最新版:版本 81.0.4044.113(正式版本) (64 位) 已經(jīng)修復(fù)這個(gè)問(wèn)題

          既然是 v8 的問(wèn)題,那么 chrome 瀏覽器也是有這個(gè)問(wèn)題的,打開(kāi)空白標(biāo)簽頁(yè),執(zhí)行前文給出的 leak.js 代碼:

          推廣時(shí)間

          我叫林樂(lè)揚(yáng),點(diǎn)擊閱讀我的更多文章,覺(jué)得有收獲記得打開(kāi)原文鏈接給我點(diǎn)個(gè)贊,或者關(guān)注我哦~

          本文作者@林樂(lè)揚(yáng) | 原文@https://zhuanlan.zhihu.com/p/252689936

          敬請(qǐng)關(guān)注「程序員成長(zhǎng)指北」微信公眾號(hào),獲取優(yōu)質(zhì)文章,如需投稿可在后臺(tái)留言與我取得聯(lián)系。

          往期精彩回顧
          前端賦能業(yè)務(wù) - Node實(shí)現(xiàn)自動(dòng)化部署平臺(tái)

          一杯茶的時(shí)間,上手 Express 框架開(kāi)發(fā)

          Next.js + TypeScript 搭建一個(gè)簡(jiǎn)易的博客系統(tǒng)


          ??愛(ài)心三連擊

          1.看到這里了就點(diǎn)個(gè)在看支持下吧,你的點(diǎn)贊在看是我創(chuàng)作的動(dòng)力。

          2.關(guān)注公眾號(hào)程序員成長(zhǎng)指北,回復(fù)「1」加入Node進(jìn)階交流群!「在這里有好多 Node 開(kāi)發(fā)者,會(huì)討論 Node 知識(shí),互相學(xué)習(xí)」!

          3.也可添加微信【ikoala520】,一起成長(zhǎng)。


          瀏覽 44
          點(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>
                  51ⅴ精品国产91久久久久久 | 岛国免费AV | 中文字幕第一页国产 | 婷婷亚洲丁香色五月 | 福利在线观看 |