将大数组传递给节点子进程

时间:2017-05-18 16:12:35

标签: javascript node.js mongodb

我想在大型阵列上进行复杂的CPU密集型工作。理想情况下,我想将此传递给子进程。

var spawn = require('child_process').spawn;

// dataAsNumbers is a large 2D array
var child = spawn(process.execPath, ['/child_process_scripts/getStatistics', dataAsNumbers]);

child.stdout.on('data', function(data){
  console.log('from child: ', data.toString());
});

但是当我这样做时,node会给出错误:

  

产生E2BIG

我遇到了this article

因此,将数据传递给子进程似乎是要走的路。我的代码现在是:

var spawn = require('child_process').spawn;

console.log('creating child........................');

var options = { stdio: [null, null, null, 'pipe'] };
var args = [ '/getStatistics' ];
var child = spawn(process.execPath, args, options);

var pipe = child.stdio[3];

pipe.write(Buffer('awesome'));

child.stdout.on('data', function(data){
  console.log('from child: ', data.toString());
});

然后在getStatistics.js中:

console.log('im inside child');

process.stdin.on('data', function(data) {
  console.log('data is ', data);
  process.exit(0);
});

然而,process.stdin.on中的回调未达成。如何在我的子脚本中接收流?

修改

我不得不放弃缓冲方法。现在我将数组作为消息发送:

var cp = require('child_process');
var child = cp.fork('/getStatistics.js');

child.send({ 
  dataAsNumbers: dataAsNumbers
});

但这仅在dataAsNumber的长度低于约20,000时才有效,否则超时。

6 个答案:

答案 0 :(得分:11)

如果有如此大量的数据,我会考虑使用shared memory而不是将数据复制到子进程(这是使用管道或传递消息时发生的情况)。这将节省内存,减少父进程的CPU时间,并且不太可能达到某种限制。

shm-typed-array是一个非常简单的模块,似乎适合您的应用程序。例如:

<强> parent.js

"use strict";

const shm = require('shm-typed-array');
const fork = require('child_process').fork;

// Create shared memory
const SIZE = 20000000;
const data = shm.create(SIZE, 'Float64Array');

// Fill with dummy data
Array.prototype.fill.call(data, 1);

// Spawn child, set up communication, and give shared memory
const child = fork("child.js");
child.on('message', sum => {
    console.log(`Got answer: ${sum}`);

    // Demo only; ideally you'd re-use the same child
    child.kill();
});
child.send(data.key);

<强> child.js

"use strict";

const shm = require('shm-typed-array');

process.on('message', key => {
    // Get access to shared memory
    const data = shm.get(key, 'Float64Array');

    // Perform processing
    const sum = Array.prototype.reduce.call(data, (a, b) => a + b, 0);

    // Return processed data
    process.send(sum);
});

请注意,我们只是通过IPC从父进程向子进程发送一个小“密钥”,而不是整个数据。因此,我们节省了大量的记忆和时间。

当然,您可以将'Float64Array'(例如double)更改为您的应用所需的typed array。请注意,此库特别只处理一维类型数组;但这应该只是一个小障碍。

答案 1 :(得分:1)

我也能够重现你所经历的延迟,但也许没有你那么糟糕。我使用了以下

// main.js
const fork = require('child_process').fork

const child = fork('./getStats.js')

const dataAsNumbers = Array(100000).fill(0).map(() =>
  Array(100).fill(0).map(() => Math.round(Math.random() * 100)))

child.send({
  dataAsNumbers: dataAsNumbers,
})

并且

// getStats.js
process.on('message', function (data) {
  console.log('data is ', data)
  process.exit(0)
})
  

节点main.js 2.72s用户0.45s系统103%cpu 3.045总计

我生成100个由100个数字组成的元素来模拟您的数据,请确保您使用message上的process事件。但也许你的孩子更复杂,可能是失败的原因,也取决于你在查询中设置的超时。

如果你想获得更好的结果,你可以做的就是将你的数据分成多个部分,这些部分将被发送到子进程并重建以形成初始数组。

