Javascript NodeJS:在递归刮取过程中内存使用量不断增加,直到崩溃

Javascript NodeJS:在递归刮取过程中内存使用量不断增加,直到崩溃,javascript,node.js,Javascript,Node.js,我正在从NodeJS中的一个GET-URL-API抓取一些东西。我在一年中的几个月里环游了很多城市。我有一个scrapeChunk函数,我为参数的每个实例调用一次,即{startDate:…,endDate:…,location:…}。在内部,我对一个表进行jsdom解析,转换为CSV,将CSV附加到一个文件中。在所有嵌套的异步回调中,我最终使用下一个parameters实例再次调用scrapeChunk函数 这一切都可以正常工作,但节点实例在RAM中不断增长,直到出现一个致命错误:CALL_和

我正在从NodeJS中的一个GET-URL-API抓取一些东西。我在一年中的几个月里环游了很多城市。我有一个scrapeChunk函数,我为参数的每个实例调用一次,即{startDate:…,endDate:…,location:…}。在内部,我对一个表进行jsdom解析,转换为CSV,将CSV附加到一个文件中。在所有嵌套的异步回调中,我最终使用下一个parameters实例再次调用scrapeChunk函数

这一切都可以正常工作,但节点实例在RAM中不断增长,直到出现一个致命错误:CALL_和_RETRY_2分配失败-进程内存不足错误


我的问题:我是做错了什么,还是这是对JavaScript和/或我正在使用的库的限制?我能不能完成每项任务,释放内存,然后开始下一项任务?我尝试了FuturesJS的一个序列,它似乎也遭受了同样的泄漏。

可能发生的情况是,您正在构建一个非常深的调用树,该树的上层保留对其数据的引用,因此垃圾收集器永远不会声明它

要做的一件事是,在您自己的代码中,当您在最后调用回调时,通过调用process.nextTick;来完成;。这样,调用函数就可以结束并释放其变量。此外,请确保您没有将所有数据堆积到一个全局结构中,从而永久保留这些引用


在没有看到代码的情况下,要得到好的响应有点棘手。但这并不是node.js或其方法的局限性有很多长期运行且复杂的应用程序在使用它,而是如何使用它。

可能发生的事情是,您正在构建一个非常深入的调用树,而该树的上层保留对其数据的引用,所以它永远不会被垃圾收集器所占用

要做的一件事是,在您自己的代码中,当您在最后调用回调时,通过调用process.nextTick;来完成;。这样,调用函数就可以结束并释放其变量。此外,请确保您没有将所有数据堆积到一个全局结构中,从而永久保留这些引用


在没有看到代码的情况下,要得到好的响应有点棘手。但这并不是node.js或其方法的局限性——有许多长期运行的复杂应用程序在使用它,而是如何使用它。

假设您的描述是正确的,我认为问题的原因是显而易见的——对scrapeChunk的递归调用。使用循环或查看节点的流设施来分派任务,并确保它们实际返回

这里发生的事情听起来像这样:

var list = [1, 2, 3, 4, ... ];
function scrapeCheck(index) {
  // allocate variables, do work, etc, etc
  scrapeCheck(index+1)
}
var list = [1, 2, 3, 4, ... ];
list.forEach(function scrapeCheck(index) {
  // allocate variables, do work, etc, etc
  return;
});
有了足够长的列表,就可以保证耗尽内存、堆栈深度、堆或任何数量的内容,具体取决于函数体期间所做的操作。我的建议是这样的:

var list = [1, 2, 3, 4, ... ];
function scrapeCheck(index) {
  // allocate variables, do work, etc, etc
  scrapeCheck(index+1)
}
var list = [1, 2, 3, 4, ... ];
list.forEach(function scrapeCheck(index) {
  // allocate variables, do work, etc, etc
  return;
});

令人沮丧的是,嵌套回调是一个正交问题,但我建议您特别看看库,它对于这类任务既流行又有用。

假设您的描述是正确的,我认为问题的原因是显而易见的——对scrapeChunk的递归调用。使用循环或查看节点的流设施来分派任务,并确保它们实际返回

这里发生的事情听起来像这样:

var list = [1, 2, 3, 4, ... ];
function scrapeCheck(index) {
  // allocate variables, do work, etc, etc
  scrapeCheck(index+1)
}
var list = [1, 2, 3, 4, ... ];
list.forEach(function scrapeCheck(index) {
  // allocate variables, do work, etc, etc
  return;
});
有了足够长的列表,就可以保证耗尽内存、堆栈深度、堆或任何数量的内容,具体取决于函数体期间所做的操作。我的建议是这样的:

