关于node.js:不要在nodejs中阻塞event-loop

简介

咱们晓得event loop是nodejs中事件处理的根底,event loop中次要运行的初始化和callback事件。除了event loop之外,nodejs中还有Worker Pool用来解决一些耗时的操作,比方I/O操作。

nodejs高效运行的秘诀就是应用异步IO从而能够应用大量的线程来解决大量的客户端申请。

而同时,因为应用了大量的线程,所以咱们在编写nodejs程序的时候,肯定要特地小心。

event loop和worker pool

在nodejs中有两种类型的线程。第一类线程就是Event Loop也能够被称为主线程,第二类就是一个Worker Pool中的n个Workers线程。

如果这两种线程执行callback破费了太多的工夫,那么咱们就能够认为这两个线程被阻塞了。

线程阻塞第一方面会影响程序的性能,因为某些线程被阻塞,就会导致系统资源的占用。因为总的资源是无限的,这样就会导致解决其余业务的资源变少,从而影响程序的总体性能。

第二方面,如果常常会有线程阻塞的状况,很有可能被歹意攻击者发动DOS攻打,导致失常业务无奈进行。

nodejs应用的是事件驱动的框架,Event Loop次要用来解决为各种事件注册的callback,同时也负责解决非阻塞的异步申请,比方网络I/O。

而由libuv实现的Worker Pool次要对外裸露了提交task的API,从而用来解决一些比拟低廉的task工作。这些工作包含CPU密集性操作和一些阻塞型IO操作。

而nodejs自身就有很多模块应用的是Worker Pool。

比方IO密集型操作:

DNS模块中的dns.lookup(), dns.lookupService()。

和除了fs.FSWatcher()和 显式同步的文件系统的API之外,其余多有的File system模块都是应用的Worker Pool。

CPU密集型操作:

Crypto模块:crypto.pbkdf2(), crypto.scrypt(), crypto.randomBytes(), crypto.randomFill(), crypto.generateKeyPair()。

Zlib模块:除了显示同步的API之外,其余的API都是用的是worker pool。

一般来说应用Worker Pool的模块就是这些了,除此之外,你还能够应用nodejs的C++ add-on来自行提交工作到Worker Pool。

event loop和worker pool中的queue

在之前的文件中,咱们讲到了event loop中应用queue来存储event的callback,实际上这种形容是不精确的。

event loop实际上保护的是一个文件描述符汇合。这些文件描述符应用的是操作系统内核的 epoll (Linux), kqueue (OSX), event ports (Solaris), 或者 IOCP (Windows)来对事件进行监听。

当操作系统检测到事件筹备好之后,event loop就会调用event所绑定的callback事件,最终执行callback。

相同的,worker pool就真的是保留了要执行的工作队列,这些工作队列中的工作由各个worker来执行。当执行结束之后,Woker将会告诉Event Loop该工作曾经执行结束。

阻塞event loop

因为nodejs中的线程无限,如果某个线程被阻塞,就可能会影响到整个应用程序的执行,所以咱们在程序设计的过程中,肯定要小心的思考event loop和worker pool,防止阻塞他们。

event loop次要关注的是用户的连贯和响应用户的申请,如果event loop被阻塞,那么用户的申请将会得不到及时响应。

因为event loop次要执行的是callback,所以,咱们的callback执行工夫肯定要短。

event loop的工夫复杂度

工夫复杂度个别用在判断一个算法的运行速度上,这里咱们也能够借助工夫复杂度这个概念来剖析一下event loop中的callback。

如果所有的callback中的工夫复杂度都是一个常量的话,那么咱们能够保障所有的callback都能够很偏心的被执行。

然而如果有些callback的工夫复杂度是变动的,那么就须要咱们认真思考了。

app.get('/constant-time', (req, res) => {
  res.sendStatus(200);
});

先看一个常量工夫复杂度的状况,下面的例子中咱们间接设置了respose的status,是一个常量工夫操作。

app.get('/countToN', (req, res) => {
  let n = req.query.n;

  // n iterations before giving someone else a turn
  for (let i = 0; i < n; i++) {
    console.log(`Iter ${i}`);
  }

  res.sendStatus(200);
});

