nodejs爬虫内存泄露排查

引子

最近在学推荐系统,萌生一个从头实现一个推荐系统的想法。说做就开始着手,第一步先写一个视频爬虫。

在网上找了一个有网页的版的视频聚合源,用nodejs+jsdom快速搭建了一个spider,爬取过程发现用并发的请求个数不好控制,太多容易把源网站爬挂了,就引入了async.parallelLimit和async.queue来做并发请求控制;另外看网上资料jsdom资源占用比较多,cheerio更轻便,便切换到cheerio。

但运行一段时间之后发现内存涨的非常快,像是存在内存泄露问题。

遇到问题不要着急,先进行下逻辑分析,再通过工具去逐步确认自己的假设或找到更多可疑的地方,两种方式不断交叉最终确认问题。

分析流程

问题:爬虫启动之后内存快速增长。

  1. 根据之前分析内存泄露的经验先仔细读下代码,看看是否有容易出现内存泄露的代码。这种代码排查过,没有可疑的地方。
  2. 引入的cheerio是否有内存泄露?快速网上查阅,有人有提及。换回jsdom快速试验下,同样出现。有可能是这2个库本来就有内存问题或者是爬虫逻辑上就存在内存的问题。
  3. 先通过工具判断下爬虫逻辑是否存在内存问题。js是内存自动管理,那看看主动gc有没有效果。给node增加了--max_old_space_size=512 --gc_interval=100 --expose_gc,然后在代码里面定时主动调用global.gc(),但内存还是飚的很快。
  4. 主动gc都没法解决,那肯定是有内存泄露,使用heapdump,定时打印heapdump出来分析对比。

发现有大量的字符串(网站的html)没有被释放,获取网上html的地方有好几处,通过二分查找能定位到代码

其中videoData就是存储从网上获取到的html。也就是说videoData没有被正确释放,根据之前做iOS的经验,如果在一个大循环内产生很多临时对象,但又没有创建AutoReleasePool的话会直到这个Runloop结束才能被释放,难道js也是这样?查询了下资料js的gc其实是很频繁的,没有这些限制,而且这个for循环里面有await,有足够的时机可以gc。那就想办法看看能不能找到是哪一句导致的问题。 同时看到日志里面有请求的url,打开一看是抓取蜡笔小新的视频,其中有1600+集。直觉告诉我这应该是集数太多所以才容易出现这个问题,这肯定跟这个大循环有关。既然知道了一个复现的地址,改下单测的代码直接抓取这个页面,同时通过--trace_gc来跟踪下内存gc情况

node --trace_gc spider.js | grep Mark-sweep

然后从代码405行开始增加continue来进行定位,这个时候内存变得非常稳定

发现在直到415行之后添加continue,内存又开始涨得很厉害了。所以可以定位是415行这句代码导致了内存泄露。415行就一个tvLink的赋值为啥会导致内存泄露呢?处于好奇就这414行打印了一句

console.log("tvLink=", tvLink)

神奇的事情发生了,再次跑的时候内存又不暴涨了,内存泄露问题解决了。咨询了下同事super大神,思路切换到既然知道videoData没有被释放掉,那就看看是谁retain着他?切换到Chrome的Profiler,可以点击字符串看到谁retain着这些字符串。

看到是一个数组retain着这些对象,然后在这个数组上Review in summary view

可以看到href是一个sliced string,记得之前看一篇文章说过sliced string导致的内存不释放的问题,顿时明白了,sliced string顾名思义就是他不实际存储字符串,而是存储他在父字符串的startOffset和len 所以href其实就是videoData的sliced string,这也是为啥videoData不能在循环的时候虽然不用了但还是不能被释放。但只要console.log就能迫使sliced string提取出确切的值,既然提取出值后面也没必要再存储成sliced string,所以内存泄露的问题也就解决了。附录还有一篇super大神写的SliceString的文章。 可以理解sliced string其实是为了优化字符串使用,但在我这个特定场景确会产生内存不能被快速释放的问题。准确的讲这不算是一个内存泄露的问题,而是一个内存堆积的问题。那有啥办法可以规避sliced string引入的问题呢?经同事建议,只要对这个字符串进行操作就能flatten sliced string,比如轻量的parseInt,而console.log其实也是一种,但不建议。

总结

  1. 对js底层的字符串机制得了解清楚,这个道理对于其他语言也一样。比如很多语言都有sliced string机制
  2. 可测性,不一定都有时间写单测,但尽量保证关键步骤都是拆分成可以独立测试的函数
  3. 如果有大循环,一定要注意哪些地方是sliced string,如果是的话执行必要的flatten操作,以便内存能及时释放
  4. 不建议着急用工具调试,有bug的代码都有规律,可以先通读代码确保逻辑上没有明显的问题,这样能提高效率;工具分析为辅助,好的工具像利器,得熟练掌握。

参考

  • 奇技淫巧学 V8 之六,字符串在 V8 内的表达
  • 闭包,作用域链,垃圾回收,内存泄露
  • V8 —— 你需要知道的垃圾回收机制
  • Node.js内存管理机制分享
  • 深入理解Node.js垃圾回收与内存管理
  • cheerio+v8 "leaks" memory from original HTML

你可能感兴趣的:(爬虫,内存管理,javascript,ViewUI)