node.js中console学习

888 阅读4分钟

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 在重要的方面有不同:

  1. console.log()console.error() 内部分别是由它们实现的。
  2. 写操作是否为同步,取决于连接的是什么流以及操作系统是 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)

下载地址:

nodejs.org/zh-cn/

\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) {}}

参考文章:

nodejs.cn/api/process…

艾玛 大致就这样了 ,简单的做一个记录。要是有问题也欢迎指点,我再卡卡改。