下面的例子是一个O(n)的工夫复杂度,依据request中传入的n的不同,咱们能够失去不同的执行工夫。

app.get('/countToN2', (req, res) => {
  let n = req.query.n;

  // n^2 iterations before giving someone else a turn
  for (let i = 0; i < n; i++) {
    for (let j = 0; j < n; j++) {
      console.log(`Iter ${i}.${j}`);
    }
  }

  res.sendStatus(200);
});

下面的例子是一个O(n^2)的工夫复杂度。

这种状况应该怎么解决呢?首先咱们须要估算出零碎可能接受的响应极限值,并且设定用户传入的参数极限值,如果用户传入的数据太长,超出了咱们的解决范畴,则能够间接从用户输出端进行限度,从而保障咱们的程序的失常运行。

Event Loop中不举荐应用的Node.js外围模块

在nodejs中的外围模块中,有一些办法是同步的阻塞API,应用起来开销比拟大,比方压缩,加密,同步IO,子过程等等。

这些API的目标是供咱们在REPL环境中应用的,咱们不应该间接在服务器端程序中应用他们。

有哪些不举荐在server端应用的API呢?

  • Encryption:

crypto.randomBytes (同步版本)
crypto.randomFillSync
crypto.pbkdf2Sync

  • Compression:

zlib.inflateSync
zlib.deflateSync

  • File system:

不要应用fs的同步API

  • Child process:

child_process.spawnSync
child_process.execSync
child_process.execFileSync

partitioning 或者 offloading

为了不阻塞event loop,同时给其余event一些运行机会,咱们实际上有两种解决办法,那就是partitioning和offloading。

partitioning就是分而治之,把一个长的工作,分成几块,每次执行一块,同时给其余的event一些运行工夫,从而不再阻塞event loop。

举个例子:

for (let i = 0; i < n; i++)
  sum += i;
let avg = sum / n;
console.log('avg: ' + avg);

比方咱们要计算n个数的平均数。下面的例子中咱们的工夫复杂度是O(n)。

function asyncAvg(n, avgCB) {
  // Save ongoing sum in JS closure.
  var sum = 0;
  function help(i, cb) {
    sum += i;
    if (i == n) {
      cb(sum);
      return;
    }

    // "Asynchronous recursion".
    // Schedule next operation asynchronously.
    setImmediate(help.bind(null, i+1, cb));
  }

  // Start the helper, with CB to call avgCB.
  help(1, function(sum){
      var avg = sum/n;
      avgCB(avg);
  });
}

asyncAvg(n, function(avg){
  console.log('avg of 1-n: ' + avg);
});

这里咱们用到了setImmediate,将sum的工作分解成一步一步的。尽管asyncAvg须要执行很屡次,然而每一次的event loop都能够保障不被阻塞。

partitioning尽管逻辑简略,然而对于一些大型的计算工作来说,并不适合。并且partitioning自身还是运行在event loop中的,它并没有享受到多核零碎带来的劣势。

这个时候咱们就须要将工作offloading到worker Pool中。

应用Worker Pool有两种形式,第一种就是应用nodejs自带的Worker Pool,咱们能够自行开发C++ addon或者node-webworker-threads。

第二种形式就是自行创立Worker Pool,咱们能够应用Child Process 或者 Cluster来实现。

当然offloading也有毛病,它的最大毛病就是和Event Loop的交互损失。

V8引擎的限度

nodejs是运行在V8引擎上的,通常来说V8引擎曾经足够优良足够快了,然而还是存在两个例外,那就是正则表达式和JSON操作。

REDOS正则表达式DOS攻打

正则表达式有什么问题呢?正则表达式有一个乐观回溯的问题。

什么是乐观回溯呢?

咱们举个例子,如果大家对正则表达式曾经很相熟了。

如果咱们应用/^(x*)y$/ 来和字符串xxxxxxy来进行匹配。

匹配之后第一个分组(也就是括号外面的匹配值)是xxxxxx。

如果咱们把正则表达式改写为 /^(x*)xy$/ 再来和字符串xxxxxxy来进行匹配。 匹配的后果就是xxxxx。

