理解NodeJS 实现高并发原理

前言

我们都知道Node.js 给我们的标签是:非阻塞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的时候,可能会有这样的疑惑:
1、浏览器运行的Javascript怎么 能与操作系统进行如此底层的交互?
2、nodejs 真的是单线程吗?
3、如果是单线程,他是如何实现高并发请求的?
4、nodejs的 事件驱动是如何实现的?

为了回答这些问题,我将从下面几个方面对NodeJS进行讲解:

  • Node.js的诞生以及简介
  • Node.js单线程实现高并发原理
    单线程
    非阻塞I/O (non-blocking I/O)
    事件驱动/事件循环
  • Node.js适用场景

Node.js的诞生

Ryan Dahl

Ryan Dahl,高性能Web服务器的专家,为了解决Web 服务器的高并发性能问题,几经探索,几经挫折之后,他觉得解决问题的关键是通过事件驱动和异步I/O来达成目的,但是当时没有很好工具。
在他快绝望的时候,V8引擎来了。2008年Google发明了Chrome浏览器,使用V8引擎来解析Js程序,非常快,并且V8引擎性能好,都是异步I/O, 闭包特性方便。V8满足他关于高性能Web服务器的想象:

  1. 历史遗留问题少,都是异步I/O
  2. 强大的编译和快速执行效率(通过运用大量算法和技巧)
  3. 使用V8引擎来解析Js程序,非常快,性能足够好,执行效率远超Python和ruby等脚本语言
  4. JavaScript语言的闭包特性非常方便。

2009年的2月,按新的想法他提交了项目的第一行代码,这个项目的名字最终被定名为“node”。
2009年5月,Ryan Dahl正式向外界宣布他做的这个项目。
2009年底,Ryan Dahl在柏林举行的JSConf EU会议上发表关于Node.js的演讲,之后Node.js逐渐流行于世。

Node.js的简介

  • Node.js 是一个构建在Chrome浏览器V8引擎上的JavaScript 运行环境
    1. 底层是Chrome V8引擎 , 使用C++开发的。
    2. V8引擎本身就是用于Chrome浏览器的JS解释部分,但是Ryan Dahl把V8搬到了服务器上,用于做服务器的软件。

  • Node.js 使用了事件驱动、非阻塞I/O模型,这些都使它轻量、好用。

  • Node.js 的包生态(npm), 是世界上最大的开源库生态系统

  • Node.js 自身哲学,是花最小的硬件成本,追求更高的并发,更高的处理性能

  • Node.js是一个让JavaScript运行在服务器端的开发平台,它让JavaScript的触角伸到了服务器端,可以与PHP、JSP、Python、Ruby平起平坐。
  • Node.js不是一种独立的语言,与PHP、ASP.Net、JSP、Python、Perl、Ruby的“既是语言,也是平台”不同,Node.js的使用JavaScript进行编程,运行在Chrome V8引擎上。
  • 与PHP、JSP等相比(PHP、ASP.Net、JSP、.net都需要运行在服务器程序上),Node.js跳过了Apache、Nginx、IIS等HTTP服务器,它自己不用建设在任何服务器软件之上。Node.js的许多设计理念与经典架构(LAMP = Linux + Apache + MySQL + PHP)有着很大的不同,可以提供强大的伸缩能力。

要想理解NodeJS实现高并发的原理,我们必须先了解一下NodeJS的底层架构。

Nodejs 架构分析

Nodejs 架构分析

从这张图上,我们可以看到,NodeJS底层框架由Node.js 标准库、Node bindings、 底层库三部分组成。

1. Node.js 标准库
这部分是由 Javascript编写的,也就是我们使用过程中直接能调用的 API,在源码中的 lib 目录下可以看到。

2. Node bindings

  • 这一部分是Javascript 能够直接调用C/C++代码的关键。
  • 主要作用是把nodejs底层实现的C/C++库暴露给Javascript 环境。 Nodejs 通过一层 C++ Binding,把 JS 传入 V8, V8 解析后交给 libuv 发起 asnyc I/O, 并等待消息循环调度。
  • 可以将其理解为一个桥,桥这头是js,桥那头是C/C++,通过这个桥可以让js调用C/C++。

