Node.js 应用故障排查手册 —— 冗余配置传递引发的内存溢出
楔子
前面一小節(jié)我們以一個真實的壓測案例來給大家講解如何利用?Node.js 性能平臺?生成的 CPU Profile 分析來進行壓測時的性能調(diào)優(yōu)。那么與 CPU 相關(guān)的問題相比,Node.js 應(yīng)用中由于不當(dāng)使用產(chǎn)生的內(nèi)存問題是一個重災(zāi)區(qū),而且這些問題往往都是出現(xiàn)在生產(chǎn)環(huán)境下,本地壓測都難以復(fù)現(xiàn),實際上這部分內(nèi)存問題也成為了很多的 Node.js 開發(fā)者不敢去將 Node.js 這門技術(shù)棧深入運用到后端的一大阻礙。
本節(jié)將以一個開發(fā)者容易忽略的生產(chǎn)內(nèi)存溢出案例,來展示如何借助于性能平臺實現(xiàn)對線上應(yīng)用 Node.js 應(yīng)用出現(xiàn)內(nèi)存泄漏時的發(fā)現(xiàn)、分析、定位問題代碼以及修復(fù)的過程,希望能對大家有所啟發(fā)。
本書首發(fā)在 Github,倉庫地址:https://github.com/aliyun-node/Node.js-Troubleshooting-Guide,云棲社區(qū)會同步更新。
最小化復(fù)現(xiàn)代碼
因為內(nèi)存問題相對 CPU 高的問題來說比較特殊,我們直接從問題排查的描述可能不如結(jié)合問題代碼來看比較直觀,因此在這里我們首先給出了最小化的復(fù)現(xiàn)代碼,大家運行后結(jié)合下面的分析過程應(yīng)該能更有收獲,樣例基于?Egg.js:如下所示:
'use strict';const Controller = require('egg').Controller;const DEFAULT_OPTIONS = { logger: console };class SomeClient {constructor(options) {this.options = options;}async fetchSomething() {return this.options.key;} }const clients = {};function getClient(options) {if (!clients[options.key]) {clients[options.key] = new SomeClient(Object.assign({}, DEFAULT_OPTIONS, options));}return clients[options.key]; }class MemoryController extends Controller {async index() {const { ctx } = this;const options = { ctx, key: Math.random().toString(16).slice(2) };const data = await getClient(options).fetchSomething();ctx.body = data;} }module.exports = MemoryController;然后在?app/router.js?中增加一個 Post 請求路由:
router.post('/memory', controller.memory.index);造成問題的 Post 請求 Demo 這里也給出來,如下所示:
'use strict';const fs = require('fs'); const http = require('http');const postData = JSON.stringify({// 這里的 body.txt 可以放一個比較大 2M 左右的字符串data: fs.readFileSync('./body.txt').toString() });function post() {const req = http.request({method: 'POST',host: 'localhost',port: '7001',path: '/memory',headers: {'Content-Type': 'application/json','Content-Length': Buffer.byteLength(postData)}});req.write(postData);req.end();req.on('error', function (err) {console.log(12333, err);}); }setInterval(post, 1000);最后我們在啟動完成最小化復(fù)現(xiàn)的 Demo 服務(wù)器后,再運行這個 Post 請求的客戶端,1s 發(fā)起一個 Post 請求,在平臺控制臺可以看到堆內(nèi)存在一直增加,如果我們按照本書工具篇中的?Node.js 性能平臺使用指南 - 配置合適的告警?一節(jié)中配置了 Node.js 進程堆內(nèi)存告警的話,過一會就會收到平臺的 短信/郵件 提醒。
問題排查過程
收到性能平臺的進程內(nèi)存告警后,我們登錄到控制臺并且進入應(yīng)用首頁,找到告警對應(yīng)實例上的問題進程,然后參照工具篇中的?Node.js 性能平臺使用指南 - 內(nèi)存泄漏?中的方法抓取堆快照,并且點擊?分析?按鈕查看 AliNode 定制后的分解結(jié)果展示:
這里默認的報表頁面頂部的信息含義已經(jīng)提到過了,這里不再重復(fù),我們重點來看下這里的可疑點信息:提示有 18 個對象占據(jù)了 96.38% 的堆空間,顯然這里就是我們需要進一步查看的點。我們可以點擊?對象名稱?來看到這18 個?system/Context?對象的詳細內(nèi)容:
這里進入的是分別以這 18 個?system/Context? 為根節(jié)點起始的支配樹視圖,因此展開后可以看到各個對象的實際內(nèi)存占用情況,上圖中顯然問題集中在第一個對象上,我們繼續(xù)展開查看:
很顯然,這里真正吃掉堆空間的是 451 個 SomeClient 實例,面對這樣的問題我們需要從兩個方面來判斷這是否真的是內(nèi)存異常的問題:
- 當(dāng)前的 Node.js 應(yīng)用在正常的邏輯下,是否單個進程需要 451 個 SomeClient 實例
- 如果確實需要這么多 SomeClient 實例,那么每個實例占據(jù) 1.98MB 的空間是否合理
對于第一個判斷,在對應(yīng)的實際生產(chǎn)面臨的問題中,經(jīng)過代碼邏輯的重新確認,我們的應(yīng)用確實需要這么多的 Client 實例,顯然此時排查重點集中在每個實例的 1.98MB 的空間占用是否合理上,假如進一步判斷還是合理的,這意味著 Node.js 默認單進程 1.4G 的堆上限在這個場景下是不適用的,需要我們來通過啟動 Flag 調(diào)大堆上限。
正是基于以上的判斷需求,我們繼續(xù)點開這些 SomeClient 實例進行查看:
這里可以很清晰的看到,這個 SomeClient 本身只有 1.97MB 的大小,但是下面的?options?屬性對應(yīng)的 Object@428973 對象一個就占掉了 1.98M,進一步展開這個可疑的?Object@428973 對象可以看到,其?ctx?屬性對應(yīng)的 Object@428919 對象正是 SomeClient 實例占據(jù)掉如此大的對空間的根本原因所在!
我們可以點擊其它的 SomeClient 實例,可以看到每一個實例均是如此,此時我們需要結(jié)合代碼,判斷這里的?options.ctx?屬性掛載到 SomeClient 實例上是否也是合理的,點擊此問題?Object 的地址:
進入到這個 Object 的關(guān)系圖中:
Search 展示的視圖不同于 Dom 結(jié)果圖,它實際上展示的是從堆快中解析出來的原始對象關(guān)系圖,所以邊信息是一定會存在的,靠邊名稱和對象名稱,我們比較容易判斷對象在代碼中的位置。
但是在這個例子中,僅僅依靠以 Object@428973 為起始點的內(nèi)存原始關(guān)系圖,看不到很明確的代碼位置,畢竟不管是?Object.ctx?還是?Object.key?都是相當(dāng)常見的 JavaScript 代碼關(guān)系,因此我們繼續(xù)點擊?Retainer?視圖:
得到如下信息:
這里的 Retainer 信息和 Chrome Devtools 中的 Retainer 含義是一樣的,它代表了節(jié)點在堆內(nèi)存中的原始父引用關(guān)系,正如本文的內(nèi)存問題案例中,僅靠可疑點本身以及其展開無法可靠地定位到問題代碼的情況下,那么展開此對象的 Retainer 視圖,可以看到它的父節(jié)點鏈路可以比較方便的定位到問題代碼。
這里我們顯然可以通過在 Retainer 視圖下的問題對象父引用鏈路,很方便地找到代碼中創(chuàng)建此對象的代碼:
function getClient(options) {if (!clients[options.key]) {clients[options.key] = new SomeClient(Object.assign({}, DEFAULT_OPTIONS, options));}return clients[options.key]; }結(jié)合看 SomeClient 的使用,看到用于初始化的?options?參數(shù)中實際上只是用到了其?key?屬性,其余的屬于冗余的配置信息,無需傳入。
代碼修復(fù)與確認
知道了原因后修改起來就比較簡單了,單獨生成一個 SomeClient 使用的 options 參數(shù),并且僅將需要的數(shù)據(jù)從傳入的 options 參數(shù)上取過來以保證沒有冗余信息即可:
function getClient(options) {const someClientOptions = Object.assign({ key: options.key }, DEFAULT_OPTIONS);if (!clients[options.key]) {clients[options.key] = new SomeClient(someClientOptions);}return clients[options.key]; }重新發(fā)布后運行,可以到堆內(nèi)存下降至只有幾十兆,至此 Node.js 應(yīng)用的內(nèi)存異常的問題完美解決。
結(jié)尾
本節(jié)中也比較全面地給大家展示了如何使用?Node.js 性能平臺?來排查定位線上應(yīng)用內(nèi)存泄漏問題,其實嚴格來說本次問題并不是真正意義上的內(nèi)存泄漏,像這種配置傳遞時開發(fā)者圖省事直接全量 Assign 的場景我們在寫代碼時或多或少時都會遇到,這個問題帶給我們的啟示還是:當(dāng)我們?nèi)ゾ帉懸粋€公共組件模塊時,永遠不要去相信使用者的傳入?yún)?shù),任何時候都應(yīng)當(dāng)只保留我們需要使用到的參數(shù)繼續(xù)往下傳遞,這樣可以避免掉很多問題。
原文鏈接
本文為云棲社區(qū)原創(chuàng)內(nèi)容,未經(jīng)允許不得轉(zhuǎn)載。
總結(jié)
以上是生活随笔為你收集整理的Node.js 应用故障排查手册 —— 冗余配置传递引发的内存溢出的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 2018年度机器学习50大热门网文
- 下一篇: 图(关系网络)数据分析及阿里应用