Node前端小白兔

理解NodeJS 实现高并发原理

2020-03-15  本文已影响0人  moofyu

前言

我们都知道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的诞生

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的简介

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

Nodejs 架构分析

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

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

2. Node bindings

3.底层库

与操作系统交互

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

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的优点: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

上一篇下一篇

猜你喜欢

热点阅读