3.底层库

  • V8: Google 推出的 Javascript VM,也是 Node.js 为什么使用的是 Javascript的关键,它为 Javascript提供了在非浏览器端运行的环境,它的高效是 Node.js 之所以高效的原因之一。
  • Libuv:它为 Node.js 提供了跨平台,线程池,事件池,异步 I/O 等能力, Node.js高效的异步编程模型很大程度上归功于libuv的实现。。
  • C-ares:提供了异步处理 DNS 相关的能力。
  • http_parser、OpenSSL、zlib 等:提供包括 http 解析、SSL、数据压缩等其他的能力。

与操作系统交互

举个简单的例子,我们想要打开一个文件,并进行一些操作,可以写下面这样一段代码:

var fs = require('fs');
fs.open('./test.txt', "w", function(err, fd) {    
    //..do something
});

这段代码的调用过程大致可描述为:lib/fs.js → src/node_file.cc → uv_fs

流程

具体来说,当我们调用 fs.open 时,Node.js 通过 process.binding 调用 C/C++ 层面的 Open 函数,然后通过它调用 Libuv 中的具体方法 uv_fs_open,最后执行的结果通过回调的方式传回,完成流程。

我们在 Javascript中调用的方法,最终都会通过 process.binding 传递到 C/C++ 层面,最终由他们来执行真正的操作,Node.js 即这样与操作系统进行互动。

Node.js 单线程

相对于Java,PHP或者.net 等经典服务器端语言中,用户每一次请求都会为用户创建单独的线程,而每一个客户端连接创建一个线程,需要耗费2MB的内存。也就是说。理论上一个8GB的服务器可以同时连接用户数为4000个左右,要存在高并发支持更多的用户,必须要额外增加服务器的数量或者增加服务器内存数,而Web应用程序的硬件成本当然也就上升了。

Node.js 不为每个客户连接创建一个新的线程,而仅仅使用一个线程(thread)。当有用户连接了,就触发一个内部事件,通过非阻塞I/O、事件驱动机制,让 Node.js 程序宏观上也是并行的。使用 Node.js,一个8GB内存的服务器,可以同时处理超过4万用户的连接。理论上,一个8G内存的服务器,可以同时容纳3到4万用户的连接

图1.1 多线程

图1.2单线程

可以通过图1.1与图1.2看出Node.js中单线程的好处是CPU的利用率永远是100%。什么意思呢?我们假设有图1.1中的五个并发业务。一个CPU平均分配给五个业务。其中每一个业务都是先计算再I/O再计算。所谓的I/O你可以简单的理解为读取数据。那么进行I/O的时候,分配给这个线程的CPU是不工作的,得等到I/O结束继续进行计算2 CPU才又开始工作,所以这一段进行I/O的时间段,这一段线程被白白的阻塞掉了。但是在单线程的工作机制中就不一样。在进行完业务一的计算1之后,遇见I/O操作,那么CPU便马上调取业务二的计算1,依此类推,等到I/O操作结束之后再马上调取业务一的计算二。所以,在单线程中,CPU的利用率永远处于100%。

另外,单线程的带来的好处,还有操作系统完全不再有线程创建、销毁的时间开销
坏处,就是一个用户造成了线程的崩溃,整个服务都崩溃了,其他人也崩溃了。

单线程程序,当并行极大的时候,CPU理论上计算能力是100%。
多线程程序,比如PHP是这样的:CPU经常会等待I/O结束,CPU的性能就白白消耗:

只要I/O越多,NodeJS宏观上越并行。如果计算多,NodeJS宏观上越不能并行,此时网页打开速度严重变慢。

因为NodeJS想在破的机器上也能够高效运行,所以采用了单线程的模式,既然是单线程就必须异步I/O。

