在NestJS HTTP服务器中,使用子进程时,CPU密集型进程会阻塞工作池。

16

Node版本:v10.13.0

我正在尝试在NodeJS上进行一个简单的测试,涉及到重度CPU计算的请求并发。我知道NodeJS不是处理CPU密集型进程的最佳工具,并且不应该 系统地生成子进程,但是这段代码是为了测试子进程的工作原理。此外,这是使用NestJS编写的TypeScript代码。

src/app.controller.ts

import { Get, Param, Controller } from '@nestjs/common';
import fork = require('child_process');

@Controller()
export class AppController {
  @Get()
  async root(): Promise<string> {
    let promise = new Promise<string>(
        (resolve, reject) => {
          // spawn new child process
          const process = fork.fork('./src/cpu-intensive.ts');
          process.on('message', (message) => {
            // when process finished, resolve
            resolve( message.result);
          });
          process.send({});    
        }
    );    
    return await promise;
  }
}

src/cpu-intensive.ts

process.on('message', async (message) => {
  // simulates a 10s-long process
  let now = new Date().getTime();
  let waittime = 10000; // 10 seconds
  while (new Date().getTime() < now + waittime) { /* do nothing */ };
  // send response to master process
  process.send({ result: 'Process ended' });
});

如果不生成新的子进程,执行这样一个漫长的过程会导致下面的结果时间轴,其中有5个并发请求(从#1到#5)。每个进程都会阻塞循环事件,每个请求必须等待前面的请求完成才能得到响应。

Time 0    10   20   30   40   50
#1   +----+
#2   +----+----+
#3   +----+----+----+
#4   +----+----+----+----+
#5   +----+----+----+----+----+

在生成新的子进程时,我期望每个进程都会由CPU上的不同逻辑核心并发处理(我的CPU有8个逻辑核心),从而导致这个预测的时间轴:

Time 0    10   20   30   40   50
#1   +----+
#2   +----+
#3   +----+
#4   +----+
#5   +----+

但是,我在每次测试中都观察到这个奇怪的结果:

Time 0    10   20   30   40   50
#1   +----+
#2   +----+----+
#3   +----+----+----+
#4   +----+----+----++
#5   +----+----+----+-+

前三个请求的行为表现出工作线程池已经饱和,尽管我原本认为会创建三个不同的线程池。后两个请求非常令人困惑,因为它们的行为表现得像是与第三个请求并发执行。

目前我正在寻找以下问题的解释:

  • 为什么前三个请求不表现得像是在并发运行
  • 为什么最后三个请求表现得像是在并发运行

请注意,如果我添加另一个“快速”方法如下所示:

  @Get('fast')
  async fast(): Promise<string> {
    return 'Fast process ended.';
  }

该方法不受并发运行的CPU密集型进程的影响,回复始终即时。


有任何更新吗? - Gonzalo Lorieto
1
据我所知,这是因为当我们调用子进程并开始执行 CPU 密集型工作时,我们的主线程正在执行同步操作,因此无法处理来自子进程的响应。当主线程完成其工作后,它才能处理来自子进程的响应。所有操作都在并行执行,但由于同步操作正在进行,主线程无法处理响应。我希望这有意义。对于 POC,您可以在子进程中执行一些操作,并检查该操作是否在启动时间内完成。 - Aabid
更多细节请查看此节点问题 https://github.com/nodejs/node/issues/14917 - Aabid
@mihai:是的,它是一致的。 - Bob
@Aabid:我查看了你发布的问题,我理解spawn()可能会在事件循环中进行一些阻塞。然而,我很难将其与我的“前三个请求被阻止;后三个同时处理”联系起来。也许我在某个地方失去了你的意思? - Bob
显示剩余2条评论
1个回答

2

我在我的电脑上执行了测试用例,一切正常,请你在你的电脑上检查一下。

Node 版本: v8.11.2 操作系统: macOS High Sierra 10.13.4,8 核

child-process-test.js

const child_process = require('child_process');  
for(let i=0; i<8; i++) {  
    console.log('Start Child Process:',i,(new Date()));
    let worker_process = child_process.fork("cpu-intensive-child.js", [i]);    
    worker_process.on('close', function (code) {  
        console.log('End Child Process:', i , (new Date()), code);  
    });
}

cpu-intensive-child.js

const fs = require('fs');
// simulates a 10s-long process
let now = new Date().getTime();
let waittime = 10000; // 10 seconds
while (new Date().getTime() < now + waittime) { /* do nothing */ };
// send response to master process
// process.send({ result: 'Process ended' });

输出

enter image description here

您可以在输出中看到,所有过程的差异仅为10秒,您可以在您的计算机上执行此测试用例并让我知道,也许可以有所帮助。


确实可以工作。但这不是同一个用例,因为这里没有外部事件处理,对吧?感谢您的努力! - Bob
是的,在这里没有外部事件处理。在您的用例中,您已经创建了一个 http-server,否则没有区别。我认为两者应该是相同的。 - Aabid
他们可能应该这样做,但他们没有像我一样使用NestJS。因此,在HTTP服务器层上必须有某些东西会产生我所描述的奇怪行为?至少你强调了它不是child_process引起的。我将尝试减少用例(不直接使用NestJS种子)。 - Bob

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接