另一种可能性是使用第三方库或协议,即使它的工作量更多。您可以查看messenger.js甚至是AMQP队列,它可以允许您在两个进程之间与池进行通信,并保证子进程确认该消息。它有一些节点实现,如amqp.node,但它仍然需要一些设置和配置工作。

答案 2 :(得分:0)

使用内存缓存(如https://github.com/ptarjan/node-cache),让父进程使用某个键存储数组内容,子进程将通过该密钥检索内容。

答案 3 :(得分:0)

您可以考虑使用操作系统管道you'll find a gist here作为节点子应用程序的输入。

我知道这并不是您要求的,但您可以使用cluster模块(包含在节点中)。这样,您可以获得与机器核心一样多的实例来加速处理。此外,如果您不需要在开始处理之前获得所有数据,请考虑使用流。如果要处理的数据太大,我会将其存储在一个文件中,这样如果在此过程中出现任何错误,您可以重新启动。 以下是群集的示例。

var cluster = require('cluster');
var numCPUs = 4;

if (cluster.isMaster) {
    for (var i = 0; i < numCPUs; i++) {
        var worker = cluster.fork();
        console.log('id', worker.id)
    }
} else {
    doSomeWork()
}

function doSomeWork(){
    for (var i=1; i<10; i++){
        console.log(i)
    }
}

通过工作人员question 8534462发送消息的更多信息。

答案 4 :(得分:0)

为什么要制作子流程?跨过子进程发送数据的成本和实时性可能比在同一进程中进行处理时节省的成本更高。

相反,我建议对于超高效编码,您考虑在与nodejs主进程在同一内存中运行的工作线程中进行统计计算。

您可以使用NAN编写可以发布到工作线程的C ++代码,然后让该工作线程在完成后将结果和事件发布回nodejs事件循环。

这样做的好处是您不需要额外的时间将数据发送到不同的进程,但缺点是您将为线程操作编写一些C ++代码,但NAN扩展应该为你解决大部分困难的任务。

答案 5 :(得分:-1)

对于长期的流程任务,您可以使用类似gearman的内容。您可以对工作人员执行繁重的工作流程,这样您就可以设置所需的工作人员数量,例如我以这种方式处理文件,如果我需要扩展你创建更多的工人实例,我也有不同的工作人员用于不同的任务,处理zip文件,生成缩略图等,这样做的好处是工人可以写在任何语言node.js,Java,python和can轻松集成到您的项目中

// worker-unzip.js
const debug = require('debug')('worker:unzip');
const {series, apply} = require('async');
const gearman = require('gearmanode');
const {mkdirpSync} = require('fs-extra');
const extract = require('extract-zip');

module.exports.unzip = unzip;
module.exports.worker = worker;

function unzip(inputPath, outputDirPath, done) {
  debug('unzipping', inputPath, 'to', outputDirPath);
  mkdirpSync(outputDirPath);
  extract(inputPath, {dir: outputDirPath}, done);
}


/**
 *
 * @param {Job} job
 */
function workerUnzip(job) {
  const {inputPath, outputDirPath} = JSON.parse(job.payload);
  series([
    apply(unzip, inputPath, outputDirPath),
    (done) => job.workComplete(outputDirPath)
  ], (err) => {
    if (err) {
      console.error(err);
      job.reportError();
    }
  });
}

function worker(config) {
  const worker = gearman.worker(config);
  if (config.id) {
    worker.setWorkerId(config.id);
  }

  worker.addFunction('unzip', workerUnzip, {timeout: 10, toStringEncoding: 'ascii'});
  worker.on('error', (err) => console.error(err));

  return worker;
}

一个简单的index.js

const unzip = require('./worker-unzip').worker;

unzip(config); // pass host and port of the Gearman server

我通常使用PM2运行工人

与您的代码集成非常容易。

之类的东西
//initialize
const gearman = require('gearmanode');

gearman.Client.logger.transports.console.level = 'error';
const client = gearman.client(configGearman); // same host and port

只需将工作添加到队列中,传递函数名称

const taskpayload = {inputPath: '/tmp/sample-file.zip', outputDirPath: '/tmp/unzip/sample-file/'}
const job client.submitJob('unzip', JSON.stringify(taskpayload));
job.on('complete', jobCompleteCallback);
job.on('error', jobErrorCallback);