定义

Node.js® is a JavaScript runtime built on Chrome’s V8 JavaScript engine.

现在 Node.js 官网的定义就这么简单,但也可以看出几个最重要的特征

  1. Node.js 不是一门语言,是一个运行时,和浏览器更像,只不过运行在服务端
  2. 这个运行时的方言是 JavaScript(不包含 BOM、DOM API,增加了 Stream、网络等 API)
  3. Node.js 是靠 Chrome V8 引擎运行 JavaScript

对应到 Java 我们可以理解 Node.js 是 JDK,装上就能在服务端跑 JavaScript 代码了。

Chrome 和 Node.js 同样是 JavaScript 运行时,都使用了 V8 引擎,主要区别在于 V8 只实现了 ECMAScript 的数据类型、对象和方法,Chrome 运行时提供了 Window、DOM、BOM,而 Node.js 运行时提供了global、 Buffer、net 等模块

下面内容需要一些计算机基础知识,但看不懂并不影响 Node.js 的学习

事件驱动 & 非阻塞 I/O 是什么

在 Node.js 才诞生的时候大家总是充满了好奇,早期官网上的介绍要更多一些,主要说了 Node 最核心的两个特性:事件驱动、非阻塞 I/O

Node.js® is a JavaScript runtime built on Chrome’s V8 JavaScript engine. Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient. Node.js’ package ecosystem, npm, is the largest ecosystem of open source libraries in the world.

举个例子理解 Node.js 和之前大部分 web 应用编程区别,当读取数据库的时候会写出这样的代码

  1. var result = db.query('select * from...');

I/O 是一个相对耗时较长的工作(这是后面讨论的前提),I/O 任务主要由 CPU 分发给 DMA 执行,等待数据库查询结果的时候进程在做什么?大部分时候就是单纯在等着而已

不同硬件设备 I/O 操作所花费 CPU cycles

  1. Action Cost (CPU cycles)
  2. L1 Cache* 3
  3. L2 Cache* 14
  4. RAM* 250
  5. Disk 41,000,000
  6. Network 240,000,000

这明显是在浪费 CPU,所以有了多线程的性能优化手段,但学习操作系统的时候我们就知道

  1. 操作系统创建线程和切换多和线程上下文需要一定的开销
  2. 因为多线程带来的执行堆栈是要占用内存的
  3. 多线程变成面对的死锁、状态同步等问题会增加使用的复杂性

上面的代码要么阻塞整个进程,要么使用了多线程,如果进程不等待 I/O 结果,直接处理后续任务就是非阻塞 I/O,这样可以不用浪费 CPU

  1. db.query('select * from...', function (result) {
  2. // 消费 result
  3. });

在 Promise、async|await 没有的年代,回调是异步的通用处理方式

进程如何获知异步 I/O 调用完成,触发回调函数呢?这就要靠 Event Loop 实现,也就是上面提到的事件驱动
image.png

这个图看起来非常复杂,有几个要点可以帮助理解

  1. 在 Node.js 中所有操作称之为事件,客户端的请求也是事件,所有事件维护在图中最左侧的事件队列中
  2. Node.js 主线程也就是图中间的循环就是 Event Loop,主要作用是轮训事件队列中是否存在事件
    1. 有非阻塞事件,按照先进先出原则依次调用处理
    2. 有阻塞事件,交给图中最右侧的 C++ 线程池处理,线程池处理完成后把结果通过 Event Loop 返回给事件队列
    3. 进行下一次循环
  3. 一个请求所有事件都被处理,把响应结果发给客户端,完成一次请求

这样一个请求 - 响应模型就完成了,如果在 Event Loop 中包含同步的 CPU 密集操作,就会阻塞主线程

Node.js 性能真的高吗?

要回答这个问题首先需要了解几个基本常识

  1. CPU 运算远远快于 I/O 操作
  2. Web 是典型的 I/O 密集场景
  3. JavaScript 是单线程,但 JavaScript 的 runtime Node.js 并不是,负责 Event Loop 的 libuv 用 C 和 C++ 编写

很多语言是依赖的多线程解决高并发,一个线程处理一条用户请求,处理完成了释放线程,在阻塞 I/O 模型下, I/O 期间该用户线程所占用的 CPU 资源(虽然十分微量,大部分交给了 DMA)什么都不做,等待 I/O,然后响应用户,而且开启多个进程/线程 CPU 切换 Context 的时间也十分可观

就像饭店的服务员只负责点菜,如果给每个厨师都配一个服务员,服务员把客人菜单给大厨后就玩手机等着一样,你是老板你也生气,况且不同于饭店大厨工资高于服务员,在计算机世界,CPU 资源比 I/O 宝贵的多

说 Node.js 在高并发、I/O 密集场景性能高,也就是 Web 场景性能高主要也是解决这个问题,没必要一个厨师配一个服务员,整个饭店说不定一个服务员就够了,剩下的钱可以随便做其它事情

用户请求来了, CPU 的部分做完不用等待 I/O,交给底层完成,然后可以接着处理下一个请求了,快就快在

  1. 非阻塞 I/O
  2. Web 场景 I/O 密集
  3. 没多线程 Context 切换开销,多出来的开销是维护 EventLoop

其它场景 NodeJS 性能确实不高,甚至非常低下,感兴趣可以看一下 Apache(多进程) 和 Nginx(事件驱动) 对比,现在大型 web 应用普遍是 Nginx 在最前面做负载均衡服务器、静态资源服务器,Apache 在下一层做实际 Web Server,响应动态请求

因此 Node.js 在 I/O 密集的 Web 场景相对于使用多进程模型语言有性能优势,这个优势不是来源于语言,而是操作系统实现,Java 按照这种模型实现性能一样很高

得益于 V8 的优化和 C/C++ 拓展,Node.js 执行 CPU 密集任务性能并不差,但如果长时间进行 CPU 运算会阻塞后续 I/O 任务发起,用 Java 实现非阻塞模型也会遇到一样问题

参考

  1. Node.js 作者 Ryan Dahl 介绍 Node.jsRyan Dahl 2009 JSconf - Node.js.pdf
  2. NGINX 如何实现高性能和可扩展性
  3. 深入理解 JavaScript Event Loop