使用了这么久的Node.js
,之前只是知道它怎么使用,并没有去深入的了解它的机制,那么今天就来聊聊它的使用机制优缺点以及适合使用的场景
是什么
Node.js
是JavaScript
运行平台,底层是采用c++编写的,基于JavaScript V8引擎
,意味着你可以使用JavaScript
编写服务器端代码,然后交给Node.js
去解释执行
特点
- 事件驱动
- 非阻塞异步
I/O
- 模块化
- 单线程
运行机制解析
应用程序的瓶颈所在
应用程序的请求过程可以分为两个部分:CPU
运算和 I/O
读写,CPU
计算速度通常远高于磁盘读写速度,这就导致CPU
运算已经完成,但是不得不等待磁盘I/O
任务完成之后再继续接下来的业务。所以I/O
才是应用程序的瓶颈所在,在I/O
密集型业务中,I/O
操作会使用大量的时间,而CPU
只需很短的时候,这样就会使得性能不够好,那么一般都会使用多线程去优化,同时开启多个线程去处理请求,这样就会快很多。
但是由于一个CPU
核心在一个时刻只能做一件事情,操作系统只能通过将CPU
切分为时间片的方法,让线程可以较为均匀的使用CPU
资源。但操作系统在内核切换线程的同时也要切换线程的上下文,当线程数量过多时,上下文切换会造成系统的很大开销,所以在大并发时,多线程结构还是无法做到强大的伸缩性。
使用Node.js解决瓶颈问题
Node.js
的单线程并不是真正的单线程,只是开启了单个线程进行业务处理(cpu
的运算),同时开启了其他线程专门去处理I/O
。当一个指令到达主线程,主线程发现有I/O
之后,直接把这个事件传给 I/O
线程,不会等待I/O
结束后,再去处理下面的业务,而是拿到一个状态后立即往下走,这就是单线程、异步I/O
。
I/O
操作完之后呢?Node.js
的I/O
处理完之后会有一个回调事件,这个事件会放在一个事件处理队列里头,在进程启动时node
会创建一个类似于While(true)
的循环,它的每一次轮询都会去查看是否有事件需要处理,是否有事件关联的回调函数需要处理,如果有就处理,然后加入下一个轮询,如果没有就退出进程,这就是所谓的事件驱动。
那么当我是多核CPU
的时候呢?还有就是当主线程业务运算超时,岂不是来不及处理事件队列里的事件?
对于这俩个问题,首先要做的一点就是在代码编写的时候尽量避免耗时的计算,将大计算进行拆分,这样能够让主线程及时得到释放,处理消费事件队列里头的事件。其次,node.js
提供了child_process
模块开启子进程,理想状态下每个进程各自利用一个CPU
,以此实现多核的利用,child_process
提供创建子进程,以及进程状态监控,进程之间通信的API
等。
一个Node.js饭店的发展历程
前面的一堆理论似乎不太好明白,最后讲一个关于饭店发展历程的故事作为结尾吧。
第一年
饭店开张,只有一个厨师(同时还兼任老板、服务员、打荷、收银员),当一个客人点餐之后,这个厨师就开始记录(服务员),然后他就开始备菜(打荷)、炒菜(厨师)、然后上菜(服务员)、收钱(收银员),这个时候即使有其他客人来了,等着吧还没忙完呢。这个厨师就这样兢兢业业,有条不紊的干着每一件事,因为每件事都是亲力亲为,都不能出错,虽然所有的事情都了然于心,但效率很低,一天只能卖出十多份饭菜。
这是饭店单线程的第一年:
利:它没有线程上下文交换所带来性能上的开销(因为每件事都是亲力亲为);
弊:无法利用多核CPU
(厨房空间那么大,完全可以很多人一起干活),同时错误会引起整个应用退出,应用的健壮性值得考验(当这个厨师生病,或者有事了饭店就得停业)第二年
这个厨师第一年赚了点钱,回到老家把表哥、表弟全拉过来,现在他们有5个人,可工作方式还是跟厨师第一年的时候一样。当客人来了,就会有一个人去记录,然后自己去厨房洗菜、切菜、炒菜、洗碗、上菜、收钱。当来了第六个客人的时候,就要等待前面的人做完所有事情才能空出一个人来接待。后来他们就想既然客户多了,厨师就得多,再回老家多叫几个兄弟吧。这时新的问题发生了,当每个厨师做完饭后就出去找客人,客人说我刚刚点餐了,然后厨师就去厨房问,刚刚是哪个表兄接待的那个客户,要是没有人接待的话,我来处理。就这样忙忙碌碌一年,他们比去年多做了好多生意,但是感觉每天客户多的时候,厨房里头乱糟糟的,总要询问这个询问那个。
这就是饭店多线程的一年:
利:一个线程服务一个请求,线程之间可以共享数据,这样可以避免内存的浪费,可以同时处理多个请求;
弊:操作系统内核在切换线程的同时也要切换线程的上下文,当线程数量过多时,时间将会被消耗在上下文切换上(厨房里头乱糟糟的,总要询问这个询问那个)。第三年
老板娘过来了(Node.Js
闪亮登场),她发现这帮厨师都在各自为战,自己拿到客户的点餐后去洗菜、切菜、洗碗、炒菜、上菜、收钱,一个人只能同时处理一个任务,而且作为厨师没必要去做洗菜、切菜、洗碗、收银之类费时的工作。
所以老板娘把所有人进行了分工:
老板作为厨师长(Node
里头的主线程),他不再去洗碗、洗菜、切菜、炒菜、收银(我们可以把洗碗、洗菜、炒菜、切菜认为是比较耗时的I/O
),他只负责将收银台小妹(观察者)拿过来的菜单分配给不同的厨师,打荷(这些人就是不同的I/O
线程),吩咐下去之后他不会等菜出来再走(进入下一个轮询),又问收银台小妹还有没有菜单要做,如果有继续轮询,如果没有了休息(退出进程)。当菜做出来之后放在上菜区(回调),收银台就显示菜出来了(将回调放入事件队列),当老板查询收银员(观察者)的时候,收银员就告诉厨师长,厨师长就通知服务员(处理不同I/O
的线程)上菜(完成回调),这样饭店有条不紊的运行下去,客人也越来越多了。
有些高端客户不想在这挤,所以老板娘就想,饭店房子那么大(多核CPU
),可以叫她弟弟(子进程的主线程)在这开个子店(child_process
),然后发现一个收银员不够用,那就多招几个收银员(多个观察者),就这样每个分店(进程)只有一个主管(主线程),主管的弱点就是无暇顾及洗碗等杂活(I/O
),只能关注业务,至于饭店能开多大(应用程序能处理多大请求),要看主管的处理能力(主线程的编程强壮度)。最后,大饭店起了个时髦的名字叫Hotel NodeJs
!
优缺点
优点
- 高并发(最重要的优点)
- 适合I/O密集型应用
- 现在新版本的
Node.js
已经可以使用子进程来充分利用多核CPU
缺点:
- 不适合CPU密集型应用;
由于JavaScript单线程的原因,如果有长时间运行的计算(比如大循环),将会导致CPU时间片不能释放,使得后续I/O无法发起;
解决方案:分解大型运算任务为多个小任务,使得运算能够适时释放,不阻塞I/O调用的发起; - 可靠性低,一旦代码某个环节崩溃,整个系统都崩溃
原因:单进程,单线程
解决方案:
(1)Nnigx反向代理,负载均衡,开多个进程,绑定多个端口;
(2)开多个进程监听同一个端口,使用cluster模块;
(3)使用child_process
- 回调太多,代码不易读
- Debug不方便
使用NodeJS的场景
适合使用
- RESTful API
Node.js
可以充分发挥其非阻塞I/O
模型以及JavaScript
对JSON
的功能支持(如JSON.stringfy
函数) - 单页面、多Ajax请求应用
如Gmail,前端有大量的异步请求,需要服务后端有极高的响应速度 - 基于
Node.js
开发Unix命令行工具
Node.js
可以大量生产子进程,并以流的方式输出,这使得它非常适合做Unix
命令行工具 - 流式数据
传统的Web应用,通常会将HTTP
请求和响应看成是原子事件。而Node.js
会充分利用流式数据这个特点,构建非常酷的应用。如实时文件上传系统transloadit
不适合使用
-
CPU
使用率较重、I/O
使用率较轻的应用
如视频编码、人工智能等,Node.js
的优势无法发挥 - 简单
Web
应用
此类应用的特点是,流量低、物理架构简单,Node.js
无法提供强大的框架