展开目录
记一次nodejs内存泄漏的排查经历
nodejs内存泄漏
X
陈尼玛的博客
记录开发生涯的踩坑经历,用时间来验证成长
加载中

给个人博客添加了一个功能:自动导出所有文档到文件夹,然后提交到github上。

我用process.memoryUsage()来查看内存占用的情况。

第一次导出全部时看了heapUsed占用160MB左右,倒也没在意,以为等自动垃圾回收之后会降下来,没想到第二天晚上看的时候居然还是稳定在这个位置,这就大概率是内存泄漏了。

起先怀疑是我写的mvc框架内存泄漏,于是我把主控制器代码改成最基本的输出,然后用ab命令访问100次。

IndexController.cjs

<?js
class IndexController extends App.Controller{
  async indexAction() {
    echo('xxx')
  }
}

module.exports=IndexController
ab -c 50 -n 100 http://127.0.0.1:19191/

image.png

看上去结果挺正常的,那么内存泄漏跟框架本身是没关系了。接下来排查对应的控制器代码。

Marked2gaController.cjs

<?js

class Marked2GaController extends App.Controller{
  async init() {
    const gm=await App.Loader.LoadService('Ga')
    if(!gm.is_local_logined_url() && !await gm.logined()) {
      this.redirect({
        controller: 'Error',
        action: 'noaccess'
      })
    }
  }

  async githubioAction() {
    const serviceMarked2=await App.Loader.LoadService('marked2')
    const detail=await serviceMarked2.getDetail(query.id)
    const serviceGithubio=await App.Loader.LoadService('githubio')
    const marked2str=await serviceGithubio.getstrbydetail(detail, query.debug==='yes')
    await this.displayView({
      dd: detail,
      marked2str,
      CY_APPID,
      CY_CONF,
      DISPLAY_NAME: MARKED2_DISPLAY_NAME,
      DESCRIPTION: MARKED2_DESCRIPTION,
    })
  }

  // 其他action代码略

}

module.exports=Marked2GaController

这里init是每次控制器被初始化时都会执行的代码钩子,githubioAction是生成导出代码的控制器。

相关的方法就这两个,于是我首先把githubioAction的代码全部注释掉改成一个空的action,然后用ab命令同样模式访问100次,测试结果图和上面的差不多,所以问题基本确定是在 githubioAction 里。

