nodejs学习

1.1 Node.js中的事件驱动

事件驱动模式是Node.js实现高并发关键点之一。Node.js的事件机制由EventEmitter模块实现,Node中很多可以发送事件的核心模块都继承自该模块。

在Web服务器中,我们可以使用以下方式来处理用户请求:

var http = require('http');

//创建一个HTTP服务器
var server = http.createServer();

//监听request事件
server.on('request', function (req, res) { 
  //对request事件的处理	
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Hello World!')
});
server.listen(3000)

在上例中,我们为server添加了'request'事件上监听。当收到用户请求时该事件对应的回调函数会被调用,在回调函数中可以进行用户请求的处理与响应等操作。

传统Web服务器处理用户请求时,用户请求只能依次处理,如果处理时间较长会造成请求的阻塞。不同于传统的Web服务器,事件驱动使得Node创建的Web服务器非常的高效,用户不必须等待上个用户请求处理完成,就可以接收新的请求,并在处理完成后调用对应回调函数响应请求即可。事件驱动回调机制赋予了Node.js强大并发处理能力。


1.2. 事件循环机制

Node.js的事件循环是靠一个单线程不断地查询队列中是否有事件,当读取到事件时,将调用与这个事件关联的回调函数。上面介绍的事件驱动编程是事件循环的具体表现形式,如:在前面创建Web服务器中,回调函数是一个I/O操作;Node.js会监听3000端口是否有HTTP连接,当收到连接时会启动这个I/O操作,但不会等待操作的完成而是继续查看是否有下一个事件,如果有则继续依次处理。

Node.js的事件循环机制由以下几部分组成:

事件生产者:Node.js通过EventEmitter模块发送事件,发送的事件会被放到事件队列中。

事件队列:事件队列(Event Queue)是一个FIFO模型,一端用于接收推入的事件,另外一端拉出要处理的事件。

事件循环:事件循环(Event Loop)是Node.js事件机制的关键点,它一个单线程运行的任务,会不断轮询事件队列,并将轮询到的事件放到线程池中进行处理。

线程池:线程池(Thread Pool)是真正执行事件和任务处理的位置,比较耗时的操作如:网络I/O、文件操作I/O及其它会引起阻塞的操作都会在这里处理。处理完成后,会调用事件对应的回调函数。

nodejs学习

结合上图及前面介绍可以认为,Node.js单线程运行最直接的体现,是指其在事件循环的时候是单线程运行的。在事件循环的时候,Node线程会把当前事件的读出,并将其放入到线程池处理,然后开始下一事件处理。

在Node的底层有一个libuv库,它实现了事件循环线程池等功能。线程池是系统级别任务处理,也就是说事件的实际处理里是在更底层系统级完成的,在这一过程中并不是单线程的。处理完成后,线程池会调用与之绑定的回调函数,这样处理结果又被传回到了Node中。


2、Libuv

Libuv 是 Node.js 关键的一个组成部分,它为上层的 Node.js 提供了统一的 API 调用,使其不用考虑平台差距,隐藏了底层实现。

具体它能做什么,官网的这张图体现的很好:

nodejs学习

可以看出,它是一个对开发者友好的工具集,包含定时器,非阻塞的网络 I/O,异步文件系统访问,子进程等功能。它封装了 Libev、Libeio 以及 IOCP,保证了跨平台的通用性。

3、一些可能的瓶颈

这里只见到讨论下自己的理解,欢迎指正。

首先,文件的 I/O 方面,用户代码的运行,事件循环的通知等,是通过 Libuv 维护的线程池来进行操作的,它会运行全部的文件系统操作。既然这样,我们抛开硬盘的影响,对于严谨的 C/C++ 来说,这个线程池一定是有大小限制的。官方默认给出的大小是 4。当然是可以改变的。在启动时,通过设置 UV_THREADPOOL_SIZE 来改变这个值即可。不过,最大也只能是 128,因为这个是涉及到内存占用的。

这个线程池对于所有的事件循环是共享的。当一个函数要使用线程池的时候(比如调用 uv_queue_work),Libuv 会预先分配并初始化 UV_THREADPOOL_SIZE 所允许的线程出来。而128 占用的内存大约是 1MB,如果设置的太高,当使用线程池频繁时,会因为内存占用过多而降低线程的性能。具体说明;

对于网络 I/O 方面,以 Linux 系统下来说,网络 I/O 采用的是 epoll 这个异步模型。它的优点是采用了事件回调的方式,大大降低了文件描述符的创建(Linux下什么都是文件)。

在每次调用 epoll_wait 时,实际返回的是就绪描述符的数量,根据这个值,去 epoll 指定的数组里面取对应数量的描述符,是一种 内存映射 的方式,减少了文件描述符的复制开销。

上面提到的 epoll 指定的数组,它的大小即可监听的数量大小,它在不同的系统下,有不同的默认值,可见这里 epoll create

#ifndef __NR_epoll_create
  # if defined(__x86_64__)
  # define __NR_epoll_create 213
  # elif defined(__i386__)
  # define __NR_epoll_create 254
  # elif defined(__arm__)
  # define __NR_epoll_create (UV_SYSCALL_BASE + 250)
  # endif
  #endif /* __NR_epoll_create */

有了大小的限制,还远不够,为了保证运行的稳定,防止你在调用 epoll 函数时,指针越界,导致内存泄漏。还会用到另外一个值 maxevents,它是 epoll_wait 所能处理的最大数量,在调用 epoll_wait 时可以指定。一般情况下小于创建时(epoll_create)的数组大小,当然,也可以设置的比 size 大,不过应该没什么用。可以想到如果就绪的事件很多,超过了 maxevents,那么超出的事件就要等待前面的事件处理完成,才可以继续,可能会导致效率的下降。

在这种情况下,你可能会担心事件会丢失。其实,是不会丢失的,它会通过 ep_collect_ready_items 将这些事件保存在一个队列中,在下一个 epoll_wait 再进行通知。