Skip to content

Node特点

Node 的特点

作为后端JavaScript的运行平台,Node保留了前端浏览器JavaScript中那些熟悉的接口,没有改写语言本身的任何特性,依旧基于作用域和原型链,区别在于它将前端中广泛运用的思想迁移到了服务器端。

异步I/O

关于异步I/O,向前端工程师解释起来或许会容易一些,因为发起Ajax调用对于前端工程师而言是再熟悉不过的场景了。下面的代码用于发起一个Ajax请求:

javascript
$.post('/url', {title: 'Node.js'}, function (data) { 
 console.log('收到响应'); 
}); 

console.log('发送Ajax结束');

熟悉异步的用户必然知道,“收到响应”是在“发送Ajax结束”之后输出的。在调用$.post()后,后续代码是被立即执行的,而“收到响应”的执行时间是不被预期的。我们只知道它将在这个异步请求结束后执行,但并不知道具体的时间点。

异步调用中对于结果值的捕获是符合“Don’t call me, I will call you”的原则的,这也是注重结果,不关心过程的一种表现。

在Node中,异步I/O也很常见。以读取文件为例,我们可以看到它与前端Ajax调用的方式是极其类似的:

javascript
const fs = require('fs'); 
fs.readFile('/path', function (err, file) { 
 console.log('读取文件完成') 
}); 
console.log('发起读取文件');

这里的“发起读取文件”是在“读取文件完成”之前输出的。同样,“读取文件完成”的执行也取决于读取文件的异步调用何时结束。

事件与回调函数

随着Web 2.0时代的到来,JavaScript在前端担任了更多的职责,事件也得到了广泛的应用。Node不像Rhino那样受Java的影响很大,而是将前端浏览器中应用广泛且成熟的事件引入后端,配合异步I/O,将事件点暴露给业务逻辑。

下面的例子展示的是Ajax异步提交的服务器端处理过程。Node创建一个Web服务器,并侦听8080端口。对于服务器,我们为其绑定了request事件,对于请求对象,我们为其绑定了data事件和end事件:

javascript
const http = require('http'); 
const querystring = require('querystring'); 
// 侦听服务器的request事件
http.createServer(function (req, res) { 
 let postData = ''; 
 req.setEncoding('utf8'); 
 // 侦听请求的data事件
 req.on('data', function (trunk) { 
 postData += trunk; 
 }); 
 // 侦听请求的end事件
 req.on('end', function () { 
 res.end(postData); 
 }); 
}).listen(8080); 
console.log('服务器启动完成');

相应地,我们在前端为Ajax请求绑定了success事件,在发出请求后,只需关心请求成功时执行相应的业务逻辑即可,相关代码如下:

javascript
$.ajax({ 
 'url': '/url', 
 'method': 'POST', 
 'data': {}, 
 'success': function (data) { 
 // success事件
 } 
});

事件的编程方式具有轻量级、松耦合、只关注事务点等优势,但是在多个异步任务的场景下,事件与事件之间各自独立,如何协作是一个问题。从前面可以看到,回调函数无处不在。这是因为在JavaScript中,我们将函数作为第一等公民来对待,可以将函数作为对象传递给方法作为实参进行调用。

与其他的Web后端编程语言相比,Node除了异步和事件外,回调函数是一大特色。纵观下来,回调函数也是最好的接受异步调用返回数据的方式。但是这种编程方式对于很多习惯同步思路编程的人来说,也许是十分不习惯的。代码的编写顺序与执行顺序并无关系,这对他们可能造成阅读上的障碍。在流程控制方面,因为穿插了异步方法和回调函数,与常规的同步方式相比,变得不那么一目了然了。

在转变为异步编程思维后,通过对业务的划分和对事件的提炼,在流程控制方面处理业务的复杂度与同步方式实际上是一致的。

单线程

Node保持了JavaScript在浏览器中单线程的特点。而且在Node中,JavaScript与其余线程是无法共享任何状态的。单线程的最大好处是不用像多线程编程那样处处在意状态的同步问题,这里没有死锁的存在,也没有线程上下文交换所带来的性能上的开销。

同样,单线程也有它自身的弱点,这些弱点是学习Node的过程中必须要面对的。积极面对这些弱点,可以享受到Node带来的好处,也能避免潜在的问题,使其得以高效利用。单线程的弱点具体有以下3方面。

  • 无法利用多核CPU。
  • 错误会引起整个应用退出,应用的健壮性值得考验。
  • 大量计算占用CPU导致无法继续调用异步I/O。

像浏览器中JavaScript与UI共用一个线程一样,JavaScript长时间执行会导致UI的渲染和响应被中断。在Node中,长时间的CPU占用也会导致后续的异步I/O发不出调用,已完成的异步I/O的回调函数也会得不到及时执行。

最早解决这种大计算量问题的方案是Google公司开发的Gears。它启用一个完全独立的进程,将需要计算的程序发送给这个进程,在结果得出后,通过事件将结果传递回来。这个模型将计算量分发到其他进程上,以此来降低运算造成阻塞的几率。后来,HTML5定制了Web Workers的标准,Google放弃了Gears,全力支持Web Workers。Web Workers能够创建工作线程来进行计算,以解决JavaScript大计算阻塞UI渲染的问题。工作线程为了不阻塞主线程,通过消息传递的方式来传递运行结果,这也使得工作线程不能访问到主线程中的UI。

