自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Node.js 應(yīng)用故障排查手冊(cè) —— 冗余配置傳遞引發(fā)的內(nèi)存溢出

存儲(chǔ) 存儲(chǔ)軟件
本節(jié)將以一個(gè)開發(fā)者容易忽略的生產(chǎn)內(nèi)存溢出案例,來(lái)展示如何借助于性能平臺(tái)實(shí)現(xiàn)對(duì)線上應(yīng)用 Node.js 應(yīng)用出現(xiàn)內(nèi)存泄漏時(shí)的發(fā)現(xiàn)、分析、定位問(wèn)題代碼以及修復(fù)的過(guò)程,希望能對(duì)大家有所啟發(fā)。

 楔子

前面我們以一個(gè)真實(shí)的壓測(cè)案例來(lái)給大家講解如何利用 Node.js 性能平臺(tái) 生成的 CPU Profile 分析來(lái)進(jìn)行壓測(cè)時(shí)的性能調(diào)優(yōu)。那么與 CPU 相關(guān)的問(wèn)題相比,Node.js 應(yīng)用中由于不當(dāng)使用產(chǎn)生的內(nèi)存問(wèn)題是一個(gè)重災(zāi)區(qū),而且這些問(wèn)題往往都是出現(xiàn)在生產(chǎn)環(huán)境下,本地壓測(cè)都難以復(fù)現(xiàn),實(shí)際上這部分內(nèi)存問(wèn)題也成為了很多的 Node.js 開發(fā)者不敢去將 Node.js 這門技術(shù)棧深入運(yùn)用到后端的一大阻礙。

[[261146]]

本節(jié)將以一個(gè)開發(fā)者容易忽略的生產(chǎn)內(nèi)存溢出案例,來(lái)展示如何借助于性能平臺(tái)實(shí)現(xiàn)對(duì)線上應(yīng)用 Node.js 應(yīng)用出現(xiàn)內(nèi)存泄漏時(shí)的發(fā)現(xiàn)、分析、定位問(wèn)題代碼以及修復(fù)的過(guò)程,希望能對(duì)大家有所啟發(fā)。

最小化復(fù)現(xiàn)代碼

