nodejs eventloop 和libuv

NodeJS and Chrome eventloop

Node.js and Chrome do not use the same event loop implementation. Chrome/Chromium uses libevent, while node.js uses libuv.

Node’s API provides a kind of asynchronous no-op, setImmediate. For that function, the “some operation” I’ve mention above is “do nothing”, after which an item is immediately added to the end of the event queue.

There is a more powerful process.nextTick which adds an event to the front of the event queue, effectively cutting in line and making all other queued events wait. If called recursively, this can cause prolonged delay for other events (until reaching maxTickDepth).

  • 问题由来

    事件驱动、异步、单线程、非阻塞I/O,这是我们听得最多的关于nodejs的介绍,连nodejs官网都是这么写的:

    Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient, perfect for data-intensive real-time applications that run across distributed devices.

    过去很长时间里,我都愿意接受这些“耳熟能详”的观点,直到最近,遇到过很多性能问题之后,我才开始思考,nodejs的内部机制到底是怎样的,nodejs的性能瓶颈在哪里?

    问题

    • nodejs既然是单线程,如何实现异步I/O?
    • nodejs如何实现非阻塞I/O的?
    • nodejs事件驱动是如何实现的?
    • nodejs全是异步调用和非阻塞I/O,就真的不用管并发数了吗?
    • nodejs如何靠js和操作系统打交道的?

    概念

    探讨上面问题之前,我们先看下这些概念是什么意思:

    • 事件驱动:
      所谓的事件驱动是对一些操作的抽象,比如 鼠标点击抽象成一个事件,收到请求抽象成一个事件,事件是对异步的一种实现。
    • 同步/异步
      所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回。
      当一个异步过程调用发出后,调用者不会立刻得到结果。实际处理这个调用的部件是在调用发出后,通过状态、通知来通知调用者,或通过回调函数处理这个调用。
    • 阻塞/非阻塞

    阻塞调用是指调用结果返回之前,当前线程会被挂起。函数只有在得到结果之后才会返回。
    非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。

注意: 很多人弄混了 同步/异步和 阻塞/非阻塞 的关系,实际上他们并不是对等的,同步不一定会阻塞,只是方法没有返回不代表线程被挂起了,实际上你也可以去做别的工作。异步也并不代表一定是非阻塞,它可以立即返回函数,但是在获取回调的时候采用了不断轮训的方式挂起了线程。

nodejs内部揭秘

要弄清楚上面的问题,首先要弄清楚nodejs是怎么工作的。

nodejs

这张图就是nodejs的内部构造。最上面一层就是我们常用的nodejs API,都是通过js封装好的,node-bings是指对底层c/c++代码的封装后和js打交道的部分,属于交界区域,这部分大都是原生API源码调用c++的情况,用户是不需要直接使用c++模块的。
然后就是底层首先是V8引擎,这个我们非常熟悉,他就是 js 的解析引擎,它的作用就是“翻译”js给计算机看,然而我们今天关注的重点并不是V8.在这里我们也看出来node是v8的关系,v8是js解释引擎,node是js的runtime,相当于浏览器是js的runtime一样,我们接下来解释的东西大都发生在runtime上面。
libuv,早期是libev和libeio组成,后来被抽象成libuv,它就是node和操作系统打交道的部分,由它来负责文件系统、网络等等底层工作。也是我们今天重点关注对象。剩下那些这次按住不表。

libuv简介

一张图揭示了libuv在node中的作用

architecture

可以看出,几乎所有和操作系统打交道的部分都离不开 libuv的支持。libuv也是node实现跨操作系统的核心所在。

现在我们可以回答js是如何同底层操作系统打交道的了?
就是通过libuv,一张简化的图如下(以fs为例):

libuv作用

上面提到过异步和非阻塞IO的特点,那么我们看 nodejs既然是单线程,如何实现异步I/O ?
聪明的你可能马上想到了,js执行线程是单线程,把需要做的I/O交给libuv,自己马上返回做别的事情,然后libuv在指定的时刻回调就行了。其实简化的流程就是酱紫的!细化一点,nodejs会先从js代码通过node-bings调用到C/C++代码,然后通过C/C++代码封装一个叫 请求对象 的东西交给libuv,这个请求对象里面无非就是需要执行的功能+回调之类的东西,给libuv执行以及执行完实现回调。

