Giter Site home page Giter Site logo

notes's People

Contributors

teng-boy avatar

Watchers

 avatar

notes's Issues

【Node】如果有个请求在处理复杂运算,发生阻塞了,后续请求还能进node服务吗

问题:如果有个请求在处理复杂运算,发生阻塞了,后续请求还能进node服务吗?

一起来看下面的案例

const http = require('http');
const longComputation = () => {
  let sum = 0;
  for (let i = 0; i < 1e10; i++) {
    sum += i;
  };
  return sum;
};
const server = http.createServer();
server.on('request', (req, res) => {
  if (req.url === '/compute') {
    console.info('计算开始',new Date());
    const sum = longComputation();
    console.info('计算结束',new Date());
    return res.end(`Sum is ${sum}`);
  } else {
    res.end('Ok')
  }
});

server.listen(3000);

启动后,先请求http://localhost:3000/compute,再请求http://localhost:3000/test,会发现在/compute接口响应之前,/test没有进入node服务,也不会正常响应。因为/compute接口在做复杂运算,阻塞了后续请求。所以如果还有人问你【如果有个请求在处理复杂运算,发生阻塞了,后续请求还能进node服务吗?】,你可以肯定的回答他【No】。

node event loop 宝典

本文只讨论node事件循环

什么是事件循环?

事件循环使Node.js可以通过将操作转移到系统内核中来执行非阻塞I/O操作。

由于大多数现代内核都是多线程的,因此它们可以处理在后台执行的多个操作。当这些操作之一完成时,内核会告诉Node.js,以便可以将适当的回调添加到轮询队列中以最终执行。

有点抽象,简单暴力上图,简化概述事件循环操作顺序

事件循环

注意:每个框将被称为事件循环的“阶段”。

每个阶段都有一个要执行的回调FIFO队列。尽管每个阶段都有其自己的特殊方式,但是通常,当事件循环进入给定阶段时,它将执行该阶段特定的任何操作,然后在该阶段的队列中执行回调,直到队列耗尽或回调的最大数量为止已执行。当队列已用完或达到回调限制时,事件循环将移至下一个阶段,依此类推。

阶段概述

  • times: 此阶段执行由setTimeout() setInterval()的回调
  • pending callbacks: 执行推迟到下一个循环迭代的I / O回调
  • idle, prepare: 仅在内部使用
  • poll: 检索新的I / O事件;执行与I / O相关的回调(除了关闭回调和计时器的回调,几乎所有其他回调);适当时,节点将在此处阻塞
  • check: setImmediate()在这里调用回调
  • close callbacks: 一些关闭回调,例如socket.on('close', ...)

下面我们通过一个案例来理解消化一下事件循环的执行过程

假设计划在100毫秒阈值后执行超时,然后脚本开始异步读取耗时95毫秒的文件

const fs = require('fs');

function someAsyncOperation(callback) {
  // Assume this takes 95ms to complete
  fs.readFile('/path/to/file', callback);
}

const timeoutScheduled = Date.now();

setTimeout(() => {
  const delay = Date.now() - timeoutScheduled;

  console.log(`${delay}ms have passed since I was scheduled`);
}, 100);

// do someAsyncOperation which takes 95 ms to complete
someAsyncOperation(() => {
  const startCallback = Date.now();

  // do something that will take 10ms...
  while (Date.now() - startCallback < 10) {
    // do nothing
  }
});

当事件循环进入轮询阶段时,它有一个空队列(fs.readFile()尚未完成),因此它将等待剩余的毫秒数,直到达到最快的计时器阈值为止。在等待95 ms过去时,fs.readFile()完成读取文件并将其需要10 ms完成的回调添加到轮询队列并执行。回调完成后,队列中不再有回调,因此事件循环将看到已达到最快计时器的阈值,然后返回到计时器阶段以执行计时器的回调。

现在,我们来看看经典面试题

async function async1(){
    console.log('async1 start')
    await async2() // 2
    console.log('async1 end') // 8
  }
async function async2(){
    console.log('async2') // 3
}
async1(); // 1
new Promise(function(resolve){
    console.log('promise1') // 4
    resolve();
    console.log('promise2') // 5
}).then(function(){
    console.log('promise3') // 9
})
process.nextTick(() => console.log('nextTick')); // 7
console.log('script end') // 6

熟悉的朋友肯定能很快得出答案,输出结果是:

async1 start
async2
promise1
promise2
script end
nextTick
async1 end
promise3

代码执行过程如代码块 1-9 所示。其中函数async1()里,先输出async1 startasync2,遇到await返回,继续寻找下一个同步代码new Promise执行输出promise1promise2,之后的同步代码块是script end。到这里,同步代码执行完了。下面开始执行异步代码,而process.nextTick是在事件循环之前执行,所以异步代码块最先输出nextTick。最后剩async1 endpromise3,因为函数async1()在队列中的位置比Promise前,所以先输出async1 end,再输出promise3

总结一下,分清哪些是macrotask,哪些是microtask,哪些的执行优先级比较高,再结合event loop的原理,再分析类似问题,会更加得心应手。

【Node】生产环境别在 console.log 了

示例

while (true) {
  console.log("Hello, World!");
}

不久之后输出

...
Hello, World!
Hello, World!
Hello, World!
Hello, World!
Hello, World!

