nodejs爬虫内存泄露排查

引子

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

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

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

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

分析流程

问题:爬虫启动以后内存快速增加。segmentfault

  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着这些字符串。dom

看到是一个数组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其实也是一种,但不建议。async

总结

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

参考

相关文章
相关标签/搜索