Node.js的特点
前言
简单理解单线程实现高并发原理:
1、Nodejs与操作系统交互,我们在 Javascript 中调用的方法,最终都会通过 process.binding 传递到 C/C++ 层面,最终由他们来执行真正的操作。Node.js 即这样与操作系统进行互动。
2、nodejs所谓的单线程,只是主线程是单线程,所有的网络请求或者异步任务都交给了内部的线程池去实现,本身只负责不断的往返调度,由事件循环不断驱动事件执行。
3、Nodejs之所以单线程可以处理高并发的原因,得益于libuv层的事件循环机制,和底层线程池实现。
4、Event loop就是主线程从主线程的事件队列里面不停循环的读取事件,驱动了所有的异步回调函数的执行,Event loop总共7个阶段,每个阶段都有一个任务队列,当所有阶段被顺序执行一次后,event loop 完成了一个 tick。
既然nodejs是单线程的,那么他怎么处理多请求高并发的?
单线程解决高并发的思路就是采用非阻塞,异步编程的思想。简单概括就是当遇到非常耗时的IO操作时,采用非阻塞的方式,继续执行后面的代码,并且进入事件循环,当IO操作完成时,程序会被通知IO操作已经完成。主要运用JavaScript的回调函数来实现。
多线程虽然也能解决高并发,但是是以建立多个线程来实现,其缺点是当遇到耗时的IO操作时,当前线程会被阻塞,并且把cpu的控制权交给其他线程,这样带来的问题就是要非常频繁的进行线程的上下文切换。
非阻塞异步io
例如,当在访问数据库取得数据的时候,需要一段时间。在传统的单线程处理机制中,在执行了访问数据库代码之后,整个线程都将暂停下来,等待数据库返回结果,才能执行后面的代码。也就是说,I/O
阻塞了代码的执行,极大地降低了程序的执行效率。
由于 Node.js 中采用了非阻塞型I/O
机制,因此在执行了访问数据库的代码之后,将立即转而执行其后面的代码,把数据库返回结果的处理代码放在回调函数中,从而提高了程序的执行效率。
当某个I/O
执行完毕时,将以事件的形式通知执行I/O
操作的线程,线程执行这个事件的回调函数。为了处理异步I/O
,线程必须有事件循环,不断的检查有没有未处理的事件,依次予以处理。
阻塞模式下,一个线程只能处理一项任务,要想提高吞吐量必须通过多线程。而非阻塞模式下,一个线程永远在执行计算操作,这个线程的CPU核心利用率永远是100%。所以,这是一种特别有哲理的解决方案:与其人多,但是好多人闲着;还不如一个人玩命,往死里干活儿。
单线程
在 Java、PHP 或者 .net 等服务器端语言中,会为每一个客户端连接创建一个新的线程。而每个线程需要耗费大约2MB内存。也就是说,理论上,一个8GB内存的服务器可以同时连接的最大用户数为4000个左右。要让Web应用程序支持更多的用户,就需要增加服务器的数量,而 Web 应用程序的硬件成本当然就上升了。
Node.js不为每个客户连接创建一个新的线程,而仅仅使用一个线程。当有用户连接了,就触发一个内部事件,通过非阻塞I/O、事件驱动机制
,让 Node.js 程序宏观上也是并行的。使用 Node.js ,一个8GB内存的服务器,可以同时处理超过4万用户的连接。
另外,单线程带来的好处,操作系统完全不再有线程创建、销毁的时间开销。但是单线程也有很多弊端,会在 Node.js 的弊端详细讲解,请继续看。
事件驱动
在 Node.js
中,客户端请求建立连接,提交数据等行为,会触发相应的事件。在 Node.js
中,在一个时刻,只能执行一个事件回调函数,但是在执行一个事件回调函数的中途,又有其他事件产生,可以转而处理其他事件(比如,又有新用户连接了),然后返回继续执行原事件的回调函数,这种处理机制,称为“事件循环”机制。
Node.js
底层是 C++
(V8
也是C++写的)。底层代码中,近半数都用于事件队列、回调函数队列的构建。用事件驱动来完成服务器的任务调度,这是鬼才才能想到的。针尖上的舞蹈,用一个线程,担负起了处理非常多的任务的使命。
注意这里的事件循环,也可以说是 Node.js
的一个精髓所在,下面引用一段 Node.js
官网的内容
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
引用Node官网中的一段内容:
注意:每个框将被称为事件循环的“阶段”。每个阶段都有一个要执行的回调
FIFO
队列。虽然每个阶段都以其自己的方式特殊,但通常情况下,当事件循环进入给定阶段时,它将执行特定于该阶段的任何操作,然后在该阶段的队列中执行回调,直到队列耗尽或最大回调数量为止已执行。当队列耗尽或达到回调限制时,事件循环将移至下一阶段,依此类推。
跨平台
起初,Node
只能在 Linux
平台上运行。后来随着 Node
的发展,微软注意到了它的存在,并投入了一个团队帮助 Node
实现 Windows
平台的兼容,在v0.6.0
版本发布时,Node
已经能够直接在 Window
平台运行了。Node 是基于libuv
实现跨平台的。
Node.js 的弊端
单线程带来的弊端
Node.js中有一个特点就是单线程,它带来了很多好处,但是它也有弊端,单线程弱点如下。
-
无法利用多核CPU
-
错误会引起整个应用退出无法继续调用异步
I/O
-
大量计算占用CPU导致无法继续调用异步
I/O
以上确实是Node的弊端,但是都会有一些对应的解决方案:
弊端1:解决方案
-
(1)一些管理工具比如
pm2,forever
等都可以实现创建多进程解决多核 CPU 的利用率问题。 -
(2)在v0.8版本之前,实现多进程可以使用
child_process
-
(3)在v0.8版本之后,可以使用
cluster
模块,通过主从模式,创建多个工作进程解决多核CPU的利用率问题。
弊端2:解决方案
-
(1)Nnigx反向代理,负载均衡,开多个进程,绑定多个端口;
-
(2) 一些管理工具比如
pm2,forever
等都可以实现进程监控,错误自动重启等 -
(3)开多个进程监听同一个端口,使用Node提供的
cluster
模块; -
(4)未出现
cluster
之前,也可以使用child_process
,创建多子线程监听一个端口。 -
(5)这里说明下,有上面的这些解决方案,但是写node后端代码的时候,异常抛出
try catch
显得格外有必要。
弊端3:解决方案
-
(1)可以把大量的密集计算像上面一样拆分成多个子线程计算
-
(2)但是如果不允许拆分,想计算100万的大数据,在一个单线程中,Node确实显得无能为力,这本身就是V8内存限制的弊端。
Node.js 的应用场景
介绍了Node.js的特点和弊端,再说一下Node.js的应用场景。
Node.js适合用来开发什么样的应用程序呢?
善于I/O
,不善于计算。因为Node.js
最擅长的就是任务调度,如果你的业务有很多的 CPU
计算,实际上也相当于这个计算阻塞了这个单线程,就不太适合Node开发,但是也不是没有解决方案,只是说不太适合。
当应用程序需要处理大量并发的I/O
,而在向客户端发出响应之前,应用程序内部并不需要进行非常复杂的处理的时候,Node.js
非常适合。Node.js
也非常适合与websocket
配合,开发长连接的实时交互应用程序。
具体场景可以表现为如下:
-
第一大类:用户表单收集系统、后台管理系统、实时交互系统、考试系统、联网软件、高并发量的web应用程序;
-
第二大类:基于web、canvas等多人联网游戏;
-
第三大类:基于web的多人实时聊天客户端、聊天室、图文直播;
-
第四大类:单页面浏览器应用程序;
-
第五大类:操作数据库、为前端和移动端提供基于
json
的API; -
第六大类,....