因?yàn)閮?nèi)存問(wèn)題相對(duì) CPU 高的問(wèn)題來(lái)說(shuō)比較特殊,我們直接從問(wèn)題排查的描述可能不如結(jié)合問(wèn)題代碼來(lái)看比較直觀,因此在這里我們首先給出了最小化的復(fù)現(xiàn)代碼,大家運(yùn)行后結(jié)合下面的分析過(guò)程應(yīng)該能更有收獲,樣例基于 Egg.js:如下所示:

  1. 'use strict'
  2. const Controller = require('egg').Controller; 
  3. const DEFAULT_OPTIONS = { logger: console }; 
  4. class SomeClient { 
  5.  constructor(options) { 
  6.  this.options = options; 
  7.  } 
  8.  async fetchSomething() { 
  9.  return this.options.key
  10.  } 
  11. const clients = {}; 
  12. function getClient(options) { 
  13.  if (!clients[options.key]) { 
  14.  clients[options.key] = new SomeClient(Object.assign({}, DEFAULT_OPTIONS, options)); 
  15.  } 
  16.  return clients[options.key]; 
  17. class MemoryController extends Controller { 
  18.  async index() { 
  19.  const { ctx } = this; 
  20.  const options = { ctx, key: Math.random().toString(16).slice(2) }; 
  21.  const data = await getClient(options).fetchSomething(); 
  22.  ctx.body = data; 
  23.  } 
  24. module.exports = MemoryController; 

然后在 app/router.js 中增加一個(gè) Post 請(qǐng)求路由:

  1. router.post('/memory', controller.memory.index); 

造成問(wèn)題的 Post 請(qǐng)求 Demo 這里也給出來(lái),如下所示:

  1. 'use strict'
  2. const fs = require('fs'); 
  3. const http = require('http'); 
  4. const postData = JSON.stringify({ 
  5.  // 這里的 body.txt 可以放一個(gè)比較大 2M 左右的字符串 
  6.  data: fs.readFileSync('./body.txt').toString() 
  7. }); 
  8. function post() { 
  9.  const req = http.request({ 
  10.  method: 'POST'
  11.  host: 'localhost'
  12.  port: '7001'
  13.  path: '/memory'
  14.  headers: { 
  15.  'Content-Type''application/json'
  16.  'Content-Length': Buffer.byteLength(postData) 
  17.  } 
  18.  }); 
  19.  req.write(postData); 
  20.  req.end(); 
  21.  req.on('error'function (err) { 
  22.  console.log(12333, err); 
  23.  }); 
  24. setInterval(post, 1000); 

***我們?cè)趩?dòng)完成最小化復(fù)現(xiàn)的 Demo 服務(wù)器后,再運(yùn)行這個(gè) Post 請(qǐng)求的客戶端,1s 發(fā)起一個(gè) Post 請(qǐng)求,在平臺(tái)控制臺(tái)可以看到堆內(nèi)存在一直增加,如果我們按照本書工具篇中的 Node.js 性能平臺(tái)使用指南 - 配置合適的告警 一節(jié)中配置了 Node.js 進(jìn)程堆內(nèi)存告警的話,過(guò)一會(huì)就會(huì)收到平臺(tái)的 短信/郵件 提醒。

問(wèn)題排查過(guò)程

收到性能平臺(tái)的進(jìn)程內(nèi)存告警后,我們登錄到控制臺(tái)并且進(jìn)入應(yīng)用首頁(yè),找到告警對(duì)應(yīng)實(shí)例上的問(wèn)題進(jìn)程,然后參照工具篇中的 Node.js 性能平臺(tái)使用指南 - 內(nèi)存泄漏 中的方法抓取堆快照,并且點(diǎn)擊 分析 按鈕查看 AliNode 定制后的分解結(jié)果展示:

Node.js 應(yīng)用故障排查手冊(cè) —— 冗余配置傳遞引發(fā)的內(nèi)存溢出

這里默認(rèn)的報(bào)表頁(yè)面頂部的信息含義已經(jīng)提到過(guò)了,這里不再重復(fù),我們重點(diǎn)來(lái)看下這里的可疑點(diǎn)信息:提示有 18 個(gè)對(duì)象占據(jù)了 96.38% 的堆空間,顯然這里就是我們需要進(jìn)一步查看的點(diǎn)。我們可以點(diǎn)擊 對(duì)象名稱 來(lái)看到這18 個(gè) system/Context 對(duì)象的詳細(xì)內(nèi)容:

Node.js 應(yīng)用故障排查手冊(cè) —— 冗余配置傳遞引發(fā)的內(nèi)存溢出

這里進(jìn)入的是分別以這 18 個(gè) system/Context 為根節(jié)點(diǎn)起始的支配樹視圖,因此展開后可以看到各個(gè)對(duì)象的實(shí)際內(nèi)存占用情況,上圖中顯然問(wèn)題集中在***個(gè)對(duì)象上,我們繼續(xù)展開查看:

很顯然,這里真正吃掉堆空間的是 451 個(gè) SomeClient 實(shí)例,面對(duì)這樣的問(wèn)題我們需要從兩個(gè)方面來(lái)判斷這是否真的是內(nèi)存異常的問(wèn)題:

  • 當(dāng)前的 Node.js 應(yīng)用在正常的邏輯下,是否單個(gè)進(jìn)程需要 451 個(gè) SomeClient 實(shí)例
  • 如果確實(shí)需要這么多 SomeClient 實(shí)例,那么每個(gè)實(shí)例占據(jù) 1.98MB 的空間是否合理

對(duì)于***個(gè)判斷,在對(duì)應(yīng)的實(shí)際生產(chǎn)面臨的問(wèn)題中,經(jīng)過(guò)代碼邏輯的重新確認(rèn),我們的應(yīng)用確實(shí)需要這么多的 Client 實(shí)例,顯然此時(shí)排查重點(diǎn)集中在每個(gè)實(shí)例的 1.98MB 的空間占用是否合理上,假如進(jìn)一步判斷還是合理的,這意味著 Node.js 默認(rèn)單進(jìn)程 1.4G 的堆上限在這個(gè)場(chǎng)景下是不適用的,需要我們來(lái)通過(guò)啟動(dòng) Flag 調(diào)大堆上限。

正是基于以上的判斷需求,我們繼續(xù)點(diǎn)開這些 SomeClient 實(shí)例進(jìn)行查看:

Node.js 應(yīng)用故障排查手冊(cè) —— 冗余配置傳遞引發(fā)的內(nèi)存溢出

這里可以很清晰的看到,這個(gè) SomeClient 本身只有 1.97MB 的大小,但是下面的 options 屬性對(duì)應(yīng)的 Object@428973 對(duì)象一個(gè)就占掉了 1.98M,進(jìn)一步展開這個(gè)可疑的 Object@428973 對(duì)象可以看到,其 ctx 屬性對(duì)應(yīng)的 Object@428919 對(duì)象正是 SomeClient 實(shí)例占據(jù)掉如此大的對(duì)空間的根本原因所在!

我們可以點(diǎn)擊其它的 SomeClient 實(shí)例,可以看到每一個(gè)實(shí)例均是如此,此時(shí)我們需要結(jié)合代碼,判斷這里的 options.ctx 屬性掛載到 SomeClient 實(shí)例上是否也是合理的,點(diǎn)擊此問(wèn)題 Object 的地址:

進(jìn)入到這個(gè) Object 的關(guān)系圖中:

Node.js 應(yīng)用故障排查手冊(cè) —— 冗余配置傳遞引發(fā)的內(nèi)存溢出

Search 展示的視圖不同于 Dom 結(jié)果圖,它實(shí)際上展示的是從堆快中解析出來(lái)的原始對(duì)象關(guān)系圖,所以邊信息是一定會(huì)存在的,靠邊名稱和對(duì)象名稱,我們比較容易判斷對(duì)象在代碼中的位置。

但是在這個(gè)例子中,僅僅依靠以 Object@428973 為起始點(diǎn)的內(nèi)存原始關(guān)系圖,看不到很明確的代碼位置,畢竟不管是 Object.ctx 還是 Object.key 都是相當(dāng)常見的 JavaScript 代碼關(guān)系,因此我們繼續(xù)點(diǎn)擊 Retainer 視圖:

得到如下信息:

這里的 Retainer 信息和 Chrome Devtools 中的 Retainer 含義是一樣的,它代表了節(jié)點(diǎn)在堆內(nèi)存中的原始父引用關(guān)系,正如本文的內(nèi)存問(wèn)題案例中,僅靠可疑點(diǎn)本身以及其展開無(wú)法可靠地定位到問(wèn)題代碼的情況下,那么展開此對(duì)象的 Retainer 視圖,可以看到它的父節(jié)點(diǎn)鏈路可以比較方便的定位到問(wèn)題代碼。

這里我們顯然可以通過(guò)在 Retainer 視圖下的問(wèn)題對(duì)象父引用鏈路,很方便地找到代碼中創(chuàng)建此對(duì)象的代碼:

  1. function getClient(options) { 
  2.  if (!clients[options.key]) { 
  3.  clients[options.key] = new SomeClient(Object.assign({}, DEFAULT_OPTIONS, options)); 
  4.  } 
  5.  return clients[options.key]; 

結(jié)合看 SomeClient 的使用,看到用于初始化的 options 參數(shù)中實(shí)際上只是用到了其 key 屬性,其余的屬于冗余的配置信息,無(wú)需傳入。

代碼修復(fù)與確認(rèn)

知道了原因后修改起來(lái)就比較簡(jiǎn)單了,單獨(dú)生成一個(gè) SomeClient 使用的 options 參數(shù),并且僅將需要的數(shù)據(jù)從傳入的 options 參數(shù)上取過(guò)來(lái)以保證沒有冗余信息即可:

  1. function getClient(options) { 
  2.  const someClientOptions = Object.assign({ key: options.key }, DEFAULT_OPTIONS); 
  3.  if (!clients[options.key]) { 
  4.  clients[options.key] = new SomeClient(someClientOptions); 
  5.  } 
  6.  return clients[options.key]; 

重新發(fā)布后運(yùn)行,可以到堆內(nèi)存下降至只有幾十兆,至此 Node.js 應(yīng)用的內(nèi)存異常的問(wèn)題***解決。

結(jié)尾

本節(jié)中也比較全面地給大家展示了如何使用 Node.js 性能平臺(tái) 來(lái)排查定位線上應(yīng)用內(nèi)存泄漏問(wèn)題,其實(shí)嚴(yán)格來(lái)說(shuō)本次問(wèn)題并不是真正意義上的內(nèi)存泄漏,像這種配置傳遞時(shí)開發(fā)者圖省事直接全量 Assign 的場(chǎng)景我們?cè)趯懘a時(shí)或多或少時(shí)都會(huì)遇到,這個(gè)問(wèn)題帶給我們的啟示還是:當(dāng)我們?nèi)ゾ帉懸粋€(gè)公共組件模塊時(shí),永遠(yuǎn)不要去相信使用者的傳入?yún)?shù),任何時(shí)候都應(yīng)當(dāng)只保留我們需要使用到的參數(shù)繼續(xù)往下傳遞,這樣可以避免掉很多問(wèn)題。