非阻塞I/O (non-blocking I/O)

例如,当在访问数据库取得数据的时候,需要一段时间。在传统的单线程处理机制中,在执行了访问数据库代码之后,整个线程都将暂停下来,等待数据库返回结果,才能执行后面的代码。也就是说,I/O阻塞了代码的执行,极大地降低了程序的执行效率。

由于Node.js中采用了非阻塞型I/O机制,因此在执行了访问数据库的代码之后,将立即转而执行其后面的代码,把数据库返回结果的处理代码放在回调函数中,从而提高了程序的执行效率。

当某个I/O执行完毕时,将以事件的形式通知执行I/O操作的线程,线程执行这个事件的回调函数。为了处理异步I/O,线程必须有事件循环,不断的检查有没有未处理的事件,依次予以处理。

阻塞模式下,一个线程只能处理一项任务,要想提高吞吐量必须通过多线程。而非阻塞模式下,一个线程永远在执行计算操作,这个线程的CPU核心利用率永远是100%。所以,这是一种特别有哲理的解决方案:与其人多,但是好多人闲着;还不如一个人玩命,往死里干活儿。

事件驱动/事件循环

Event Loop is a programming construct that waits for and dispatches events or messages in a program.

1、每个Node.js进程只有一个主线程在执行程序代码,形成一个执行栈(execution context stack)。

2、Node.js 在主线程里维护了一个"事件队列"(Event queue),当用户的网络请求或者其它的异步操作到来时,Node都会把它放到Event Queue之中,此时并不会立即执行它,代码也不会被阻塞,继续往下走,直到主线程代码执行完毕。

3、主线程代码执行完毕完成后,然后通过Event Loop,也就是事件循环机制,检查队列中是否有要处理的事件,这时要分两种情况:如果是非 I/O 任务,就亲自处理,并通过回调函数返回到上层调用;如果是 I/O 任务,就从 线程池 中拿出一个线程来处理这个事件,并指定回调函数,当线程中的 I/O 任务完成以后,就执行指定的回调函数,并把这个完成的事件放到事件队列的尾部,线程归还给线程池,等待事件循环。当主线程再次循环到该事件时,就直接处理并返回给上层调用。 这个过程就叫 事件循环 (Event Loop)。

4、期间,主线程不断的检查事件队列中是否有未执行的事件,直到事件队列中所有事件都执行完了,此后每当有新的事件加入到事件队列中,都会通知主线程按顺序取出交EventLoop处理。

总结:

  • 我们所看到的node.js单线程只是一个js主线程,本质上的异步操作还是由线程池完成的,node将所有的阻塞操作都交给了内部的线程池去实现,本身只负责不断的往返调度,并没有进行真正的I/O操作,从而实现异步非阻塞I/O,这便是node单线程和事件驱动的精髓之处了。

  • Nodejs之所以单线程可以处理高并发的原因,得益于libuv层的事件循环机制,和底层线程池实现。

  • Event loop就是主线程从主线程的事件队列里面不停循环的读取事件,驱动了所有的异步回调函数的执行,Event loop总共7个阶段,每个阶段都有一个任务队列,当所有阶段被顺序执行一次后,event loop 完成了一个 tick。

优缺点

node的优点:I/O密集型处理是node的强项,因为node的I/O请求都是异步的(如:sql查询请求、文件流操作操作请求、http请求...)

node的缺点:不擅长cpu密集型的操作

什么是cpu密集型操作(复杂的运算、图片的操作)

// 这就是一个cpu密集型的操作
for (let i = 0; i < 1000000; i++) {
 console.log(i);
}

适用场景

RESTful API: 请求和响应只需少量文本,并且不需要大量逻辑处理, 因此可以并发处理数万条连接。
聊天服务: 轻量级、高流量,没有复杂的计算逻辑。

参考文章

http://www.mamicode.com/info-detail-2517916.html
https://www.jb51.net/article/81262.htm

你可能感兴趣的:(理解NodeJS 实现高并发原理)