var list = [1, 2, 3, 4, ... ];
function scrapeCheck(index) {
  // allocate variables, do work, etc, etc
  scrapeCheck(index+1)
}
var list = [1, 2, 3, 4, ... ];
list.forEach(function scrapeCheck(index) {
  // allocate variables, do work, etc, etc
  return;
});

令人沮丧的是,嵌套回调是一个正交问题,但我建议您特别看看这个库,它对于这类任务既流行又有用。

您可能想尝试一下,而不是JSDom。作者声称它更精简,速度快了8倍。

您可能想尝试一下,而不是JSDom。作者声称它更精简,速度快8倍。

这与对函数的递归调用有关。将递归调用放入

setTimeout(()=>{
recursiveScrapFunHere();
}, 2000);
这样,调用是异步的,并且被放在优先级堆中,而不是放在通常的递归堆栈中,这是同步调用的情况

通过这种方式,您的父函数—相同的函数—将一直运行到结束,并且递归ScrapFun位于递归堆栈之外


这里的调用将在延迟2秒后进行。

这与对函数的递归调用有关。将递归调用放入

setTimeout(()=>{
recursiveScrapFunHere();
}, 2000);
这样,调用是异步的,并且被放在优先级堆中,而不是放在通常的递归堆栈中,这是同步调用的情况

通过这种方式,您的父函数—相同的函数—将一直运行到结束,并且递归ScrapFun位于递归堆栈之外

在这里,通话将在延迟2秒后进行。

您可以发布您的电子邮件吗
算法,或者更好,只是给我们递推算法的数量级和输入大小?这会有帮助。我在使用请求和递归刮取时遇到了完全相同的问题。我可能抓取了20k个URL,它会抛出一个错误。问题是数据值只有1毫克左右——但我猜被刮取的url的主体没有被垃圾收集,所以在大约10公里后,我得到了内存不足错误。我想这可能会占用4gb的内存。你能发布你的算法吗?或者更好,只是告诉我们递归算法的数量级和输入大小?这会有帮助。我在使用请求和递归刮取时遇到了完全相同的问题。我可能抓取了20k个URL,它会抛出一个错误。问题是数据值只有1毫克左右——但我猜被刮取的url的主体没有被垃圾收集,所以在大约10公里后,我得到了内存不足错误。我想这可能会消耗4gb的内存。然而,ScrapCheck似乎是异步的,OP说他尝试了FutureJS;一个好的引擎应该能够解决尾部递归。异步并不能告诉我们函数是否具有可优化的尾部调用。但问题是没有实际意义——Javascript并没有消除尾部调用。我递归的唯一原因是,每个任务都执行一个HTTP请求,然后在请求回调中进行jsdom解析,而请求回调本身可能是异步的,对此我不确定。我不想把所有的~一次25000个HTTP请求,我也不想一次完成那么多jsdom解析。我希望整个任务的异步工作全部完成,GC任务工作,然后开始下一个任务。如果我使用setTimeouttask作为递归调用而不是直接调用,会怎么样?这是正确的答案。我建议使用来执行项目的批处理池处理。基本上,您编写了一个函数,该函数弹出队列的一个项,执行所有异步HTTP请求,然后调用其回调以指示它已完成。每个限制允许您并行处理多个请求。setTimeout不是推荐的方法,因为它比节点的内部事件调度选项慢得多。此外,如果您的请求需要更长的时间,它也不起作用;一个好的引擎应该能够解决尾部递归。异步并不能告诉我们函数是否具有可优化的尾部调用。但问题是没有实际意义——Javascript并没有消除尾部调用。我递归的唯一原因是,每个任务都执行一个HTTP请求,然后在请求回调中进行jsdom解析,而请求回调本身可能是异步的,对此我不确定。我不想把所有的~一次25000个HTTP请求,我也不想一次完成那么多jsdom解析。我希望整个任务的异步工作全部完成,GC任务工作,然后开始下一个任务。如果我使用setTimeouttask作为递归调用而不是直接调用,会怎么样?这是正确的答案。我建议使用来执行项目的批处理池处理。基本上,您编写了一个函数,该函数弹出队列的一个项,执行所有异步HTTP请求,然后调用其回调以指示它已完成。每个限制允许您并行处理多个请求。setTimeout不是推荐的方法,因为它比节点的内部事件调度选项慢得多。而且,如果你的请求需要更长的时间,它也不起作用。我对cheerio也有同样的问题,尽管你是对的。它比jsdom好。我对cheerio也有同样的问题,尽管你是对的。它比jsdom好。你能给我解释一下吗?我也有同样的问题,一次只能处理10k请求。process.nextTick为我完成了这项工作。回答得真好!你能给我解释一下吗?我也有同样的问题,一次只能处理10k请求。process.nextTick为我完成了这项工作。回答得真好!