为满足云智慧透视宝用户对Node.js的代码级性能监控需求,我们的程序猿Else对Node.js的工作原理和运行机制进行了大量的深入研究,而本次分享正是来自Else的心得体会。
本文分为俩个部分:
第一部分:通过应用层面,给大家演示什么Node.js,它能做什么,怎么去做。
第二部分:在大家对Node.js有个基本认识之后,从理论层面谈谈Node.js的运行机制。
Node.js应用解析
Node.js是能够跨平台的,今晚我们分享将基于windows,但linux或者macro大致相同。
nodejs环境搭建
首先登陆node官网下载安装包,地址是:nodejs.org/en/
下载完成之后,安装步骤就是傻瓜式的下一步下一步:
安装完成之后在windows下打开powershell,然后执行 " node -v "就能查看到我们node的版本信息,这也就说明我们的node已经安装成功。
第一个demo
之后我们找个目录新建一个项目文件夹,然后在项目文件夹下面新建一个hello__.js的文件,将如下代码写入hello__.js的文件:
而后我们cd到项目目录下,运行node hello__.js
然后在浏览器里输入127.0.0.1:8899就能直接访问我们的服务了
我们在代码中用了 var http = require("http"); 这种方式引用了node的一个自身模块http,模块可以理解成DotNet的类库、Java的包,然后应用这个http模块快速建了一个服务,这也是node给开发人员带来的便利。
NPM
说到便利,对于node.js还必须要提到NPM,在上个demo中我们用到var http = require("http"); 来引用模块,我们也说到了http是node自身的模块,那么如果需要引用一些node的非自身模块,应该如何做呢?
这个任务的第一步就交给NPM,在安装node的同时已经将NPM工具安装完成,我们只需要执行NPM -v就能查看NPM的版本,直接输入NPM可以查看相关帮助。
NPM的最常见用法就是安装依赖模块,当安装完成之后我们的项目就能对安装的依赖模块进行引用。现在我们拿 express模块来举个栗子,首先需要cd 到项目的node_modules目录下,然后运行npm install express:
当你看到这样的界面的时候就说明模块已经安装成功,回到项目目录下就能看见模块目录下新增了一个express目录,然后在项目目录下创建一个hello.js的脚本文件,文件内容如下:
下一步就是启动这个项目,直接运行 node .\hello.js
然后在浏览器里输入地址127.0.0.1:8081
这种方式引用模块非常方便。那么我们是不是也可以编写自己的第三方模块上传到npm库给他人使用呢?答案是肯定的!接下再举个栗子给大家说明下,首先在项目根目录下创建一个npmtest文件夹:
在当前目录下创建一个hello.js文件,内容如下:
然后运行npm init 命令,在运行命令之后会要求你输入一些模块打包的描述信息,这些信息最终会生成一个包的描述文件叫package.json
输入所有信息之后会问你"Is this ok?",输入yes就完成了打包过程。查看文件夹下面会增加一个描述包文件:
然后大家再look下我们生成描述文件的内容:
可以看到这些信息都是刚才输入的内容。包打好了,接下来就是上传到仓库,上传仓库前我们先注册一个账号npm adduser,然后输入用户名、密码还有邮箱地址就可以了。
现在万事具备了,东风来吧,运行npm publish ,就可以把我们包上传了:
注意publish后面有一个点 ”.“。当我们的包上传完成之后,通过调用来做个验证,换个目录下来执行 npm install cloudwise_else_npmtest:
在我们的目录里查看,就知道创建的模块已经引用过来了:
上面我们介绍Node.js的环境搭建,模块安装、引用,以及如何创建和引用自己的模块,并对每一个点都做了相应的示例。
调试工具
现在我们已经能够让项目run起来了,项目运行过程中如果出现异常怎么办,这是程序猿尤为关心的。在我接触的PHP开发者中,他们比较习惯用日志去跟踪,本人是DotNet的忠实粉丝,江湖流传宇宙最强IDE的Visual Studio给DotNet开发者提供了非常强大的调试功能,可以让我们随心所欲的调试,快速准确的定位到Bug。所以日志调试让我很不能接受,如此火热的Node.js肯定也给开发者提供了不错的debug工具。
首先还是用npm来安装,命令如下:
npm install -g node-inspector
启动的时候我们会在中间加一个debug,命令如下:
node debug .\hello.js
接下来我们启动调试插件 node-inspector.cmd,此处linux跟windows有点差别,linux下没有后面cmd,然后打开浏览器(当前此插件只支持chrome跟Opera),地址如下:http://127.0.0.1:8080/?port=5858
当我们在代码中打断点的时候,程序就会捕获到断点,然后在这里添加监视,也可以单步执行或者逐过程逐语句,爱怎么玩就怎么玩。说到这里我们对Node.js是什么、能做什么、怎么做有了基本的认识,那么是不是就可以开始coding工作了呢?
完全可以的!有人或许会问,Node.js能做的事情php、java、.net也都能做,为什么要选择Node.js呢,难道单单是因为他不用去搭建Apache、IIS、Tomcat吗?接下来我就根据自己的理解和大家探讨下Node.js能够在最近几年被聚光的缘由。
Node.js运行机制解析
当我们搜索Node.js时,夺眶而出的关键字就是 "单线程,异步I/O,事件驱动",应用程序的请求过程可以分为俩个部分:CPU运算和I/O读写,CPU计算速度通常远高于磁盘读写速度,这就导致CPU运算已经完成,但是不得不等待磁盘I/O任务完成之后再继续接下来的业务。
所以I/O才是应用程序的瓶颈所在,在I/O密集型业务中,假设请求需要100ms来完成,其中99ms化在I/O上。如果需要优化应用程序,让他能同时处理更多的请求,我们会采用多线程,同时开启100个、1000个线程来提高我们请求处理,当然这也是一种可观的方案。
但是由于一个CPU核心在一个时刻只能做一件事情,操作系统只能通过将CPU切分为时间片的方法,让线程可以较为均匀的使用CPU资源。但操作系统在内核切换线程的同时也要切换线程的上线文,当线程数量过多时,时间将会被消耗在上下文切换中。所以在大并发时,多线程结构还是无法做到强大的伸缩性。
那么是否可以另辟蹊径呢?!我们先来看看单线程,《深入浅出Node》一书提到 "单线程的最大好处,是不用像多线程编程那样处处在意状态的同步问题,这里没有死锁的存在,也没有线程上下文切换所带来的性能上的开销",那么一个线程一次只能处理一个请求岂不是无稽之谈,先让我们看张图:
Node.js的单线程并不是真正的单线程,只是开启了单个线程进行业务处理(cpu的运算),同时开启了其他线程专门处理I/O。当一个指令到达主线程,主线程发现有I/O之后,直接把这个事件传给I/O线程,不会等待I/O结束后,再去处理下面的业务,而是拿到一个状态后立即往下走,这就是“单线程”、“异步I/O”。
I/O操作完之后呢?Node.js的I/O 处理完之后会有一个回调事件,这个事件会放在一个事件处理队列里头,在进程启动时node会创建一个类似于While(true)的循环,它的每一次轮询都会去查看是否有事件需要处理,是否有事件关联的回调函数需要处理,如果有就处理,然后加入下一个轮询,如果没有就退出进程,这就是所谓的“事件驱动”。
本了解了异步I/O、单线程、事件驱动这几个Node的标签,这里再引入一个观察者的概念,每次轮询都会去向观察者询问是否有事件需要处理,这个过程就如同饭馆的后厨,厨房一轮一轮的制作菜肴,具体做什么菜取决于餐厅里客人的下单,厨房做完成就询问收银小妹接下来做什么菜,而收银小妹就是观察者,他收到的客人点单就是关联的回调函数,如果生意好的饭馆会有多个收银小妹,就如同事件循环中有多个观察者,收到下单就是一个事件,一个观察者里头可能有多个事件。
在node.js中,事件主要来源于网络请求,文件I/O等,根据事件的不同对观察者进行了分类,有文件I/O观察者,网络I/O观察者。事件驱动是一个典型的生产者/消费者模型,请求到达观察者那里,事件循环从观察者进行消费,主线程就可以马不停蹄的只关注业务不用再去进行I/O等待。
那么您可能会问,这种单个线程进行运算,对于多核CPU的服务器岂不是英雄无用武之地,还有就是当主线程业务运算超时,岂不是来不及处理事件队列里(观察者里头)的事件?
对于这俩个问题,首先要做的一点就是在代码编写的时候尽量避免耗时的计算,将大计算进行拆分,这样能够让主线程及时得到释放,处理消费事件队列里头的事件。其次,node.js提供了child_process模块开启子进程,理想状态下每个进程各自利用一个CPU,以此实现多核的利用,child_precess提供创建子进程,以及进程状态监控,进程之间通信的API,感兴趣的小伙伴可以问问度娘,或者欢迎私聊。
聊到这里,我们就可以回顾前面的问题,为什么在有PHP、Java、DotNet的今天我们还会去选择Node.js,因为它的单线程、异步I/O、事件驱动特点能够更好的处理I/O密集型的业务场景,同时它在多核CPU利用上面也做的非常优秀,这就是他存在的理由!
当然,如果你的业务场景几乎没有任何I/O操作,属于纯CPU密集型的业务,那最好还是选择一种多线程语言。
一个Node.js饭店的发展历程
前面的一堆理论似乎不太好明白,最后讲一个关于饭店发展历程的故事作为结尾吧。
第一年
饭店开张,只有一个厨师(同时还兼任老板、服务员、打荷、收银员),当一个客人点餐之后,这个厨师就开始记录(服务员),然后他就开始备菜(打荷)、炒菜(厨师)、然后上菜(服务员)、收钱(收银员),这个时候即使有其他客人来了,等着吧还没忙完呢。这个厨师就这样兢兢业业,有条不紊的干着每一件事,因为每件事都是亲力亲为,都不能出错,虽然所有的事情都了然于心,但效率很低,一天只能卖出十多份饭菜。
这是饭店单线程的第一年:
利:它没有线程上下文交换所带来性能上的开销(因为每件事都是亲力亲为);
弊:无法利用多核CPU(厨房空间那么大,完全可以很多人一起干活),同时错误会引起整个应用退出,应用的健壮性值得考验(当这个厨师生病,或者有事了饭店就得停业)
第二年
这个厨师第一年赚了点钱,回到老家把表哥、表弟全拉过来,现在他们有5个人,可工作方式还是跟厨师第一年的时候一样。当客人来了,就会有一个人去记录,然后自己去厨房洗菜、切菜、炒菜、洗碗、上菜、收钱。当来了第六个客人的时候,就要等待前面的人做完所有事情才能空出一个人来接待。后来他们就想既然客户多了,厨师就得多,再回老家多叫几个兄弟吧。这时新的问题发生了,当每个厨师做完饭后就出去找客人,客人说我刚刚点餐了,然后厨师就去厨房问,刚刚是哪个表兄接待的那个客户,要是没有人接待的话,我来处理。就这样忙忙碌碌一年,他们比去年多做了好多生意,但是感觉每天客户多的时候,厨房里头乱糟糟的,总要询问这个询问那个。
这就是饭店多线程的一年:
利:一个线程服务一个请求,线程之间可以共享数据,这样可以避免内存的浪费,可以同时处理多个请求;
弊:操作系统内核在切换线程的同时也要切换线程的上下文,当线程数量过多时,时间将会被消耗在上下文切换上(厨房里头乱糟糟的,总要询问这个询问那个)。
第三年
老板娘过来了(Node.Js闪亮登场),她发现这帮厨师都在各自为战,自己拿到客户的点餐后去洗菜、切菜、洗碗、炒菜、上菜、收钱,一个人只能同时处理一个任务,而且作为厨师没必要去做洗菜、切菜、洗碗、收银之类费时的工作。
所以老板娘把所有人进行了分工:
老板作为厨师长(Node里头的主线程),他不再去洗碗、洗菜、切菜、炒菜、收银(我们可以把洗碗、洗菜、炒菜、切菜认为是比较耗时的I/O),他只负责将收银台小妹(观察者)拿过来的菜单分配给不同的厨师,打荷(这些人就是不同的I/O线程),吩咐下去之后他不会等菜出来再走(进入下一个轮询),又问收银台小妹还有没有菜单要做,如果有继续轮询,如果没有了休息(退出进程)。当菜做出来之后放在上菜区(回调),收银台就显示菜出来了(将回调放入事件队列),当老板查询收银员(观察者)的时候,收银员就告诉厨师长,厨师长就通知服务员(处理不同I/O的线程)上菜(完成回调),这样饭店有条不紊的运行下去,客人也越来越多了。
有些高端客户不想在这挤,所以老板娘就想,饭店房子那么大(多核CPU),可以叫她弟弟(子进程的主线程)在这开个子店(child_process),然后发现一个收银员不够用,那就多招几个收银员(多个观察者),就这样每个分店(进程)只有一个主管(主线程),主管的弱点就是无暇顾及洗碗等杂活(I/O),只能关注业务,至于饭店能开多大(应用程序能处理多大请求),要看主管的处理能力(主线程的编程强壮度)。最后,大饭店起了个时髦的名字叫Hotel NodeJs!
希望这个小故事能够给大家对Node.Js运行的理解带来一点帮助,谢谢。