nodejs异步模型

顺便回答了问题 nodejs真的是单线程吗?,只有js执行是单线程,I/O显然是其它线程,比如我们看到libuv起码要一个线程接受nodejs的异步请求并执行,当然远不止这样,我们后面再说。

libuv何时执行回调?

我们上面提到了libuv接过了js传递过来的 I/O请求,那么何时来处理回调呢?
有人说这还不简单,I/O完了我就回调行不行。这是极度不安全的做法,我们知道js执行是单线程的,如果两个回调同时回来,或者js线程正在工作状态,将会出现回调竞争的情况,这在一个单线程的模式下面是不应该出现的问题,所以,libuv有一个事件循环(event loop)的机制,来接受和管理回调函数的执行。

event loop是libuv的核心所在,上面我们提到 js 会把回调和任务交给libuv,libuv何时来调用回调就是 event loop 来控制的。event loop 首先会在内部维持多个事件队列(或者叫做观察者 watcher),比如 时间队列、网络队列等等,使用者可以在watcher中注册回调,当事件发生时事件转入pending状态,再下一次循环的时候按顺序取出来执行,而libuv会执行一个相当于 while true的无限循环,不断的检查各个watcher上面是否有需要处理的pending状态事件,如果有则按顺序去触发队列里面保存的事件,同时由于libuv的事件循环每次只会执行一个回调,从而避免了 竞争的发生。libuv官方的event loop执行图:

loop_iteration
哪天有时间了详细讲一下这个循环的过程,也很有意思

文件I/O

上面有副图提到了libuv在nodejs中的作用,右半部分 文件I/O ,DNS 和用户的代码对应的是线程池的机制,它的执行过程大概就是:
1 js层面调用如fs.open等指令通过node-bindings转成c/c++代码。
2 把回调函数等封装成一个请求对象,如果线程池有空闲线程,交给一个线程去执行。
3 执行完成在libuv的事件循环中的文件观察中注入一个回调事件,这个事件中会向上转换成js的回调并执行。

在这里我们就看到了线程池的概念,发现nodejs并不是单线程的,而且还有并行事件发生。同时,线程池默认大小是 4 ,也就是说,同时能有4个线程去做文件i/o的工作,剩下的请求会被挂起等待直到线程池有空闲。 nodejs全是异步调用和非阻塞I/O,就真的不用管并发数了吗?得到了回答。

线程池的大小可以通过 UV_THREADPOOL_SIZE 这个环境变量来改变 或者在nodejs代码中通过 process.env.UV_THREADPOOL_SIZE来重新设置。大概的工作流程可参考下面的流程图:

event-loop_0

还有深入浅出中的图也很有代表性:
Node异步IO流程

网络I/O

libuv的网络I/O采用了纯事件机制,其实现是使用的操作系统底层方法,在不同的操作系统中选择了不同的解决方案,比如linux下面使用的是 epoll,在windows下面使用的是IOCP等。
以linux为例,epoll是linux下面非常高效的一种异步I/O解决方案,nginx便是采用的这种方案,通过epoll(见下图)可以实现事件通知机制,网络内核在接收到任何绑定了的事件之后都会通知绑定者,然后执行相应的代码。

epoll

所以我们可以理解为, js绑定事件-> libuv绑定事件 -> 网络内核监听事件. 内核事件触发 -> libuv -> js回调的过程。 所以网络I/O 并没有并发数的限制,因为它也没有线程池的概念,在一个线程中飞快的处理各种回调。

网络I/O的执行流程和文件I/O不同的地方就在于它并没有线程池,而是通过事件机制交给了操作系统去做,操作系统响应或者处理了请求就会触发libuv的callback,进而传递到nodejs执行相应的业务代码。

FROM HERE

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s