这个过程是怎么样的呢?

首先(x)会尽可能的匹配更多的x,晓得遇到字符y。 这时候(x)曾经匹配了6个x。

接着正则表达式继续执行(x)之后的xy,发现不能匹配,这时候(x)须要从曾经匹配的6个x中,吐出一个x,而后从新执行正则表达式中的xy,发现可能匹配,正则表达式完结。

这个过程就是一个回溯的过程。

如果正则表达式写的不好,那么就有可能会呈现乐观回溯。

还是下面的例子,然而这次咱们用/^(x*)y$/ 来和字符串xxxxxx来进行匹配。

依照下面的流程,咱们晓得正则表达式须要进行6次回溯,最初匹配失败。

思考一些极其的状况,可能会导致回溯一个十分大的次数,从而导致CPU占用率飙升。

咱们称正则表达式的DOS攻打为REDOS。

举个nodejs中REDOS的例子:

app.get('/redos-me', (req, res) => {
  let filePath = req.query.filePath;

  // REDOS
  if (filePath.match(/(\/.+)+$/)) {
    console.log('valid path');
  }
  else {
    console.log('invalid path');
  }

  res.sendStatus(200);
});

下面的callback中,咱们本意是想匹配 /a/b/c这样的门路。然而如果用户输出filePath=///…/n,如果有100个/,最初跟着换行符。

那么将会导致正则表达式的乐观回溯。因为.示意的是匹配除换行符 n 之外的任何单字符。然而咱们只到最初才发现不可能匹配,所以产生了REDOS攻打。

如何防止REDOS攻打呢?

一方面有一些现成的正则表达式模块,咱们能够间接应用,比方safe-regex,rxxr2和node-re2等。

一方面能够到www.regexlib.com网站上查找要应用的正则表达式规定,这些规定是通过验证的,能够缩小本人编写正则表达式的失误。

JSON DOS攻打

通常咱们会应用JSON.parse 和 JSON.stringify 这两个JSON罕用的操作,然而这两个操作的工夫是和输出的JSON长度相干的。

举个例子:

var obj = { a: 1 };
var niter = 20;

var before, str, pos, res, took;

for (var i = 0; i < niter; i++) {
  obj = { obj1: obj, obj2: obj }; // Doubles in size each iter
}

before = process.hrtime();
str = JSON.stringify(obj);
took = process.hrtime(before);
console.log('JSON.stringify took ' + took);

before = process.hrtime();
pos = str.indexOf('nomatch');
took = process.hrtime(before);
console.log('Pure indexof took ' + took);

before = process.hrtime();
res = JSON.parse(str);
took = process.hrtime(before);
console.log('JSON.parse took ' + took);

下面的例子中咱们对obj进行解析操作,当然这个obj比较简单,如果用户传入了一个超大的json文件,那么就会导致event loop的阻塞。

解决办法就是限度用户的输出长度。或者应用异步的JSON API:比方JSONStream和Big-Friendly JSON。

阻塞Worker Pool

nodejs的理念就是用最小的线程来解决最大的客户连贯。下面咱们也讲过了要把简单的操作放到Worker Pool中来借助线程池的劣势来运行。

然而线程池中的线程个数也是无限的。如果某一个线程执行了一个long run task,那么就等于线程池中少了一个worker线程。

歹意攻击者实际上是能够抓住零碎的这个弱点,来施行DOS攻打。

所以对Worker Pool中long run task的最优解决办法就是partitioning。从而让所有的工作都有平等的执行机会。

当然,如果你能够很分明的辨别short task和long run task,那么咱们实际上能够别离结构不同的worker Pool来别离为不同的task工作类型服务。

总结

event loop和worker pool是nodejs中两种不同的事件处理机制,咱们须要在程序中依据理论问题来选用。

本文作者:flydean程序那些事

本文链接:http://www.flydean.com/nodejs-block-eventloop/

本文起源:flydean的博客

欢送关注我的公众号:「程序那些事」最艰深的解读,最粗浅的干货,最简洁的教程,泛滥你不晓得的小技巧等你来发现!

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理