问题
大约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/path
或rsync --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);
});