引子
最近在学推荐系统,萌生一个从头实现一个推荐系统的想法。说做就开始着手,第一步先写一个视频爬虫。
在网上找了一个有网页的版的视频聚合源,用nodejs+jsdom快速搭建了一个spider,爬取过程发现用并发的请求个数不好控制,太多容易把源网站爬挂了,就引入了async.parallelLimit和async.queue来做并发请求控制;另外看网上资料jsdom资源占用比较多,cheerio更轻便,便切换到cheerio。
但运行一段时间之后发现内存涨的非常快,像是存在内存泄露问题。
遇到问题不要着急,先进行下逻辑分析,再通过工具去逐步确认自己的假设或找到更多可疑的地方,两种方式不断交叉最终确认问题。
分析流程
问题:爬虫启动之后内存快速增长。
- 根据之前分析内存泄露的经验先仔细读下代码,看看是否有容易出现内存泄露的代码。这种代码排查过,没有可疑的地方。
- 引入的cheerio是否有内存泄露?快速网上查阅,有人有提及。换回jsdom快速试验下,同样出现。有可能是这2个库本来就有内存问题或者是爬虫逻辑上就存在内存的问题。
- 先通过工具判断下爬虫逻辑是否存在内存问题。js是内存自动管理,那看看主动gc有没有效果。给node增加了
--max_old_space_size=512 --gc_interval=100 --expose_gc
,然后在代码里面定时主动调用global.gc()
,但内存还是飚的很快。 - 主动gc都没法解决,那肯定是有内存泄露,使用heapdump,定时打印heapdump出来分析对比。
node --trace_gc spider.js | grep Mark-sweep
发现在直到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其实也是一种,但不建议。
总结
- 对js底层的字符串机制得了解清楚,这个道理对于其他语言也一样。比如很多语言都有sliced string机制
- 可测性,不一定都有时间写单测,但尽量保证关键步骤都是拆分成可以独立测试的函数
- 如果有大循环,一定要注意哪些地方是sliced string,如果是的话执行必要的flatten操作,以便内存能及时释放
- 不建议着急用工具调试,有bug的代码都有规律,可以先通读代码确保逻辑上没有明显的问题,这样能提高效率;工具分析为辅助,好的工具像利器,得熟练掌握。