10. 为什么我们不要在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);
});