这个方法功能不多,涉及运算的也就serviceMarked2.getDetailserviceGithubio.getstrbydetail,第一个方法在其他地方都用到过,也验证过没有内存泄漏问题,那么问题就是在后者serviceGithubio.getstrbydetail了。


  async getstrbydetail(detail, is_debug) {
    let str=detail.content
    const ms=await App.Loader.LoadService('marked2')
    const related=(await ms.related(detail.tags)).filter(a=>a.id!==detail.id)
    const ls=await ms.ls(1)
    const rand=(_=>{
      let ret={}
      for(let i=0; i<20; i++) ret[ls.length*Math.random()|0]=1
      return Object.keys(ret).map(i=>{
        if(ls[i].id===detail.id) return
        return ls[i]
      }).filter(a=>a)
    })()
    const id2url=id=>is_debug?`?id=${id}&debug=yes`:`${id}.html`
    const RELA=`\n\n> ### 相关文档\n`
    str+=RELA+(related.length?related.map(({id, title})=>`  1. [${title}](${id2url(id)})`).join('\n\n'):`暂无`)
    const RAND=`\n\n> ### 随便看看\n`
    str+=RAND+(rand.length?rand.map(({id, title})=>`  1. [${title}](${id2url(id)})`).join('\n\n'):`暂无`)
    const marked=require('marked')
    marked.setOptions({
      sanitize: true,
    })
    marked.use({
      renderer : {
        codespan: (str, b)=>{
          const [,color,text]=str.match(/(^#{0,1}[a-z\d]+)\s*;\s*(.*$)|$/)
          if(color) return `<font color="${color}">${text}</font>`
          return false
        },
      },
    })
    return await marked(str)
  }

看上去这里涉及运算的方法有:

  1. ms.related 获取相关文档列表的方法
  2. ms.ls 获取文档列表的方法
  3. marked marked解析器

逐个排查,先把这三个方法全部干掉:

 async getstrbydetail(detail, is_debug) {
   let str=detail.content
   const ms=await App.Loader.LoadService('marked2')
   const related=[] // (await ms.related(detail.tags)).filter(a=>a.id!==detail.id)
   const ls=[] // await ms.ls(1)
   const rand=(_=>{
     let ret={}
     for(let i=0; i<20; i++) ret[ls.length*Math.random()|0]=1
     return Object.keys(ret).map(i=>{
       if(ls[i].id===detail.id) return
       return ls[i]
     }).filter(a=>a)
   })()
   const id2url=id=>is_debug?`?id=${id}&debug=yes`:`${id}.html`
   const RELA=`\n\n> ### 相关文档\n`
   str+=RELA+(related.length?related.map(({id, title})=>`  1. [${title}](${id2url(id)})`).join('\n\n'):`暂无`)
   const RAND=`\n\n> ### 随便看看\n`
   str+=RAND+(rand.length?rand.map(({id, title})=>`  1. [${title}](${id2url(id)})`).join('\n\n'):`暂无`)
   return str
   /*
   const marked=require('marked')
   marked.setOptions({
     sanitize: true,
   })
   marked.use({
     renderer : {
       codespan: (str, b)=>{
         const [,color,text]=str.match(/(^#{0,1}[a-z\d]+)\s*;\s*(.*$)|$/)
         if(color) return `<font color="${color}">${text}</font>`
         return false
       },
     },
   })
   return await marked(str)
   */
 }

然后ab命令测,果然内存情况正常了。

然后依次还原这三个方法,最后定位问题出在marked上,我猜测原因是每次初始化marked都会存在内存泄漏,那么解决办法就是让这玩意只初始化一次,即用单例模式来使用它。

修改之后如下


  // 把marked方法变成单例模式使用
  async markedstr(str) {
    // marked可能存在内存泄漏,使用单例模式相对安全
    const marked=await Singleton('marked', async _=>{
      const marked=require('marked')
      marked.setOptions({
        sanitize: true,
      })
      marked.use({
        renderer : {
          codespan: (str, b)=>{
            const [,color,text]=str.match(/(^#{0,1}[a-z\d]+)\s*;\s*(.*$)|$/)
            if(color) return `<font color="${color}">${text}</font>`
            return false
          },
        },
      })
      return marked
    })
    return marked(str)
  }

  // 把原先写marked(str)的写法改成这样
  return await markedstr(str)

这次再用ab命令测试,问题已经解决了。

有感而发,就是因为我完全贯彻了mvc架构的分层理念,这次排查问题才能顺藤摸瓜非常轻松地解决了。实际上这些被封装方法的背后还有数据库查询,界面输出等功能逻辑,如果全部写在一起的混乱代码,靠这种排除法的方式来查就会相当麻烦。

网上搜了一些nodejs内存泄漏的排查方式,最靠谱的是借助代码侵入方法导出内存使用快照,然后用chrome开发者工具来定位内存占用时间最长的代码。以后有机会我也试试看吧,问题复杂的时候不失为一种快捷的方法。

相关文档

暂无

随便看看

  1. git 大小写不区分问题

  2. react项目webpack打包时拆分异步加载的文件

  3. curl用法

  4. npm包命令行调用

  5. jxa运动指令脚本

  6. git删除远程分支

  7. html5 全屏代码

  8. mongodb 批量修改字段语句

  9. dd备份/还原

  10. linux自启动/禁止自启动服务

  11. 树莓派配置wifi热点

  12. TIME_WAIT过多

  13. git记住/删除账号密码

  14. git 设置代理服务器

  15. ssl 证书生成方式

  16. centos查看最近一次的开机时间

  17. centos7 开放或者关闭端口

  18. 简易版事件封装

  19. youku电脑版跳过广告代码

畅言模块加载中