写作不易,未经作者允许禁止以任何形式转载!
Node作者Ryan Dahl: 基于V8创建一个轻量级的高性能Web服务器并提供一套库
Ryan Dahl是一名资深的C/C++程序员,创造出Node之前主要工作是围绕Web高性能服务器进行的 他发现Web高性能服务器的两个要点:
Ryan Dahl也曾评估过使用C、Lua、Haskell、Ruby等语言作为备选实现,得出以下结论:
JavaScript的优势:
img
除了HTML、Webkit和显卡这些UI相关技术没有支持外,Node的结构与Chrome十分相似。他们都是基于事件驱动的异步架构:
在Node中,JavaScript还被赋予了新的能力:
Node使JavaScript可以运行在不同的地方,不再限制在浏览器中、DOM树打交道。如果HTTP协议是水平面,Node就是浏览器在协议栈另一边的倒影。 Node不处理UI,但用与浏览器相同的机制和原理运行,打破了JavaScript只能在浏览器中运行的局面。前后端编程环境统一,可以大大降低前后端转换所需要的上下文代价。
img
var fs = require('fs');
fs.readFile('/path', function (err, file) {
console.log('读取文件完成')
});
console.log('发起读取文件');
熟悉的用户必知道,“读取文件完成”是在“发起读取文件”之后输出的 fs.readFile后的代码是被立即执行的,而“读取文件完成”的执行时间是不被预期的 只知道它将在这个异步操作后执行,但并不知道具体的时间点 异步调用中对于结果值的捕获是符合“Don't call me, I will call you”原则的 这也是注重结果,不关心过程的一种表现
Node中,绝大多数操作都以异步的方式进行调用,Ryan Dahl排除万难,在底层构建了很多异步I / O的API,从文件读取到网络请求等。使开发者很已从语言层面很自然地进行并行I / O操作,在每个调用之间无需等待之前的I / O调用结束,在编程模型上可以极大提升效率
「注:异步I / O机制将在下文中详细阐述」
「事件」
随着Web2.0的到来,JavaScript在前端担任了更多的职责,时间也得到了广泛的应用。将前端浏览器中广泛应用且成熟的事件与回到函数引入后端,配合异步I / O ,可以很好地将事件发生的时间点暴露给业务逻辑。
对于服务器绑定了request事件
对于请求对象,绑定了data和end事件
var http = require('http');
var querystring = require('querystring');
// 侦听服务器的request事件
http.createServer(function (req, res) {
var postData = '';
req.setEncoding('utf8');
// 侦听请求的data事件
req.on('data', function (trunk) {
postData += trunk;
});
// 侦听请求的end事件
req.on('end', function () {
res.end(postData);
});
}).listen(8080);
console.log('服务器启动完成');
发出请求后,只需关心请求成功时执行相应的业务逻辑即可
request({
url: '/url',
method: 'POST',
data: {},
success: function (data) {
// success事件
}
});
事件的编程方式具有轻量级、松耦合、只关注事务点等优势,但是在多个异步任务的场景下,事件与事件之间各自独立,如何协作是一个问题,后续也出现了一系列异步编程解决方案:
「回调函数」
Node保持了JavaScript在浏览器中单线程的特点 JavaScript与其他线程是无法共享任何状态的,最大的好处是不用像多线程编程那样处处在意状态的同步问题,这里没有死锁的存在,也没有线程上下文交换所带来的性能上的开销
起初Node只能在Linux平台上运行,如果想在Windows平台上学习和使用Node,则必须通过Cygwin / MinGW,后微软投入通过基于libuv实现跨平台架构
在操作系统与Node上层模块系统之间构建了一层平台架构
img
通过良好的架构,Node的第三方C++模块也可以借助libuv实现跨平台
背景: 在其他高级语言中,Java有类文件,Python有import机制,Ruby有require,PHP有include和require。而JavaScript通过script标签引入代码的方式显得杂乱无章,。人们不得不用命名空间等方式人为地约束代码,以达到安全和易用的目的。 直到后来出现了CommonJS...
希望JavaScript能在任何地方运行
对于JavaScript自身而言,它的规范依然是薄弱的,还有以下缺陷:
CommonJS的提出,主要是为了弥补当前JavaScript没有标准的缺陷,以达到像Python、Ruby和Java具备开发大型应用的基础能力,而不是停留在小脚本程序的阶段,希望可以利用JavaScript开发:
img
CommonJS规范涵盖了:
Node与浏览器以及W3C组织、CommonJS组织、ECMAScript之间的关系,共同构成了一个繁荣的生态系统
上下文提供了exports对象用于导出当前模块的方法或者变量,并且它是导出的唯一出口 在模块中,还存在一个module对象,它代表模块自身,而exports是module的属性 在Node中,一个文件就是一个模块,将方法挂载在exports对象上作为属性即可定义导出的方式
// math.js
exports.add = function(a, b){
return a + b;
}
const math = require('./math');
const res = math.add(1, 1);
console.log(res);
// 2
在CommonJS规范中,存在require方法,这个方法接受模块标识,以此引入一个模块的API到当前上下文中
模块标识就是传递给require方法的参数,可以是:
img
模块的定义十分简单,接口也十分简洁
每个模块具有独立的空间,它们互不干扰,在引用时也显得干净利落
将类聚的方法和变量等限定在私有的作用域中,同时支持引入和导出功能以顺畅地连接上下游依赖
在Node引入模块,需要经历以下三个步骤
Node中模块分为两类:
编译过程中,编译进了二进制执行文件 在Node进程启动时,部分核心模块就直接被加载进内存中,所以这部分核心模块引入时,文件定位和编译执行这两个步骤可以省略,并且在路径分析中优先判断,所以它的加载速度是最快的。
运行时动态加载,需要完整的路径分析、文件定位、编译执行过程,速度比核心模块慢
与浏览器会缓存静态脚本文件以提高性能一样,Node对引入过的模块都会进行二次缓存,以减少二次引入时的开销。不同点在于:
无论核心模块还是文件模块,require方法对相同模块的二次加载都一律采用缓存优先的方式
「标识符分析(路径)」
前面说到过,require方法接受一个参数作为标识符,分为以下几类:
优先级仅次于缓存加载,在Node的源代码编译过程中已编译为二进制代码,加载过程最快 「注:加载一个与核心模块标识符相同的自定义模块是不会成功的,只能通过选择不同的标识符 / 换用路径的方式实现」
以 ./ 、../ 开头的标识符都被当做文件模块处理 require方法会将路径转为真实路径,并以真实路径为索引,将编译执行后的结果存放到缓存中,以使二次加载更快 文件模块给Node指明了确切的文件位置,所以在查找过程中可以节约大量时间,其加载速度仅慢于核心模块
是一种特殊的文件模块,是一个文件或者包的形式 这类模块的查找是最费时的,也是最慢的一种
先介绍一下模块路径这个概念,也是定位文件模块时制定的查找策略,具体表现为一个路径组成的数组
console.log(module.path)
['/home/bytedance/reasearch/node_modules',
'/home/bytedance/node_modules',
'home/node_module', /node_modules']
可以看出规则如下:
它的生成方式与JavaScript原型链 / 作用域链的查找方式十分类似 在加载过程中,Node会逐个尝试模块路径中的路径,直到找到目标文件 文件路径越深,模块查找耗时会越多,这是自定义模块的加载速度最慢的原因
「文件定位」
require分析标识符会出现不包含文件扩展名的情况 会按.js、.json、.node的次序补足扩展名,一次尝试 过程中,需调用fs模块同步阻塞地判断文件是否存在,Node单线程因此会引起性能问题 如果是.node / .json文件带上扩展名能加快点速度,配合缓存机制,可大幅缓解Node单线程阻塞调用的缺陷
分析标识符的过程中可能没有找到文件,却得到一个目录,则会将目录当做一个包来处理 通过解析package.json文件对应该包的main属性指定的文件名 如果main相应文件解析错误 / 没有package.json文件,node会将index作为文件名 一次查找index.js index.json index.node 该目录没有定位成功则进行下一个模块路径进行查找 直到模块路径数组都被遍历完依然没有查找到目标文件则抛出异常
在Node中,每个文件模块都是一个对象
function Module(id, parent) {
this.id = id;
this.exports = {};
this.parent = parent;
if (parent && parent.children) {
parent.children.push(this);
}
this.filename = null;
this.loaded = false;
this.children = [];
}
每一个编译成功的模块都会将其文件路径作为索引存在Module.cache对象上,以提高二次引入的性能
Node组织了自身核心模块,也使得第三方文件模块可以有序地编写和使用 但是在第三方模块中,模块与模块之间仍然是散列在各地的,相互之间不能直接引用 而在模块之外,包和 NPM 则是将模块联系起来的一种机制 一定程度上解决了变量依赖、依赖关系等代码组织性问题
img
包实际上是一个存档文件,即一个目录直接打包为一个.zip/tar.gz格式的文件,安装后解压还原为目录
package.json
CommonJS为package.json定义了如下一些必须的字段
包规范的定义可以帮助Node解决依赖包安装的问题,而NPM正是基于该规范进行了实现
CommonJS包规范是理论,NPM是其中一种实践 NPM于Node,相当于gem于Ruby,pear于PHP 帮助完成了第三方模块的发布、安装和依赖等
npm -v
npm
npm install {packageName}
执行该命令后,NPM会在当前目录下创建node_modules目录下创建包目录,接着将相应的包解压到这个目录下
npm install {packageName} -g
全局模式并不是将一个模块包安装为一个全局包的意思,它并不意味着可以从任何地方reuqire它 全局模式这个称谓并不精确,-g 实际上是将一个包安装为全局可用的执行命令 它根据包描述文件中的bin字段配置,将实际脚本链接到与Node可执行文件相同的路径下
对于一些没有发布到NPM上的包,或者因为网络原因无法直接安装的包 可以通过将包下载到本地,然后本地安装
npm install <tarball file>
npm install <tarball url>
npm install folder>
如果不能通过官方源安装,可以通过镜像源安装
npm install --registry={urlResource}
如果使用过程中几乎全使用镜像源,可以指定默认源
npm config set registry {urlResource}
package.json中scripts字段的提出就是让包在安装或者卸载等过程中提供钩子机制
"scripts":{
"preinstall": "preinstall.js",
"install": "install.js",
"uninstall": "uninstall.js",
"test": "test.js",
}
npm install <package>
时,preinstall指向的脚本会被加载执行,然后install指向的脚本会被执行npm uninstall <package>
时,uninstall指向的脚本也许会做一些清理工作npm test
将会运行test指向的脚本,一个优秀的包应当包含测试用例,并在package.json文件正配置好运行测试的命令,方便用户运行测试用例,以便检验包是否稳定可靠企业的限制在于,一方面需要享受到模块开发带来的低耦合和项目组织上的好处,另一方面却要考虑模块保密性的问题。所以,通过NPM共享和发布存在潜在的风险。
为了同时能够享受到NPM上众多的包,同时对自己的包进行保密和限制,现有的解决方案就是企业搭建自己的NPM仓库,NPM无论是它的服务端和客户端都是开源的。
img
局域NPM仓库的搭建方法与搭建镜像站的方式几乎一样,与镜像仓库不同的地方在于可以选择不同步官方源仓库中的包
浏览器中JavaScript在单线程上执行,还和UI渲染共用一个线程 《高性能JavaScript》曾总结过,如果脚本执行的时间超过100ms用户就会感到页面卡顿 如果网页临时需要获取一个网络资源,通过同步的方式获取,JS需要等资源完全从服务器获取后才能继续执行,这期间UI将停顿,不响应用户的交互行为。可以想象,这样的用户体验将会多差。 而采用异步请求,JavaScript和UI的执行都不会处于等待状态,给用户一个鲜活的页面 I / O是昂贵的,分布式I / O 是更昂贵的 只有后端能够快速响应资源,才能让前端体验变好
计算机在发展过程中将组件进行了抽象,分为了I / O设备和计算设备 假设业务场景有一组互不相关的任务需要完成,主流方法有两种:
多线程的代价在于创建线程和执行线程上下文切换的开销较大。
在复杂的业务中经常面临锁、状态同步等问题。但是多线程在多核CPU上能够有效提升CPU利用率
单线程顺序执行任务比较符合编程人员按顺序思考的思维方式,依然是主流的编程方式
串行执行的缺点在于性能,任意一个略慢的任务都会导致后续执行代码被阻塞
在计算机资源中,通常I / O与CPU计算是可以并行的,同步编程模型导致的问题是,I / O的进行会让后续任务等待,这造成资源不能更好地被利用
利用单线程,远离多线程死锁、状态同步等问题; 利用异步I / O,让单线程可以远离阻塞,更好地使用CPU 为了弥补单线程无法利用多核CPU的缺点,Node提供了类似前端浏览器中Web Workers的子进程,该子进程可以通过工作进程高效地利用CPU和I / O 异步I / O的提出是期望I / O的调用不再阻塞后续运算,将原有等待I / O完成的这段时间分配给其余需要的业务去执行
img
操作系统内核对于I / O方式只有两种:阻塞与非阻塞 在调用阻塞I / O时,应用程序需要等待I / O完成才返回结果 特点:调用之后一定要等到系统内核层面完成所有操作后调用才结束 例子:系统内核在完成磁盘寻道、读取数据、复制数据到内幕才能中之后,这个调用才结束》
img
非阻塞I / O与阻塞I / O的差别为调用之后会立即返回 非阻塞I / O返回之后,CPU的时间片可以用来处理其他事务,此时的性能提升是明显的 存在的问题:
img
它是最原始、性能最低的一种,通过重复调用检查I / O的状态来完成数据的完整读取 在得到最终数据前,CPU一直耗用在等待上
img
它是在read的基础上改进的一种方案,通过对文件描述符上的事件状态来进行判断 限制:它采用一个1024长度的数组来存储状态,最多可以同时检查1024个文件描述符
img
较select有所改进,采用链表的方式避免数组长度的限制,其次它能避免不需要的检查 文件描述符较多时,它的性能还是十分低下的
img
该方案是Linux下效率最高的I / O事件通知机制,在进入轮询的时候如果没有检查到I / O事件,将会进行休眠,直到事件将它唤醒。它是真实利用了事件通知、执行回调的方式,而不是遍历查询,所以不会浪费CPU,执行效率较高
img
尽管epoll已经利用了时间来降低CPU的耗用,但是休眠期间CPU几乎是限制的,对于当前线程而言利用率不够
完美的异步I / O应该是应用程序发起非阻塞调用,无需通过遍历或者时间唤醒等方式轮询
可以直接处理下一个任务,只需在I / O完成后通过信号或回调将数据传递给应用程序即可
img
Linux下存在原生提供的一种异步I / O方式(AIO)就是通过信号或者回调来传递数据的
缺点:
注:关于O_DIRECT
通过让部分线程进行阻塞I / O或者非阻塞I / O加轮询技术来完成数据获取,让一个线程进行计算处理,通过线程之间的通信将I / O得到的数据进行传递,这就轻松实现了异步I / O(尽管它是模拟的
img
img
Node完成整个异步I / O环节的有事件循环、观察者和请求对象等
着重强调一下Node自身的执行模型——事件循环
Node进程启动时,会创建一个类似while(true)的循环
每次循环体的过程称之为Tick,每个Tick的过程就是查看是否有事件待处理
如果有就取出事件及其相关的回调函数,并执行它们
img
每个事件循环中有一个或多个观察者,而判断是否有事件要处理的过程就是向这些观察者询问是否有要处理的事件
事件循环、观察者、请求对象、I / O线程池这四者共同构成了NOde异步I / O模型的基本要素
由于我们知道JavaScipt是单线程的,所以按尝试很容易理解它不能充分利用多核CPU
事实上在Node中,除了JavaScript是单线程外,Node自身其实是多喜爱昵称的,只是I / O线程使用的CPU较少
另一个需要注意的点是,除了用户代码无法并行执行以外,所有的I / O是可以并行执行的
注:图为Node整个异步I / O过程
img
前面对异步的讲解,也基本勾勒出了事件驱动的实质,即通过主循环加事件触发的方式来运行程序
下面为几种经典的服务器模型:
事件驱动带来的高效已经渐渐开始为业界所重视 知名服务器Nginx也摒弃了多线程的方式,采用和Node相同的事件驱动 不同之处在于Nginx采用纯C写成,性能较高,但是它仅适合于做Web服务器,用于反向代理或者负载均衡服务,在业务处理方面较为欠缺 Node则是一套高性能平台,可以利用它构建与Nginx相同的功能,也可以处理各种具体业务 Node没有Nginx在Web服务器方面那么专业,但场景更大,自身性能也不错 在实际项目中可以结合它们各自的优点以达到应用的最优性能 JavaScript在服务器端近乎空白,使得Node没有任何历史包袱,而Node在性能优化上的表现使得它一下子就在社区中流行了起来~
本文介绍了Node被创造的目的、语言选型、特点、模块机制、包管理机制以及异步I / O等相关知识,希望能给你带来对Node有一个新的认识。最近一直也在计划学习Node和服务端相关知识,感兴趣的同学可以一起学习和交流~