作者:奕鈞

責(zé)任編輯:武曉燕 來(lái)源: 阿里云云棲社區(qū)
相關(guān)推薦

2022-06-23 06:34:56

Node.js子線程

2017-03-19 16:40:28

漏洞Node.js內(nèi)存泄漏

2017-03-20 13:43:51

Node.js內(nèi)存泄漏

2023-06-30 23:25:46

HTTP模塊內(nèi)存

2020-01-03 16:04:10

Node.js內(nèi)存泄漏

2025-01-08 08:47:44

Node.js內(nèi)存泄露定時(shí)器

2020-01-15 14:20:07

Node.js應(yīng)用程序javascript

2013-11-01 09:34:56

Node.js技術(shù)

2011-11-10 11:08:34

Node.js

2021-12-01 00:05:03

Js應(yīng)用Ebpf

2015-03-10 10:59:18

Node.js開發(fā)指南基礎(chǔ)介紹

2020-05-29 15:33:28

Node.js框架JavaScript

2012-02-03 09:25:39

Node.js

2021-12-25 22:29:57

Node.js 微任務(wù)處理事件循環(huán)

2020-07-31 13:35:34

Node.js應(yīng)用分析前端

2011-09-09 14:23:13

Node.js

2011-11-01 10:30:36

Node.js

2011-09-08 13:46:14

node.js

2011-09-02 14:47:48

Node

2020-05-09 13:49:00

內(nèi)存空間垃圾
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)