我可以屈服于子进程并在Node.js中返回响应吗?

时间:2017-09-24 21:47:47

标签: javascript node.js heroku koa

简而言之,我遇到了一个问题,即对我的Node.js服务器发出多个并行GET请求导致服务器被堵塞""并挂起,从而导致客户端超时(503,服务不可用)。

经过大量的性能分析后,我发现它是一个CPU问题。特定请求(我们称之为GET /foo)通过HTTP查询来自多个服务的数据,然后进行大量计算,并将结果返回给客户端,如下所示:

  1. 客户请求GET /foo
  2. /foo控制器通过HTTP查询来自多个其他服务的数据
  3. 然后
  4. /foo控制器对数据进行一堆迭代,为客户端编译一些输出
  5. 步骤3大约需要2秒钟才能完成。但是,如果我并行向/foo发送2个请求,则每个客户端将在大约4秒内收到响应。当我在使用更多内核的群集中运行应用程序时,请求运行得更快,但不是我想要的。

    好像我有几个选择:

    1. 预先计算响应(理想情况下现在想避免这种情况,因为它需要整个"缓存失效"方案),或
    2. /foo将CPU阻塞计算异步发送到另一个进程(使用Heroku,这将是另一个dyno),然后我可以使用websocket或其他东西将结果推送到客户端(同样,非常我的情况很复杂),或
    3. 以某种方式屈服于请求中的子进程并将结果返回给客户端
    4. 愿意做类似选项3的事情。

      get('/foo', function*(request) {
        // I/O, so not blocking the event loop (I think)
        let data = yield getData(request)
      
        // make this happen in a different process
        let response = yield doSomeHeavyProcessing(data)
      
        return response
      })
      

      我上面省略了很多实施细节,但如果有必要知道,我使用Koa和Node.js 6。

      理想情况下,doSomeHeavyProcessing会在某个单独的过程中执行CPU密集型计算,并且当它完成时,仍会将结果发送回"同步"时尚的请求客户。

      一直试图围绕儿童流程,网络工作者,纤维等,并一直在做一些基本的"你好世界"用这些来让他们基本上做到以上,但无济于事。如有必要,可以发布更多细节。

2 个答案:

答案 0 :(得分:2)

以下是您可以尝试的一些方法:

<强> 1 拆分小块中的阻塞计算,并使用setImmediate将下一块工作放在事件队列的末尾。因此计算不再阻塞,可以处理其他请求。

<强> 2 微软最近发布了napajs。如其自述文件中所述

  

随着它的发展,我们发现在CPU绑定任务中补充Node.js很有用,能够在多个V8隔离区中执行JavaScript并在它们之间进行通信。

我还没有尝试过,但看起来很有希望:

var napa = require('napajs');
var zone1 = napa.zone.create('zone1', { workers: 4 });

get('/foo', function*(request) {
  let data = yield getData(request)

  let response = yield zone1.execute(doSomeHeavyProcessing, [data])

  return response
})

3。如果上述情况都不够,并且您需要将负载分散到多台计算机上,那么您可能无法避免使用某种消息队列将工作分配到不同的服务器。在这种情况下,请查看ZeroMQ。它非常易于从节点使用,您可以使用它实现任何类型的分布式消息传递模式。

答案 1 :(得分:1)

为方便起见,您可以将Child process与其他包装一起使用。

worker.js - 此模块将在一个单独的进程中运行,并将执行繁重的工作

const crypto = require('crypto');

function doHeavyWork(data) {
  return crypto.pbkdf2Sync(data, 'salt', 100000, 64, 'sha512');
}

process.on('message', (message) => {
  const result = doHeavyWork(message.data);
  process.send({ id: message.id, result });
});

client.js - 子进程的便利(但原始)包装器

const cp = require('child_process');

let worker;
const resolves = new Map();

module.exports = {
  init(moduleName, errorCallback) {
    worker = cp.fork(moduleName);
    worker.on('error', errorCallback);

    worker.on('message', (message) => {
      const resolve = resolves.get(message.id);
      resolves.delete(message.id);
      if (!resolve) {
        errorCallback(new Error(`Got response from worker with unknown id: ${message.id}`));
        return;
      }

      resolve(message.result);
    });

    console.log(`Service PID: ${process.pid}, Worker PID: ${worker.pid}`);
  },

  doHeavyWorkRemotly(data) {
    const id = `${Date.now()}${Math.random()}`;

    return new Promise((resolve) => {
      worker.send({ id, data });
      resolves.set(id, resolve);
    });
  }
}

我使用fork()来使用文档中声明的其他通信渠道。

此外,我保留所有提交给工作进程请求(const resolves = new Map();)的记录,并仅在工作进程返回特定请求的响应时解析Promises(resolve(message.result);)(const resolve = resolves.get(message.id);

run.js - 一个启动模块,它利用co来“执行”生成器。

const co = require('co');

const client = require('./client');

function errorCallback(error) {
  console.log('Got an unexpected error!');
  console.log(error);
}

client.init('./worker.js', errorCallback);

function* run() {
  while(true) {
    yield client.doHeavyWorkRemotly('mydata');
  }
}

co(run);

要测试它只需运行node run.js,它就会打印

  

服务PID:XXXX,工人PID:XXXX

然后看看CPU利用率,工作进程可能需要大约100%的CPU,而服务将非常闲置。