一次nodejs内存泄漏debug
发布于 7 年前 作者 pipi32167 7209 次浏览 来自 分享

1、定位问题 周四晚上收到报警短信,跟运维兄弟jim反馈以后,jim认为是突发流量增长,机器不足导致的,临时加了几个节点。 周五早上看短信吓尿了,短信报警一直在,加完机器以后还有问题,那就不是节点不足的缘故。 得益于jim搭建的zabbix监控,很快就定位到了问题:内存泄漏。见下图: nodejs_memory_leak_debug_01 可用内存到谷底后快速回升,要么是进程崩溃自动重启,要么是人为手动重启。 然而知道了内存泄漏并不能直接知道问题所在,只是缩小了问题域,导致内存泄漏可能有几种原因:

  • 代码编写不当。
  • 使用的外部模块有内存泄漏。
  • 使用的外部模块的C++部分有内存泄漏。这个最难排查。
  • 生产速度大于消费速度,堆积起来,这个一般涉及到IO。比如数据库查询、外部api调用、日志打印写磁盘等。

而且我早在几个月以前已经做过一次压测,把常见的内存泄漏问题都排查过了,急切间同事老司机建议直接用线上一个节点来排查问题,遂采纳之。

2、定位工具:heapdump + chrome 安装heapdump模块,需要注意的是,高版本heapdump依赖高版本的gcc,如果安装编译报错,请升级gcc: npm i heapdump 程序启动头部引入模块:

//app.js
require(‘heapdump')

程序启动起来后,使用以下命令将内存布局dump下来,隔一段时间,内存显著增长,再dump一次: kill -USR2 [pid]

3、解决问题: 将dump下来的文件下载至本地,用chrome打开,比较两次内存的增长,发现新增的内存中有大量的字符串 nodejs_memory_leak_debug_02 随意选取一个字符串查看其引用,基本上就确定是本地缓存使用不当导致的问题: nodejs_memory_leak_debug_03 检查代码,发现了问题,localCache中保存了大量的key没有被释放掉:

const localCache = {}

function setLocalCache(key, value) {
  localCache[key] = value
  setTimeout(() => {
    localCache[key] = null
  }, 5000)
}

解决方案——使用WeakMap来替代object,让内存及早释放。 写了2个脚本来测试:

// 内存泄漏版本:
// node —-expose-gc test1.js 
const localCache = {}

function setLocalCache(key, value) {
    localCache[key] = value
    setTimeout(() => localCache[key] = null, 5000)
}

const prefix = 'test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:'

for(let i = 0; i < 1000000; i++ ) {
    const key = prefix + i.toString()
    setLocalCache(key, key)
}

setInterval(() => global.gc(), 1000)
// 修复版本:
// node —-expose-gc test2.js 
const localCache = new WeakMap()

function setLocalCache(key, value) {
	key = new Object(key)
	localCache.set(key, value)
    setTimeout(() => {
        localCache.delete(key)
        key = null //关键:将key的引用释放
    }, 5000)
}

const prefix = 'test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:test:'

for(let i = 0; i < 1000000; i++ ) {
    const key = prefix + i.toString()
    setLocalCache(key, key)
}

setInterval(() => global.gc(), 1000)

运行两个脚本后,可以看到test2.js的内存使用率极低: nodejs_memory_leak_debug_04 至此,内存泄漏完美解决。

参考: 《Node.js 调试 GC 以及内存暴涨的分析》 《JavaScript 内存泄漏教程》 《EcmaScript 6入门》WeakMap的使用

27 回复

不错, 学习一下

像cache本来就应该放在redis之类,为什么要写在代码中?

非常棒的分享,感谢~

@kimown 你可以想下为啥会需要这个

关于这个cache请教大佬一个问题: 假如现在有A,B两台服务器做负载,按上面cache的方式,假如用户第一次上来先连到了A服务器,那么就会在A服务器产生cache_A,第二次用户上来的时候假如连到了B,那么发现B里没有是不是又得产生新的缓存cache_B?这样的话是不是就得保证用户第一次上来连的哪个服务器后面的操作就一直是哪个服务啊? 而如果放在redis里,redis单独起一台服务器是不是就不会有这个问题,因为所有逻辑服共用一个缓存服务器?

ls好问题,这个本地缓存的应用场景是很极端的,需要符合2个前提:高并发、极少修改

redis当然是要用的

战略性马克,万哥

战术性马克,万仔

先mark,后研究

再讲得详细一点: 考虑这样一个场景:1000个节点,每个节点请求 qps 1000,每个请求访问2次redis,那么redis的qps就高达 1000 * 1000 * 2 = 2000000。 普通的redis节点很难扛得住这样的并发,就算扛得住,网络带宽可能也会成为瓶颈。 有两种优化策略,一种是横向扩容,增加redis节点,负载均衡,另一种是纵向增加一级本地缓存,减少redis的请求量。 第二种还有两种处理方案,一种简单就是WeakMap即可,如果需要复杂一点的缓存管理策略,可以采用leveldb。

@imhered 这个问题可以nginx可以解决,根据ip hash把一定范围的ip地址始终落到同一台机器上

@pipi32167 那是否可以redis设置一个阈值,lru过滤掉最不常使用的cache数据

@楼主 dump的node是什么版本?谢谢

@kimown 看我上面的回复,应用场景很明确,也很极端。

@shrpcn node v4 v6 v8都试过,应该都没问题

@kimown 如果用Nginx的话,是不是得有一太入口机器(即做Nginx分发的那台),得保证这台不说100%至少99%不能挂对吧?因为这台挂了所有的都挂了?还是Nginx也可以做多台负载啊?

@imhered nginx当然可以做负载均衡啊

@pipi32167 没怎么用过Nginx。 我的意思是假如我现在有3台服务器分别是a、b、c,我在a服务器上装了Nginx,分别负载a,b,c 3台服务器。 用户每次连上来都是连的a服务器,然后通过Nginx分发到其他的服务器上。 那这样的话我是不是得保证a服务器不能挂,因为Nginx在a服务器上。 Nginx负载是这样做的对吧?

@imhered a、b、c三台业务服务器,d、e两台nginx,域名解析至d、e的ip,nginx转发至a、b、c 要做得再健壮一点,前端增加slb层, 自动将不可用的nginx节点摘除。

@pipi32167 哦,明白了,谢谢! 那这样的话,域名解析值d、e两台ip,玩家上来走d还是走e是不可控的了?随机的?

@imhered 是的,web服务负载均衡的一个重要特点就是服务无状态化,状态存储于redis之类的缓存中

回到顶部