1.结论
我们秉承着结论现行的原则,结论就是如果你的node服务请求或者处理任务比较密集的情况下,还是要谨慎使用console.error/log等打印大的对象。因为疯狂打印大的对象 还是会让你的服务 heapTotal 变大。
简单来说就是 打印log要有节制哦!
2. 简单的例子
哎 我们线上服务打印的log的疯狂程度,和对象的大小也不亚于下面的了
var showMem = function() {
var mem = process.memoryUsage();
var format = function(bytes) {
return (bytes/1024/1024).toFixed(2)+'MB';
};
console.error('WHX heapTotal '+format(mem.heapTotal) + ' heapUsed ' + format(mem.heapUsed) + ' rss ' + format(mem.rss));
};
function test () {
var sMQID = "00000000000000000000000000";
var oDownCfg = { url: '/',
jsonData: '大对象',
jsonData2: '大对象' ,
jsonData3: '大对象' }
console.error(LOGHEAD, "sMQID:", sMQID, "oDownCfg:", oDownCfg);
}
for (let i = 0; i < 60000; i ++) {
test();
showMem();
}
瞅一瞅:
如果我们注释掉上面的console.error的效果:
没有注释掉的话:
艾玛 明显感觉到 我的控制台 都跟不上打印了。
3.官方文档介绍
先总结一下:对于打印console.log来说实际上相当于process.stdout去做的,而再往下看是net.Socket流去做的。
process.stdout
process.stdout 属性返回连接到 stdout (fd 1) 的流。 它是一个 net.Socket 流(也就是双工流),除非 fd 1 指向一个文件,在这种情况下它是一个可写流。
例如,要将 process.stdin 拷贝到 process.stdout:
process.stdin.pipe(process.stdout);
process.stdout 与其他的 Node.js 流有重大区别。 有关更多信息,参见有关进程 I/O 的注意事项。
process.stdout.fd#
进程 I/O 的注意事项#
process.stdout and process.stderr 与 Node.js 中其他 streams 在重要的方面有不同:
- console.log() 和 console.error() 内部分别是由它们实现的。
- 写操作是否为同步,取决于连接的是什么流以及操作系统是 Windows 还是 POSIX :
- 文件:在 Windows 和 POSIX 上是同步的。
- TTY(终端):在 Windows 上是异步的,在 POSIX 上是同步的。
- 管道(和 socket):在 Windows 上是同步的,在 POSIX 上是异步的。
这些行为部分是历史原因,改变他们可能导致向后不兼容,而且他们的行为也符合部分用户的预期。
同步写避免了调用 console.log() 或 console.error() 产生不符合预期的交错输出问题,或是在异步写完成前调用了process.exit()导致未写完整。 查看process.exit() 获取更多信息。
同步写将会阻塞事件循环直到写完成。 有时可能一瞬间就能写到一个文件,但当系统处于高负载时,管道的接收端可能不会被读取、缓慢的终端或文件系统,因为事件循环被阻塞的足够频繁且足够长的时间,这些可能会给系统性能带来消极的影响。当你向一个交互终端会话写时这可能不是个问题,但当生产日志到进程的输出流时要特别留心。
如果要检查一个流是否连接到了一个 TTY 上下文, 检查 isTTY 属性。
4.源码概览
(本文源码版本为node-v14.15.1)
下载地址:
\lib\internal\console\constructor.js
看一看我们的log打印的方法:
const consoleMethods = {
log(...args) {
this[kWriteToConsole](kUseStdout, this[kFormatForStdout](args));
},
warn(...args) {
this[kWriteToConsole](kUseStderr, this[kFormatForStderr](args));
},
//......下面的就省略的
}
我们接着往下看:
下面可以看到 直接就会调用_stdout.write去打印喽,这里面说的_stdout 就是上面官方文档介绍的了。
[kWriteToConsole]: {
...consolePropAttributes,
value: function(streamSymbol, string) {
const ignoreErrors = this._ignoreErrors;
const groupIndent = this[kGroupIndent];
const useStdout = streamSymbol === kUseStdout;
const stream = useStdout ? this._stdout : this._stderr;
const errorHandler = useStdout ?
this._stdoutErrorHandler : this._stderrErrorHandler;
if (groupIndent.length !== 0) {
if (string.includes('\n')) {
string = string.replace(/\n/g, `\n${groupIndent}`);
}
string = groupIndent + string;
}
string += '\n';
if (ignoreErrors === false) return stream.write(string);
// There may be an error occurring synchronously (e.g. for files or TTYs
// on POSIX systems) or asynchronously (e.g. pipes on POSIX systems), so
// handle both situations.
//走这里 ignoreErrors在lib\internal\console\global.js
//globalConsole[kBindProperties](true, 'auto'); 声明是true
try {
// Add and later remove a noop error handler to catch synchronous
// errors.
if (stream.listenerCount('error') === 0)
stream.once('error', noop);
stream.write(string, errorHandler);
} catch (e) {
// Console is a debugging utility, so it swallowing errors is not
// desirable even in edge cases such as low stack space.
if (isStackOverflowError(e))
throw e;
// Sorry, there's no proper way to pass along the error here.
} finally {
stream.removeListener('error', noop);
}
}
因为是通过net.Socket流去做的。因为在tty / console的情况下写入stdout是可能是同步(POSIX )或者异步(windows)。因此,如果tty / console无法跟上,非常快速地记录大量数据很可能会导致大量写入缓冲在内存中。
\lib\_http_common.jsconst parsers = new FreeList('parsers', 1000, function parsersCb() {
const parser = new HTTPParser();
cleanParser(parser);
parser[kOnHeaders] = parserOnHeaders;
parser[kOnHeadersComplete] = parserOnHeadersComplete;
parser[kOnBody] = parserOnBody;
parser[kOnMessageComplete] = parserOnMessageComplete;
return parser;
});
再瞅瞅FreeList 我们可以发现会申请一个1000的数组喽。 调用free的时候,会将obj塞入list。 当程序在运行时,如果使用console输出内容很频繁,还没有gc前,内存占用会很高。
class FreeList {
constructor(name, max, ctor) {
this.name = name;
this.ctor = ctor;
this.max = max;
this.list = [];
}
alloc() {
return this.list.length > 0 ?
this.list.pop() :
ReflectApply(this.ctor, this, arguments);
}
free(obj) {
if (this.list.length < this.max) {
this.list.push(obj);
return true;
}
return false;
}
}
当然有点疑惑 谁用了parsers 这个呀。
这个就在\lib\internal\child_process.js方法里面。
'net.Socket': {
send(message, socket, options) {}}
参考文章:
艾玛 大致就这样了 ,简单的做一个记录。要是有问题也欢迎指点,我再卡卡改。