<--- Last few GCs --->

  710976 ms: Mark-sweep 698.3 (737.7) -> 698.3 (737.7) MB, 15343.7 / 0 ms [allocation failure] [GC in old space requested].
  726188 ms: Mark-sweep 698.3 (737.7) -> 698.3 (737.7) MB, 15212.6 / 0 ms [allocation failure] [GC in old space requested].
  741831 ms: Mark-sweep 698.3 (737.7) -> 698.3 (737.7) MB, 15642.7 / 0 ms [last resort gc].
  757723 ms: Mark-sweep 698.3 (737.7) -> 698.3 (737.7) MB, 15892.2 / 0 ms [last resort gc].


<--- JS stacktrace --->

==== JS stack trace =========================================

Security context: 0x37c258b9 <JS Object>
    1: nextTick [node.js:~465] [pc=0x341fac9c] (this=0x37c7f939 <a process with map 0x563136b1>,callback=0x3e7bf5e1 <JS Function afterWrite (SharedFunctionInfo 0x3e777679)>)
    2: arguments adaptor frame: 5->1
    3: onwrite(aka onwrite) [_stream_writable.js:~314] [pc=0x341f3f50] (this=0x37c08099 <undefined>,stream=0x3e7bf6b1 <a WriteStream with map 0x56322bc9>,er=0x37c08099 <undefined>)
    4: _w...

FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - process out of memory
Aborted

在node环境,仅仅是输出个string,为什么会内存不足?这是一个已知的"问题",因为在tty / console的情况下写入stdout是异步的。因此,如果tty / console无法跟上,那么非常快速地记录大量数据很可能会导致大量写入缓冲在内存中。

console的关键代码如下

// internal/console/constructor.js
Console.prototype[kBindStreamsLazy] = function(object) {
  let stdout;
  let stderr;
  Object.defineProperties(this, {
    '_stdout': {
      enumerable: false,
      configurable: true,
      get() {
        if (!stdout) stdout = object.stdout;
        return stdout;
      },
      set(value) { stdout = value; }
    },
    '_stderr': {
      ...
    }
  });
};

着重观察下 _stdout对象。当我们运行js代码,执行console.log的时候,就会调用_stdout对象。
那么,内容在输出时,是缓存在哪里呢?
请继续看下面的代码

// _http_common.js
const parsers = new FreeList('parsers', 1000, function parsersCb() {
  const parser = new HTTPParser();
  ...
  return parser;
});

当请求进来时,上面的new FreeList会申请大小为1000的缓冲区,我们再看看freelist干了什么

// freelist.js
  constructor(name, max, ctor) {
    this.name = name;
    this.ctor = ctor;
    this.max = max;
    this.list = [];
  }
  ...
  free(obj) {
    if (this.list.length < this.max) {
      this.list.push(obj);
      return true;
    }
    return false;
  }

调用free的时候,会将obj压入list。
当程序在运行时,如果使用console.log输出内容很频繁,还没有gc前,内存占用会很高。

结论:生产环境最好不要console.log,可以将内容输出到文件,就可以解决console.log带来的内存占用问题。

构造函数使用call继承方法与原型链继承方法的内存使用对比

我们定义一个函数,经常会这样:

function a(name){
	this.name = name;
}

如果想得到a的实例,new a()即可。给a添加say方法,可以这样做:

function a(name){
	this.name = name;
	this.say = function(){
		console.log(`hello `, this.name);
	}
}
var v = new a('teng-boy');
v.say();

上面代码,会输出hello teng-boy

疑问:如果有很多子类继承了a中的say方法,会发生什么呢?

function a(name){
  this.name = name;
  this.say = function(){
    console.log(`hello `, this.name);
  }
}
function b(name){
    a.call(this, name);
}
//用循环模拟1000000个子类
for(let i=0; i<1000000; i++){
    var v = new b('teng-boy');
}
console.log(process.memoryUsage());

在node环境中打印内存使用情况:

{ rss: 35753984,
  heapTotal: 15204352,
  heapUsed: 10977504,
  external: 8252 }

疑问:如果将say方法定义在a的原型上呢?

function a(name){
    this.name = name;
}
a.prototype.say = function(){
    console.log(`hello `, this.name);
}
function b(name){
    a.call(this, name);
}
b.prototype = Object.create(a.prototype);
//用循环模拟1000000个子类
for(let i=0; i<1000000; i++){
    var v = new b('teng-boy');
}
console.log(process.memoryUsage());

在node环境中打印内存使用情况:

{ rss: 27287552,
  heapTotal: 8912896,
  heapUsed: 4204088,
  external: 8252 }

由上面两种内存的使用情况来看,把公用的方法定义在原型链上,内存占用是优于构造函数的。

原因分析,直接上代码

//第一种,构造函数call继承方法
function a(name){
	this.name = name;
	this.say = function(){
		console.log(`hello `, this.name);
	}
}
function b(name){
    a.call(this, name);
}
var one = new b('teng-boy1');
var two = new b('teng-boy2');

console.log(one.name === two.name); //false
console.log(one.say === two.say); //false

//第二种,原型链继承方法
function a(name){
  this.name = name;
}
a.prototype.say = function(){
  console.log(`hello `, this.name);
}
function b(name){
  a.call(this, name);
}
b.prototype = Object.create(a.prototype);
var one = new b('teng-boy1');
var two = new b('teng-boy2');

console.log(one.name === two.name); //false
console.log(one.say === two.say); //true

可见,使用原型链继承,利用js沿原型链查找的特性,让a的所有子类的say,都指向a的say。
而不使用原型链继承,每继承一次,就声明一次say函数,造成了内存的浪费。所以在保证性能的
情况下,原型链的继承在内存占用方面,要优于构造函数call的继承。

说明:这是作者的随记,本着互相学习的心态写的,如果有什么不对的地方,欢迎指正。另,欢迎star

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.