子进程使用 SIGTERM 退出(可能是由于超出最大缓冲区);如何确认这是缓冲区问题并修复它?



问题

大约30分钟后,我的子进程将退出,SIGTERM没有其他调试输出。考虑到Node.js子进程退出SIGTERM的信息,我认为该进程很可能是由于超过其maxBuffer而退出的,因为正常运行时间是不确定的,并且确实通过增加maxBuffer而得到了改善。默认为205 KB的maxBuffer,它可以持续运行1-3分钟;10 MB,可持续运行30-60分钟。

目标

子进程以大约每10分钟1MB(每秒1.66kB)的平均速率生成文本流。

文本流中的日志条目是多行的(请参阅下面组成一个日志条目的行的示例),因此我使用Node逐行解析它们,以提取感兴趣的信息(从* << Request >>- End):

*   << Request  >> 113214123 
-   Begin          req 113214077 rxreq
-   ReqMethod      GET
-   ReqURL         /ping
-   RespStatus     200
-   End   

代码

const { exec } = require('child_process');
const { createInterface } = require('readline');
const cp = exec("tail -F 2021-02-25.log", { maxBuffer: 10000000 });
createInterface(cp.stdout, cp.stdin)
.on('line', line => {
// ...
// (Implementation not shown, as it's hundreds of lines long):
// Add the line to our line-buffer, and if we've reached "-   End   " yet, parse
// those lines into a corresponding JS object and clear the line-buffer, ready
// to receive another line.
// ...
});
cp.on('close', (code, signal) => {
console.error(`Child process exiting unexpectedly. Code: ${code}; signal: ${signal}.`);
process.exit(1);
});

问题

本质上;我怎样才能避免得到SIGTERM"——但更具体地说:

  • 由于子进程超出缓冲区,我如何确认SIGTERM真的收到了?例如,有没有一种方法可以在子进程运行时检查其缓冲区的使用情况
  • 缓冲区是否可能由于Node执行行解析函数的时间过长而过载?有办法监控吗
  • 我是否错过了一个需要做的额外方面,比如手动刷新一些缓冲区

我认为在问题上添加额外的缓冲区是解决问题的错误方法;10MB似乎已经太多了,我需要能够保证无限期的正常运行时间(而不是每次失败都增加一点缓冲区)。

如何诊断子进程因超出缓冲区而退出

我在Node.js代码库的测试中搜索了提到maxBuffer的内容,找到了一个显示如何诊断子进程因超过分配的maxBuffer而退出的测试,我将在这里复制它:

// One of the tests from the Node.js codebase:
{
const cmd =
`"${process.execPath}" -e "console.log('a'.repeat(1024 * 1024))"`;
cp.exec(cmd, common.mustCall((err) => {
assert(err instanceof RangeError);
assert.strictEqual(err.message, 'stdout maxBuffer length exceeded');
assert.strictEqual(err.code, 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER');
}));
}

所以我在我的应用程序中加入了一个等效的诊断功能:

const { exec } = require('child_process');
const { createInterface } = require('readline');
/**
* This termination callback is distinct to listening for the "error" event
* (which does not fire at all, in the case of buffer overflow).
* @see https://nodejs.org/api/child_process.html#child_process_event_error
* @see https://nodejs.org/api/child_process.html#child_process_child_process_exec_command_options_callback
* @param {import("child_process").ExecException | null} error 
* @param {string} stdout
* @param {string} stderr 
* @type {import("child_process").SpawnOptions}
*/
function terminationCallback(error, stdout, stderr){
if(error === null){
// Healthy termination. We'll get an exit code and signal from
// the "close" event handler instead, so will just defer to those
// logs for debug.
return;
}
console.log(
`[error] Child process got error with code ${error.code}.` + 
` instanceof RangeError: ${error instanceof RangeError}.` +
` Error message was: ${error.message}`
);
console.log(`stderr (length ${stderr.length}):n${stderr}`);
console.log(`stdout (length ${stdout.length}):n${stdout}`);
}
const cp = exec(
"tail -F 2021-02-25.log",
{ maxBuffer: 10000000 },
terminationCallback
);
createInterface(cp.stdout, cp.stdin)
.on('line', line => {
// ...
// Implementation not shown
// ...
});
cp.on('close', (code, signal) => {
console.error(
`Child process exiting unexpectedly. ` + 
`Code: ${code}; signal: ${signal}.`
);
process.exit(1);
});

事实上,当我运行我的应用程序几分钟时,我发现这个终止回调被调用了,并且它满足了Node.js测试中对由于超出缓冲区而退出的子进程的所有断言。

我还注意到,终止回调中返回的stdout正好有1000000个字符长——这与我设置为maxBuffer的字节数完全匹配。正是在这一点上,我开始理解require("child_process").exec()require("child_process").spawn()之间的区别。

如何使子进程能够安全地从stdout流式传输任何数量的数据

exec()和spawn()具有重叠的功能,但最终适用于不同的目的,这在子进程文档中并没有真正说明。线索就在他们所接受的结构论点中。

  • exec()接受终止回调,其选项支持maxBuffer(但不支持stdio)
  • spawn()不接受终止回调,其选项支持stdio(但不支持maxBuffer)

这里的标题是:

  • exec()适用于有明确结束的任务(在该任务上,您可以获取子进程在整个工作过程中一直累积到其缓冲区中的stdout/stderr)
  • spawn()适用于可能无限期运行的任务,因为您可以配置stdout/stderr/stdin流的管道连接位置。options.stdio"pipe"的默认配置将它们管道连接到父进程(Node.js应用程序),适用于我们需要建立readline接口并逐行使用stdout的示例。除了操作系统本身强加的缓冲区限制之外,没有其他明确的缓冲区限值(应该相当慷慨!)

所以,如果你正在编写一个Node.js应用程序,该应用程序管理一个子进程,该子进程的任务是无限期运行:

  • 不间断地监视日志(例如tail -F 2021-02-25.log)并对其进行解析
  • 运行实时流媒体服务(例如ffmpeg <some complex args here>)

。。。您应该使用spawn()

相反,对于具有明确结束和可预测、合理缓冲区大小的任务(例如mkdir -vp some/dir/pathrsync --verbose <src> <dest>),则可以继续使用exec()

当然,两者之间可能还有其他区别,但流处理的这一方面确实很有影响力。

如何使用spawn()重写

只需要更改两行(其中一行只是import语句)!注意,"pipe"的默认options.stdio值在这里是合适的,所以我们甚至不需要传入options对象。

const { spawn } = require('child_process');
const { createInterface } = require('readline');
const cp = spawn("tail", ["-F", "2021-02-25.log"]);
createInterface(cp.stdout, cp.stdin)
.on('line', line => {
// ...
// (Implementation not shown, as it's hundreds of lines long):
// Add the line to our line-buffer, and if we've reached "-   End   " yet, parse
// those lines into a corresponding JS object and clear the line-buffer, ready
// to receive another line.
// ...
});
cp.on('close', (code, signal) => {
console.error(`Child process exiting unexpectedly. Code: ${code}; signal: ${signal}.`);
process.exit(1);
});

最新更新