Node采用了与Web Workers相同的思路来解决单线程中大计算量的问题:child_process。

子进程的出现,意味着Node可以从容地应对单线程在健壮性和无法利用多核CPU方面的问题。通过将计算分发到各个子进程,可以将大量计算分解掉,然后再通过进程之间的事件消息来传递结果,这可以很好地保持应用模型的简单和低依赖。通过Master-Worker的管理方式,也可以很好地管理各个工作进程,以达到更高的健壮性。

跨平台

起初,Node只可以在Linux平台上运行。如果想在Windows平台上学习和使用Node,则必须通过Cygwin或者MinGW。随着Node的发展,微软注意到了它的存在,并投入了一个团队帮助Node实现Windows平台的兼容,在v0.6.0版本发布时,Node已经能够直接在Windows平台上运行了。

兼容Windows和*nix平台主要得益于Node在架构层面的改动,它在操作系统与Node上层模块系统之间构建了一层平台层架构,即libuv。

目前,libuv已经成为许多系统实现跨平台的基础组件。通过良好的架构,Node的第三方C++模块也可以借助libuv实现跨平台。目前,除了没有保持更新的C++模块外,大部分C++模块都能实现跨平台的兼容。

Node.js的弊端

单线程带来的弊端

Node.js中有一个特点就是单线程,它带来了很多好处,但是它也有弊端,单线程弱点如下。

  1. 无法利用多核CPU
  2. 错误会引起整个应用退出无法继续调用异步I/O
  3. 大量计算占用CPU导致无法继续调用异步I/O

以上确实是Node的弊端,但是都会有一些对应的解决方案:

弊端1:解决方案

  • (1)一些管理工具比如pm2,forever 等都可以实现创建多进程解决多核 CUP 的利用率问题。
  • (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)可以把大量的密集计算像上面一样拆分成多个子线程计算
  • 但是如果不允许拆分,想计算100万的大数据,在一个单线程中,Node确实显得无能为力,这本身就是V8内存限制的弊端。

说明:child_process与cluster模块我会单独拿一篇文章来讲。 值得开心的是上面这些弊端随着Node的版本更新,和新的api模块出现,好像解决了这些弊端。

Node 的应用场景

I/O密集型

在Node的推广过程中,无数次有人问起Node的应用场景是什么。如果将所有的脚本语言拿到一处来评判,那么从单线程的角度来说,Node处理I/O的能力是值得竖起拇指称赞的。通常,说Node擅长I/O密集型的应用场景基本上是没人反对的。Node面向网络且擅长并行I/O,能够有效地组织起更多的硬件资源,从而提供更多好的服务。

I/O密集的优势主要在于Node利用事件循环的处理能力,而不是启动每一个线程为每一个请求服务,资源占用极少。

是否不擅长CPU密集型业务

CPU密集型应用给Node带来的挑战主要是:

由于JavaScript单线程的原因,如果有长时间运行的计算(比如大循环),将会导致CPU时间片不能释放,使得后续I/O无法发起。但是适当调整和分解大型运算任务为多个小任务,使得运算能够适时释放,不阻塞I/O调用的发起,这样既可同时享受到并行异步I/O的好处,又能充分利用CPU。

关于CPU密集型应用,Node的异步I/O已经解决了在单线程上CPU与I/O之间阻塞无法重叠利用的问题,I/O阻塞造成的性能浪费远比CPU的影响小。对于长时间运行的计算,如果它的耗时超过普通阻塞I/O的耗时,那么应用场景就需要重新评估,因为这类计算比阻塞I/O还影响效率,甚至说就是一个纯计算的场景,根本没有I/O。此类应用场景或许应当采用多线程的方式进行计算。Node虽然没有提供多线程用于计算支持,但是还是有以下两个方式来充分利用CPU。

  • Node可以通过编写C/C++扩展的方式更高效地利用CPU,将一些V8不能做到性能极致的地方通过C/C++来实现。通过C/C++扩展的方式实现斐波那契数列计算,速度比Java还快。
  • 如果单线程的Node不能满足需求,甚至用了C/C++扩展后还觉得不够,那么通过子进程的方式,将一部分Node进程当做常驻服务进程用于计算,然后利用进程间的消息来传递结果,将计算与I/O分离,这样还能充分利用多CPU。

CPU密集不可怕,如何合理调度是诀窍。

分布式应用

阿里巴巴的数据平台对Node的分布式应用算是一个典型的例子。分布式应用意味着对可伸缩性的要求非常高。数据平台通常要在一个数据库集群中去寻找需要的数据。阿里巴巴开发了中间层应用NodeFox、ITier,将数据库集群做了划分和映射,查询调用依旧是针对单张表进行SQL查询,中间层分解查询SQL,并行地去多台数据库中获取数据并合并。NodeFox能实现对多台MySQL数据库的查询,如同查询一台MySQL一样,而ITier更强大,查询多个数据库如同查询单个数据库一样,这里的多个数据库是指不同的数据库,如MySQL或其他的数据库。

这个案例其实也是高效利用并行I/O的例子。Node高效利用并行I/O的过程,也是高效使用数据库的过程。对于Node,这个行为只是一次普通的I/O。对于数据库而言,却是一次复杂的计算,所以也是进而充分压榨硬件资源的过程。