问题
我的子进程在使用SIGTERM
大约30分钟后退出,没有其他调试输出。考虑到关于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
?例如,是否有一种方法可以在子进程运行时检查其缓冲区使用情况?我认为在这个问题上抛出额外的缓冲区是解决问题的错误方法;10 MB看起来已经太过了,我需要能够保证无限期的正常运行时间(而不是每次失败时增加一些缓冲区)。
发布于 2021-02-26 18:50:55
如何诊断子进程因超出其缓冲区而退出
我在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()
接受终止回调,它的选项支持maxBuffer
(但不支持斯迪奥)。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语句)!请注意,这里的默认options.stdio
值"pipe"
是适当的,因此我们甚至不需要传入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);
});
https://stackoverflow.com/questions/66372463
复制相似问题