mongo游标导致的栈溢出报错

Posted by Galen on November 2, 2015

背景:

在mongo中每天的数据量大概是5500W+,每天一张collection。 大数据量面前,直接把数据load到内存里的话,不用说,直接死翘翘了。这里采用了游标的方式。 每天早上7点开始每5分钟跑一次脚本。用游标的方式遍历数据,数据量到7点为止,差不多是700+W。

node定时器:-传送门-


主要代码结构:

主要代码结构


###解决过程:

晚上写好定时脚本,第二天去看nohup的日志,发现7点15分,即在只执行了几次之后就报错了。

用游标,大概跑了2次700+W的数据之后,在第三次定时跑的时候显示 内存溢出。 去谷歌了下,发现目测是循环引用导致

主要代码结构

1、尝试在每一次跑完之后,人为的去回收函数变量以及对象,比如cursor,每次processoMaps、processGraph调用完回收item。试试 主要代码结构

错误代码变了点,重新分析了下,看来问题还是出现在游标的调用上。

2、再google了下,发现这个错误除了栈内存爆了,也有可能是nodejs的异步调用池溢出了。根据内存分析,也确实,栈内存溢出的可能性比较低。于是在递归调用cursor的地方,外面加上了setImmediate。卧槽,跑下来,没问题了。

后来也在这里找到了问同样问题的:http://stackoverflow.com/…


拓展:

既然说到了setImmediate,我们就来看看直接调用、setImmediate、process.nextTick、setTimeout这四个有什么差别呢?

* 直接调用

我们跑一下下面的demo:

function forNext(i,end){
     if(i>end){
          console.log('program is end!');
     }else{
          console.log('调用递归中......i='+i);
          return forNext(i+1,end);
     }
}

forNext(0,100000);

主要代码结构

这时候报错了。 而且执行多几遍,发现报错时候 i 对应的数值还会不相同

很显然,这正是文章最前面我们调用cursor.nextObject()报错的原因。


* process.nextTick、setImmediate、setTimeout

首先要了解什么是nodejs的event loop。这个就不解释了,看链接:传送门

简单说:event loop是主线程执行栈的行为,每次event loop,我们称为Tick。当执行栈为空的时候,去任务队列取里面的任务

在nodejs0.9版本之前,递归调用用的都是process.nextTick,之后推荐使用setImmediate

这是官网文档说明:
Note: the nextTick queue is completely drained on each pass of the event loop beforeadditional I/O is processed. As a result, recursively setting nextTick callbacks will block any I/O from happening, just like a while(true); loop.

所以,process.nextTick算是恶劣的插队行为,直接插在当前执行栈的末尾、io回调之前。如果递归使用它,会造成回调不断延后,造成event loop 饥饿。
而setImmediate是插在下一次event loop的时候,发生在io回调之后,不会影响io回调

我们执行下这个demo:

setImmediate(function() {
     console.log(5)
})

process.nextTick(function() {
     console.log(2)
})

process.nextTick(function() {
     console.log(3)
})

执行结果是: 主要代码结构

可以看出,跟书写的顺序确实是没有关系的。

process.nextTick的回调放在一个数组中,在下一次Tick之前,nextTick回调数组里面的全部一次性执行。 setImmediate的回调是放在一个链表中的,每一次Tick,只执行链表中的一个回调。

在event loop中,process.nextTick属于idle观察者,setImmediate属于check观察者。
idle观察者优先于io观察者,io观察者优先于check观察者

这篇回答貘大从源码层面深挖了原理:[2]

那setTimeout呢? 我们来执行以下的代码来对比一下:

setImmediate(function() {
     console.log(1)
})

setTimeout(function() {
     console.log(2)
}, 0)

setTimeout(function() {
     console.log(4)
}, 0)

setImmediate(function() {
     console.log(3)
})

主要代码结构

因为setImmediate不会导致event loop被block住,允许其他的I/O或者timer的callback执行,所以setTimeout的执行会穿插在setImmediate中


总结:

  • process.nextTick插入在当前执行栈的尾部,io回调之前,setImmediate插入在下一次的event loop中,io回调之后
  • process.nextTick的回调放在数组中,一次性全部执行。setImmediate则放在链表中。nextTick全部执行完才会执行setImmediate的回调
  • process.nextTick会造成event loop饥饿
  • 递归回调均用setImmediate


参考:

[1] http://stackoverflow.com/…
[2] http://www.zhihu.com/…
[3] https://nodejs.org/api/…