为什么要使用Node.js?
在众多可用的Web应用程序开发平台中,有许多堆栈可供选择,为什么要选择Node.js,Node.js为什么能够脱颖而出?我们将在以下几节中获得这个问题的答案。
流行
Node.js正在迅速成为一个流行的开发平台,并被许多大小玩家采用。其中一个玩家是PayPal,他们正在使用Node.js编写的系统取代现有的基于Java的系统。其他大型Node.js采用者包括沃尔玛的在线电子商务平台以及LinkedIn和eBay。
有关PayPal的博客文章,访问:https://www.paypal-engineerin... ypal/。
据NodeSource统计,Node.js的用户正在迅速增长(有关更多信息,请访问https://nodesource.com/node-b...)。这种增长的证据包括增加下载Node.js版本的带宽,新增的与Node.js相关的GitHub项目等等。
开发者虽然对JavaScript的兴趣非常浓厚,但多年来JavaScript一直处于停滞状态,从搜索量(GoogleInsights)及其作为编程技能的使用(Dice Skills Center)来衡量。对Node.js的兴趣一直在快速增长,但有迹象表明正在趋于稳定。
有关这方面的更多信息,请参阅https://itnext.io/choosing-ty... 或者http:// bit.ly/2q5cu0w。
最好不要随大流,因为有不同的人群,每个人都声称他们的软件平台很酷。Node.js虽然能做一些很酷的事情,但更重要的是平台的技术优势
无处不在的JavaScript
在服务器和客户机上使用相同的编程语言一直是Web届的一个长期梦想。这个梦想可以追溯到Java的早期,浏览器中的Java小程序是用Java编写的前端服务器应用程序,JavaScript最初被设想为和这些小程序类似的轻量级脚本语言。Java从来没有实现过它作为客户端编程语言的宣言,甚至“Java小程序”这个短语也正在被逐渐抛弃,成为客户端应用程序模型中的模糊记忆。我们最终以JavaScript作为浏览器客户端语言,而不是Java。通常,前端JavaScript开发者与服务器端团队所处的语言环境不同,后者可能使用PHP、Java、Ruby或Python进行开发。
随着时间的推移,浏览器中的JavaScript引擎变得异常强大,让我们能够编写更加复杂的浏览器端应用程序。有了Node.js,我们终于可以通过在Web浏览器和服务器上使用相同的编程语言——JavaScript实现Web应用程序的开发。
前端和后端使用统一语言具有以下几个潜在好处:
- 一个开发团队可以同时负责前端和后端开发工作。
- 可以更容易地在服务器和客户端之间迁移代码。
- 服务器和客户端之间的通用数据格式(JSON)。
- 服务器和客户端都有通用的软件工具。
- 用于服务器和客户端的通用测试或质量报告工具。
在编写Web应用程序时,视图模板可以在两端使用。
JavaScript语言非常流行,因为它在Web浏览器中无处不在。与其他语言相比,JavaScript是一种现代的高级的编程语言。由于它的流行,有大量经验丰富的JavaScript程序员人才。利用谷歌对V8的支持
为了使Chrome成为一款流行且优秀的Web浏览器,谷歌将V8引擎投资打造成一款超高速的JavaScript引擎。因此,谷歌有巨大的动力继续改进V8。V8虽然是Chrome的JavaScript引擎,但也可以独立运行。
Node.js构建在V8 JavaScript引擎之上,能充分利用V8的所有特性。因此,Node.js能够快速实现JavaScript的新特性,因为这些新特性是由V8实现的,Node.js能够基于同样的原因获得同样的性能优势。更精简、异步、事件驱动的模型
据称,Node.js体系结构构建在单个执行线程上,具有巧妙的面向事件的异步编程模型和快速的JavaScript引擎,比基于线程的体系结构能够节省大量的CPU资源和内存资源。其他使用线程进行并发的系统往往具有Node.js所没有的内存开销和复杂性。我们将在本章后面的部分进一步讨论这个问题。
微服务体系架构
微服务体系架构是软件开发中的一种新的架构概念。微服务专注于将大型Web应用程序拆分为可以由小型团队轻松开发的小型的、紧密关注的服务。虽然它们并不完全是一个新概念,但更多的是对旧的客户机-服务器计算模型的重构,微服务模式非常适合敏捷项目管理技术,并为我们提供了更精细的应用程序部署。Node.js是实现微服务的优秀平台。我们稍后再谈。
Node.js在经历重大分裂和敌对后变得更加强大
2014年和2015年期间,Node.js社区在政策、方向和控制方面面临重大分歧。io.js项目是一个敌对的分支,由一个想要整合几个特性并改变决策过程的团队驱动。最终的结果是Node.js和IO.JS知识库的合并,一个独立的Node.js基金会来运行这个项目,社区共同努力向前迈向共同的方向。
愈合这一裂痕的一个具体结果是迅速采用了新的ECMAScript语言特性。V8引擎正在迅速采用这些新功能,以推进Web开发的状态。反过来,Node.js团队正以V8中所展示的速度采用这些功能,这意味着Promise和异步函数正迅速成为Node.js程序员的现实。
归根结底,Node.js社区不仅在io.js分裂和后来的ayo.js分裂之后幸存了下来,而且社区和平台也因此变得更加强大。
在本节中,你了解了使用Node.js的几个原因。Node.js不仅是一个受欢迎的平台,背后有强大的社区支持,而且使用Node.js还有严重的技术原因。它的架构有一些关键的技术优势,所以让我们深入研究一下。Node.js事件驱动体系架构
Node.js惊人的性能据说是因为它的异步事件驱动体系架枸和V8 JavaScript引擎的使用。这使Node.js能够同时处理多个任务,例如在来自多个Web浏览器的请求之间切换。Node.js的原始创建者Ryan Dahl遵循以下要点:
- 与依赖线程处理多个并发任务的应用程序服务器相比,单线程、事件驱动的编程模型更易于开发,复杂性和资源开销更低。
- 通过将阻塞函数调用转换为异步代码执行,您可以配置系统,以便在满足阻塞请求时发出事件。
- 你可以从Chrome浏览器中利用V8 JavaScript引擎,所有工作都将用于改进V8;因此,进入V8的所有性能增强都有利于Node.js。
在大多数应用服务器中,并发或处理多个并发请求是通过多线程体系架构实现的。在这样的系统中,任何数据请求或任何其他阻塞函数调用都会导致当前执行线程挂起并等待结果。处理并发请求需要有多个执行线程。当一个线程挂起时,另一个线程可以执行。当应用服务器启动和停止线程来处理请求时,这会导致混乱。每个挂起线程(通常等待输入/输出操作完成)都会消耗一个完整的调用堆栈内存,从而增加开销。线程增加了应用服务器的复杂性和服务器开销。
为了帮助我们理解为什么会这样,Node.js的创建者Ryan Dahl提供了以下示例。在2010年5月的Cinco de NodeJS演示中
(https://www.youtube.com/watch...)Dahl问我们执行一行代码时会发生什么情况,如:
1 query('SELECT * from db.table', function (err, result) {
2 if (err) throw err; // handle errors operate on result
3 });
当然,当数据库层向数据库发送查询并等待结果或错误时,程序会在此时暂停。这是一个阻塞函数调用的示例。根据查询的不同,此暂停可能相当长(好吧,几毫秒,这在计算机时间中是很长的)。此暂停是错误的,因为执行线程在等待结果到达时无法执行任何操作。如果你的软件在运行单线程平台上,则整个服务器将被阻塞且无法响应。如果你的应用程序运行在基于线程的服务器平台上,则需要一个线程上 下文开关来响应到达的所有其他请求。到达服务器的未完的成连接数量越多,线程上下文切换的数量就越多。上下文切换不是免费的,因为每个线程状态需要更多的内存,CPU在线程管理开销上将花费更多的时间。
Node.js最初开发的关键灵感是单线程系统的简单性。单执行线程意味着服务器没有多线程系统的复杂性。这个选择意味着Node.js需要一个事件驱动模型来处理并发任务。代码不再等待阻塞请求的结果(例如从数据库检索数据),而是将事件分派给事件处理程序。
使用线程实现并发通常会存在很多问题,例如代价高昂且容易出错,Java的同步原语易出错,或者设计并发软件可能非常复杂且容易出错。复杂性来自对共享变量的访问以及避免线程之间死锁和竞争的各种策略。Java的同步原语就是这种策略的一个例子,显然许多程序员发现这种方式很难使用。有一种倾向是创建诸如java.util.concurrent之类的框架来减小线程并发的复杂性,但有些人认为,掩盖复杂性只会使问题变得更复杂。
Java程序员可能会在这一点上提出异议。也许他们的应用程序代码是针对Spring这样的框架编写的,或者他们直接使用JavaEE。在这两种情况下,他们的应用程序代码都不使用并发特性或处理线程,因此我们刚才描述的复杂性在哪里?仅仅因为复杂性隐藏在Spring和JavaEE中并不意味着没有复杂性和开销。
好的,我们明白了:虽然多线程系统可以做令人惊奇的事情,但它有其固有的复杂性。Node.js解决了什么?
Node.js解决了复杂性问题
Node.js要求我们以不同的方式思考并发性。从事件循环异步触发的回调是一种更简单的并发模型,更易于理解、更易于实现、更易于推理、更易于调试和维护。
Node.js有一个执行线程,无需等待I/O或上下文切换。相反,有一个事件循环,在事件发生时将事件分派给处理程序函数。本来会阻止执行线程的请求会异步执行,结果或错误会触发事件。任何阻塞或需要时间才能完成的操作都必须使用异步模型。
原始的Node.js范例将调度的事件传递给匿名函数。既然JavaScript具有异步函数,Node.js范式正在转
变,通过wait关键字处理的Promise来交付结果和错误。调用异步函数时,控件会快速传递到事件循环,而不会导致Node.js阻塞。事件循环继续处理各种事件,同时记录每个结果或错误的发送位置。
通过使用异步事件驱动的I/O,Node.js极大地节省了内存开销。
Ryan Dahl在Cinco de Node演示文稿中提出的一个观点是不同请求的执行时间层次结构。与硬盘上的对象或通过网络检索的对象(毫秒或秒)相比,内存中的对象访问速度更快(以纳秒为单位)。外部对象的较长访问时间是以无数个时钟周期来衡量的,当你的客户坐在他们的Web浏览器前准备继续浏览时,如果加载页面的时间超过两秒,那么客户就会很可能离开我们的页面。
因此,并发请求处理意味着使用一种策略来处理需要更长时间才能响应的请求。如果目标是避免多线程系统的复杂性,那么系统必须像Node.js那样使用异步操作。
这些异步函数调用是什么样子的?
Node.js中的异步请求
在Node.js中,我们前面查看的查询代码如下所示:
query('SELECT * from db.table', function (err, result) {
if (err) throw err; // handle errors operate on result
});
当结果(或错误)可用时,提供一个被调用的函数(因此称为回调函数)。查询函数仍然需要同样的时间,但不会阻塞执行线程,而是返回事件循环,然后可以自由地处理其他请求。Node.js最终将触发一个事件,调用此回调函数并显示结果或错误指示。
客户端JavaScript中使用了类似的范例,我们一直在编写事件处理程序函数。
JavaScript语言的进步为我们提供了新的选择。使ES2015 promise,等同代码如下:
1 query('SELECT * from db.table') .then(result => {
2 // operate on result
3 })
4 .catch(err => {
5 // handle errors
6 });
7 // handle errors
8 }
除了async和await关键字之外,这看起来像是我们用使其他语言编写的代码,而且更易于阅读。由于是使用await执行的,所以这仍然是异步执行代码。
这三段代码段都执行我们前面编写的查询。与其说查询是一个阻塞函数的调用,不如说是异步的不会阻
塞执行线程的异步代码。使用回调函数和promise(期约)的异步编码,Node.js有其自身的复杂性问题。通常,我们一个接一个地调用异步函数。对于回调函数,这意味着是深度嵌套的回调函数;对于Promise,这意味着一长串.then处理函数。除了代码的复杂性,我们还有错误和结果出现在不应该出现的地方。异步执行的回调函数跳到下一行代码上,而是被调用。执行顺序不是一行接一行,就像在同步编程语言中一样;相反,执行顺序由回调函数的执行顺序决定。
异步函数方法解决了这种编码复杂性。编码风格更自然,因为结果和错误都出现在该出现的地方。await关键字集成了异步结果处理,而不是阻塞执行线程。在async/await特性的封装下隐藏了很多细节,在本书中我们将广泛地介绍这个模型。
但是Node.js的异步体系结构真的提高性能了吗?
性能和利用率
Node.js的一些令人兴奋之处在于它的吞吐量(可以处理的每秒请求数)。根据类似应用程序的比较基准测试(例如,Apache)表明Node.js具有巨大的性能提升。
下面是一个简单的HTTP服务器(https://nodejs.org/en/),直接从内存返回Hello World消息:
1 var http = require('http');
2 http.createServer(function (req, res) {
3 res.writeHead(200, {'Content-Type': 'text/plain'});
4 res.end('Hello World\n');
5 }).listen(8124, "127.0.0.1");
6 console.log('Server running at http://127.0.0.1:8124/');
这是一个使用Node.js构建的更简单的Web服务器。http对象封装了http协议,其http.createServer方法创建了一个完整的Web服务器,在侦听方法中指定了侦听的端口。该Web服务器上的每个请求(无论是对任何URL的GET还是POST)都会调用提供的函数。它非常简单和轻便。在这种情况下,不管URL是什么,它都会返回一个简单的文本/纯文本,即Hello World响应。
Ryan Dahl在名为Ryan Dahl:Introduction to Node.js的视频中展示了一个简单的基准测试(在YouTube上的YUI Library频道上,https://www.youtube.com/watch? v=M-sc73Y-zQA)。它使用了与此类似的HTTP服务器,但返回了一个1兆字节的二进制缓冲区;Node.js给出了每秒822个请求的成绩,
而Nginx给出的成绩为每秒708个请求,比Nginx提高了15%。他还指出,Nginx的峰值内存为4兆字节,而Node.js的峰值内存为64兆字节。
关键的观察结果是,Node.js运行的是一种解释的、JIT编译的高级语言,其速度大约与Nginx一样快,Nginx由高度优化的C代码构建,同时运行类似的任务。该演示是在2010年5月,Node.js从那以后有了很大的改进,正如我们前面提到的Chris Bailey的演讲所示。
雅虎!搜索工程师Fabian Frank发布了一个使用Apache/PHP和Node.js堆栈的两个变体实现的真实搜索查询建议小部件的性能案例研究(http://www.slideshare.net/Fab...
study). 该应用程序是一个弹出式面板,显示用户使用基于JSON的HTTP查询输入短语时的搜索建议。在相同的请求延迟下,Node.js版本每秒可以处理8倍的请求数。Fabian Frank说这两个Node.js堆栈都是线性扩展的,直到CPU使用率达到100%。
LinkedIn在服务器端使用Node.js对他们的移动应用程序进行了大规模的修改,以取代旧的Ruby on Rails应用程序。这迁移将他们的服务器从30向到3台,并且合并了前端和后端团队,因为所有内容都是用JavaScript编写的。在选择Node.js之前,他们使用Event Machine评估了Rails,使用Twisted评估了Python和和Node.js。他们选择Node.js,原因正是我们刚才讨论的。要了解LinkedIn具体的测试过程,请访问http://arstechnica.com/inform...大多数现有的Node.js性能提示都是为使用了CrankShaft优化器的旧版V8编写的。V8团队已经完全抛弃了CrankShaft,并选择了新的优化器,叫做TurboFan例。例如,在CrankShaft下,使使用try/catch、let/const、generator等函数的速度较慢。因此,人们普遍认为不要使用这些特性,这很令人沮丧,因为我们想要使用新的JavaScript特性,因为新特性对JavaScript语言有很大的改进。Google V8团队的工程师Peter Marshall在Node.js Interactive 2017上发表演讲,声称使用TurboFan,你完全可以编写原生的JavaScript。使用TurboFan的目标是全面提升V8引擎的性能。要查看演示文稿,请访问https:// www.youtube.com/watch?v=YqOhBezMx1o。
关于JavaScript的一个“真理”是,由于JavaScript的性质,所以JvaScript不适合繁重的计算任务。我们将在下一节讨论与此相关的一些问题。Mikola Lysenko在Node.js Interactive 2016上的演讲讨论了JavaScript数值计算的一些问题,以及一些可能的解决方案。普通的数值计算涉及由数值算法处理的大型数值数组,这些数值算法可能是在微积分或线性代数课程中学习的。JavaScript缺少的是多维数组和对某些CPU指令的读取。JavaScriptt提出的解决方案是使用JavaScript多维数组库,以及数值计算算法库读
取CPU指令。要查看演示文稿,请参阅Mikola Lysenko在https上制作的名为“JavaScript中的数值计算”的视频:https://www.youtube.com/watch...。
在2017年的Node.js交互会议上,IBM的Chris Bailey证明Node.js是高度可扩展微服务的最佳选择。关键性能特征是I/O性能(以每秒事务数衡量)、启动时间(因为这限制了服务扩展到满足需求的速度)和内存占用(因为这决定了每台服务器可以部署多少应用程序实例)。Node.js在所有这些指标上都表现出色;在随后的每个版本中,它要么在每个度量上都有所改进,要么保持相当稳定。Bailey给出了一些数字,将Node.js与在Spring Boot中编写的类似基准进行了比较,显示Node.js的性能要好得多。要查看他的演讲,请参阅名为Node.js Performance and Highly Scalable Micro Services-Chris Bailey,IBM,https://www. youtube.com/watch?v=Fbhhc4jtGW4。
底线是Node.js在事件驱动的I/O吞吐量方面表现出色。Node.js程序能否在计算程序方面表现出色,取决于你能否巧妙地克服JavaScript语言中的一些缺陷。
计算式编程的一个大问题是阻止了事件循环的执行。正如我们将在下一节中看到的那样,这可能会使Node.js看起来不适合所有应用。
Node.js是一场恶性的可伸缩性灾难吗
2011年10月,一篇名为Node.js的博文(从发布该博文的博客中摘取)是一种称为Node.js的癌症——可伸缩性灾难。为证明这一点而展示的示例是斐波那契序列算法的CPU限制实现。虽然这个论点是有缺陷的,因为没有人实现斐波那契,这就证明了Node.js应用程序开发者必须考虑以下问题:在哪里进行繁重的计算任务?维持Node.js应用程序高吞吐量的关键是确保事件得到快速处理。因为它使用单个执行线程,所以如果该线程因大量计算而堵塞,Node.js将无法处理事件,事件吞吐量将受到影响。
斐波那契序列作为繁重计算任务的代名词,对于这样一个大大计算量的实现,计算成本很快就会变的高昂:
1 const fibonacci = exports.fibonacci = function(n) {
2 if (n === 1 || n === 2) {
3 return 1;
4 } else {
5 return fibonacci(n-1) + fibonacci(n-2);
6 } 7 }
这是一种计算斐波那契数的特别简单的方法。是的,有很多方法可以更快地计算斐波那契数列。我们将这个计算方法作为一个通用示例来演示当事件处理程序运行缓慢时Node.js会出现什么问题,而不是讨论计算数学函数的最佳方法。考虑以下服务器:
1 const http = require('http');
2 const url = require('url');
3 http.createServer(function (req, res) {
4 const urlP = url.parse(req.url, true);
5 let fibo;
6 res.writeHead(200, {'Content-Type': 'text/plain'});
7 if (urlP.query['n']) {
8 fibo = fibonacci(urlP.query['n']); // Blocking
9 res.end('Fibonacci '+ urlP.query['n'] +'='+ fibo); } else {
10 res.end('USAGE: http://127.0.0.1:8124?n=## where ##
11 is the Fibonacci number desired');
12 }
13 }).listen(8124, '127.0.0.1');
14 console.log('Server running at http://127.0.0.1:8124');
这是前面显示的简单Web服务器的扩展。Web服务器查找请求URL中的参数n,计算它的斐波那契数。计算结束后,结果将返回给调用者。
对于足够大的n值(例如,40),服务器将完全无响应,因为事件循环未运行。相反,计算函数已阻止事件处理,因为当函数在进行计算时,事件循环无法分派事件。换句话说,斐波那契函数是所有阻塞操作的替代品。
这是否意味着Node.js是一个有缺陷的平台?不,这只是意味着程序员必须注意识别长时间运行的计算代码并开发解决方案。包括重写处理事件循环的算法,通过重写算法提高效率,集成原生代码库,或者使用后端服务器计算需要大量计算性能的计算任务。
简单地重写通过事件循环分派的计算任务,让服务器继续处理事件循环中的请求。使用回调和闭包(匿名函数),使我们能够维护异步I/O和并发Promise,如以下代码所示:
1 const fibonacciAsync = function(n, done) {
2 if (n === 0) {
3 return 0;
4 } else if (n === 1 || n === 2) {
5 done(1);
6 } else if (n === 3) {
7 return 2;
8 } else {
9 process.nextTick(function() {
10 fibonacciAsync(n-1, function(val1) {
11 process.nextTick(function() {
12 fibonacciAsync(n-2, function(val2) { done(val1+val2); }); 13 });
14 });
15 });
16 }17 }
这是一种计算斐波那契数的同样愚蠢的方法,但是通过使用process.nextTick,事件循环有机会执行。由于这是一个采用回调函数的异步函数,因此需要对服务器进行一些重构:
1 const http = require('http');
2 const url = require('url');
3 http.createServer(function (req, res) {
4 let urlP = url.parse(req.url, true);
5 res.writeHead(200, {'Content-Type': 'text/plain'});
6 if (urlP.query['n']) {
7 fibonacciAsync(urlP.query['n'], fibo => { // Asynchronous
8 res.end('Fibonacci '+ urlP.query['n'] +'='+ fibo); 9 });
10 } else {
11 res.end('USAGE: http://127.0.0.1:8124?n=## where ## is the
12 Fibonacci number desired');
13 }
14 }).listen(8124, '127.0.0.1');
15 console.log('Server running at http://127.0.0.1:8124');