深入浅出 Node.js(一):什么是 Node.js

阅读数:237847 2011 年 10 月 13 日

【编者按】:Node.js从 2009 年诞生至今,已经发展了两年有余,其成长的速度有目共睹。从在 github 的访问量超过 Rails,到去年底 Node.jsS 创始人 Ryan Dalh 加盟 Joyent 获得企业资助,再到今年发布 Windows 移植版本,Node.js 的前景获得了技术社区的肯定。InfoQ 一直在关注 Node.js 的发展,在今年的两次 Qcon 大会(北京站和杭州站)都有专门的讲座。为了更好地促进 Node.js 在国内的技术推广,我们决定开设“深入浅出 Node.js”专栏,邀请来自 Node.js 领域的布道师、开发人员、技术专家来讲述 Node.js 的各方面内容,让读者对 Node.js 有更深入的了解,并且能够积极投入到新技术的讨论和实践中。


专栏的第一篇文章《什么是 Node.js》尝试从各个角度来阐述 Node.js 的基本概念、发展历史、优势等,对该领域不熟悉的开发人员可以通过本文了解 Node.js 的一些基础知识。

从名字说起

有关 Node.js 的技术报道越来越多,Node.js 的写法也是五花八门,有写成 NodeJS 的,有写成 Nodejs 的,到底哪一种写法最标准呢,我们不妨遵循官方的说法。在 Node.js 的官方网站上,一直将其项目称之为”Node“或者”Node.js“,没有发现其他的说法,”Node“用的最多,考虑到 Node 这个单词的意思和用途太广泛,容易让开发人员误解,我们采用了第二种称呼——”Node.js“,js 的后缀点出了 Node 项目的本意,其他的名称五花八门,没有确切的出处,我们不推荐使用。

Node.js 不是 JS 应用、而是 JS 运行平台

看到 Node.js 这个名字,初学者可能会误以为这是一个 Javascript 应用,事实上,Node.js 采用 C++ 语言编写而成,是一个 Javascript 的运行环境。为什么采用 C++ 语言呢?据 Node.js 创始人 Ryan Dahl 回忆,他最初希望采用 Ruby 来写 Node.js,但是后来发现 Ruby 虚拟机的性能不能满足他的要求,后来他尝试采用 V8 引擎,所以选择了 C++ 语言。既然不是 Javascript 应用,为何叫.js 呢?因为 Node.js 是一个 Javascript 的运行环境。提到 Javascript,大家首先想到的是日常使用的浏览器,现代浏览器包含了各种组件,包括渲染引擎、Javascript 引擎等,其中 Javascript 引擎负责解释执行网页中的 Javascript 代码。作为 Web 前端最重要的语言之一,Javascript 一直是前端工程师的专利。不过,Node.js 是一个后端的 Javascript 运行环境(支持的系统包括 *nux、Windows),这意味着你可以编写系统级或者服务器端的 Javascript 代码,交给 Node.js 来解释执行,简单的命令类似于:

#node helloworld.js

Node.js 采用了 Google Chrome 浏览器的 V8 引擎,性能很好,同时还提供了很多系统级的 API,如文件操作、网络编程等。浏览器端的 Javascript 代码在运行时会受到各种安全性的限制,对客户系统的操作有限。相比之下,Node.js 则是一个全面的后台运行时,为 Javascript 提供了其他语言能够实现的许多功能。

Node.js 采用事件驱动、异步编程,为网络服务而设计

事件驱动这个词并不陌生,在某些传统语言的网络编程中,我们会用到回调函数,比如当 socket 资源达到某种状态时,注册的回调函数就会执行。Node.js 的设计思想中以事件驱动为核心,它提供的绝大多数 API 都是基于事件的、异步的风格。以 Net 模块为例,其中的 net.Socket 对象就有以下事件:connect、data、end、timeout、drain、error、close 等,使用 Node.js 的开发人员需要根据自己的业务逻辑注册相应的回调函数。这些回调函数都是异步执行的,这意味着虽然在代码结构中,这些函数看似是依次注册的,但是它们并不依赖于自身出现的顺序,而是等待相应的事件触发。事件驱动、异步编程的设计(感兴趣的读者可以查阅笔者的另一篇文章《Node.js 的异步编程风格》),重要的优势在于,充分利用了系统资源,执行代码无须阻塞等待某种操作完成,有限的资源可以用于其他的任务。此类设计非常适合于后端的网络服务编程,Node.js 的目标也在于此。在服务器开发中,并发的请求处理是个大问题,阻塞式的函数会导致资源浪费和时间延迟。通过事件注册、异步函数,开发人员可以提高资源的利用率,性能也会改善。

从 Node.js 提供的支持模块中,我们可以看到包括文件操作在内的许多函数都是异步执行的,这和传统语言存在区别,而且为了方便服务器开发,Node.js 的网络模块特别多,包括 HTTP、DNS、NET、UDP、HTTPS、TLS 等,开发人员可以在此基础上快速构建 Web 服务器。以简单的 helloworld.js 为例:

var http = require('http');
http.createServer(function (req, res) {
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.end('Hello World\n');
}).listen(80, "127.0.0.1");

上面的代码搭建了一个简单的 http 服务器(运行示例部署在http://helloworld.cnodejs.net/中,读者可以访问),在本地监听 80 端口,对于任意的 http 请求,服务器都返回一个头部状态码为 200、Content-Type'值为 text/plain'的”Hello World“文字响应。从这个小例子中,我们可以看出几点:

  • Node.js 的网络编程比较便利,提供的模块(在这里是 http)开放了容易上手的 API 接口,短短几行代码就可以构建服务器。
  • 体现了事件驱动、异步编程,在 createServer 函数的参数中指定了一个回调函数(采用 Javascript 的匿名函数实现),当有 http 请求发送过来时,Node.js 就会调用该回调函数来处理请求并响应。当然,这个例子相对简单,没有太多的事件注册,在以后的文章中读者会看到更多的实际例子。

Node.js 的特点

下面我们来说说 Node.js 的特点。事件驱动、异步编程的特点刚才已经详细说过了,这里不再重复。

Node.js 的性能不错。按照创始人 Ryan Dahl 的说法,性能是 Node.js 考虑的重要因素,选择 C++ 和 V8 而不是 Ruby 或者其他的虚拟机也是基于性能的目的。Node.js 在设计上也是比较大胆,它以单进程、单线程模式运行(很吃惊,对吧?这和 Javascript 的运行方式一致),事件驱动机制是 Node.js 通过内部单线程高效率地维护事件循环队列来实现的,没有多线程的资源占用和上下文切换,这意味着面对大规模的 http 请求,Node.js 凭借事件驱动搞定一切,习惯了传统语言的网络服务开发人员可能对多线程并发和协作非常熟悉,但是面对 Node.js,我们需要接受和理解它的特点。由此我们是否可以推测出这样的设计会导致负载的压力集中在 CPU(事件循环处理?)而不是内存(还记得 Java 虚拟机抛出 OutOfMemory 异常的日子吗?),眼见为实,不如来看看淘宝共享数据平台团队对 Node.js 的性能测试

  • 物理机配置:RHEL 5.2、CPU 2.2GHz、内存 4G
  • Node.js 应用场景:MemCache 代理,每次取 100 字节数据
  • 连接池大小:50
  • 并发用户数:100
  • 测试结果(socket 模式):内存(30M)、QPS(16700)、CPU(95%)

从上面的结果,我们可以看到在这样的测试场景下,qps 能够达到 16700 次,内存仅占用 30M(其中 V8 堆占用 22M),CPU 则达到 95%,可能成为瓶颈。此外,还有不少实践者对 Node.js 做了性能分析,总的来说,它的性能让人信服,也是受欢迎的重要原因。既然 Node.js 采用单进程、单线程模式,那么在如今多核硬件流行的环境中,单核性能出色的 Node.js 如何利用多核 CPU 呢?创始人 Ryan Dahl 建议,运行多个 Node.js 进程,利用某些通信机制来协调各项任务。目前,已经有不少第三方的 Node.js 多进程支持模块发布,专栏后面的文章会详细讲述 Node.js 在多核 CPU 下的编程。

Node.js 的另一个特点是它支持的编程语言是 Javascript。关于动态语言和静态语言的优缺点比较在这里不再展开讨论。只说三点:

var hostRequest = http.request(requestOptions,function(response) {
    var responseHTML ='';
    response.on('data', function (chunk) {
        responseHTML = responseHTML + chunk;
    });
    response.on('end',function(){
        console.log(responseHTML);
        // do something useful
   });
});

在上面的代码中,我们需要在 end 事件中处理 responseHTML 变量,由于 Javascript 的闭包特性,我们可以在两个回调函数之外定义 responseHTML 变量,然后在 data 事件对应的回调函数中不断修改其值,并最终在 end 事件中访问处理。

  1. Javascript 作为前端工程师的主力语言,在技术社区中有相当的号召力。而且,随着 Web 技术的不断发展,特别是前端的重要性增加,不少前端工程师开始试水”后台应用“,在许多采用 Node.js 的企业中,工程师都表示因为习惯了 Javascript,所以选择 Node.js。
  2. Javascript 的匿名函数和闭包特性非常适合事件驱动、异步编程,从 helloworld 例子中我们可以看到回调函数采用了匿名函数的形式来实现,很方便。闭包的作用则更大,看下面的代码示例:
  3. Javascript 在动态语言中性能较好,有开发人员对 Javacript、Python、Ruby 等动态语言做了性能分析,发现 Javascript 的性能要好于其他语言,再加上 V8 引擎也是同类的佼佼者,所以 Node.js 的性能也受益其中。

Node.js 发展简史

2009 年 2 月,Ryan Dahl 在博客上宣布准备基于 V8 创建一个轻量级的 Web 服务器并提供一套库。

2009 年 5 月,Ryan Dahl 在 GitHub 上发布了最初版本的部分 Node.js 包,随后几个月里,有人开始使用 Node.js 开发应用。

2009 年 11 月和 2010 年 4 月,两届 JSConf 大会都安排了 Node.js 的讲座。

2010 年年底,Node.js 获得云计算服务商 Joyent 资助,创始人 Ryan Dahl 加入 Joyent 全职负责 Node.js 的发展。

2011 年 7 月,Node.js 在微软的支持下发布 Windows 版本。

Node.js 应用案例

虽然 Node.js 诞生刚刚两年多,但是其发展势头逐渐赶超 Ruby/Rails,我们在这里列举了部分企业应用 Node.js 的案例,听听来自客户的声音。

在社交网站 LinkedIn 最新发布的移动应用中,NodeJS 是该移动应用的后台基础。LinkedIn 移动开发主管 Kiran Prasad 对媒体表示,其整个移动软件平台都由 NodeJS 构建而成:

LinkedIn 内部使用了大量的技术,但是在移动服务器这一块,我们完全基于 Node。

(使用它的原因)第一,是因为其灵活性。第二,如果你了解 Node,就会发现它最擅长的事情是与其他服务通信。移动应用必须与我们的平台 API 和数据库交互。我们没有做太多数据分析。相比之前采用的 Ruby on Rails 技术,开发团队发现 Node 在性能方面提高很多。他们在每台物理机上跑了 15 个虚拟服务器(15 个实例),其中 4 个实例即可处理双倍流量。容量评估基于负载测试的结果。

企业社会化服务网站 Yammer 则利用 Node 创建了针对其自身平台的跨域代理服务器,第三方的开发人员可以通过该服务器实现从自身域托管的 Javascript 代码与 Yammer 平台 API 的 AJAX 通信。Yammer 平台技术主管 Jim Patterson 对 Node 的优点和缺点提出了自己的看法

(优点)因为 Node 是基于事件驱动和无阻塞的,所以非常适合处理并发请求,因此构建在 Node 上的代理服务器相比其他技术实现(如 Ruby)的服务器表现要好得多。此外,与 Node 代理服务器交互的客户端代码是由 javascript 语言编写的,因此客户端和服务器端都用同一种语言编写,这是非常美妙的事情。

(缺点)Node 是一个相对新的开源项目,所以不太稳定,它总是一直在变,而且缺少足够多的第三方库支持。看起来,就像是 Ruby/Rails 当年的样子。

知名项目托管网站 GitHub 也尝试了 Node 应用。该 Node 应用称为 NodeLoad,是一个存档下载服务器(每当你下载某个存储分支的 tarball 或者 zip 文件时就会用到它)。GitHub 之前的存档下载服务器采用 Ruby 编写。在旧系统中,下载存档的请求会创建一个 Resque 任务。该任务实际上在存档服务器上运行一个 git archive 命令,从某个文件服务器中取出数据。然后,初始的请求分配给你一个小型 Ruby Sinatra 应用等待该任务。它其实只是在检查 memcache flag 是否存在,然后再重定向到最终的下载地址上。旧系统运行大约 3 个 Sinatra 实例和 3 个 Resque worker。GitHub 的开发人员觉得这是 Node 应用的好机会。Node 基于事件驱动,相比 Ruby 的阻塞模型,Node 能够更好地处理 git 存档。在编写新下载服务器过程中,开发人员觉得 Node 非常适合该功能,此外,他们还里利用了 Node 库 socket.io 来监控下载状态。

不仅在国外,Node 的优点也同样吸引了国内开发人员的注意,淘宝就实际应用了 Node 技术:

MyFOX 是一个数据处理中间件,负责从一个 MySQL 集群中提取数据、计算并输出统计结果。用户提交一段 SQL 语句,MyFOX 根据该 SQL 命令的语义,生成各个数据库分片所需要执行的查询语句,并发送至各个分片,再将结果进行汇总和计算。 MyFOX 的特点是 CPU 密集,无文件 IO,并只处理只读数据。起初 MyFOX 使用 PHP 编写,但遇到许多问题。例如 PHP 是单线程的,MySQL 又需要阻塞查询,因此很难并发请求数据,后来的解决方案是使用 nginx 和 dirzzle,并基于 HTTP 协议实现接口,并通过 curl_multi_get 命 令进行请求。不过 MyFOX 项目组最终还是决定使用 Node.js 来实现 MyFOX。

选择 Node.js 有许多方面的原因,比如考虑了兴趣及社区发展,同时也希望可以提高并发能力,榨干 CPU。例如,频繁地打开和关闭连接会让大量端口处于等待状态,当并发数量上去之后,时常会因为端口不够用(处于 TIME_WAIT 状态)而导致连接失败。之前往往是通过修改系统设置来减少等待时间以绕开这个错误,然而使用连接池便可以很好地解决这个问题。此外,以前 MyFOX 会在某些缓存失效的情况下出现十分密集的访问压力,使用 Node.js 便可以共享查询状态,让某些请求“等待片刻”,以便系统重新填充缓存内容。

小结

本文简要介绍了 Node.js 的基本知识,包括概念、特点、历史、案例等等。作为一个仅仅 2 岁的平台,Node.js 的发展势头有目共睹,越来越多的企业开始关注并尝试 Node.js,前后端开发人员应该了解相关的内容。

作者的微信公众号“老崔瞎编”,关注 IT 趋势,承载前沿、深入、有温度的内容。感兴趣的读者可以搜索 ID:laocuixiabian,或者扫描下方二维码加关注。

参考文献

[1] http://nodejs.org/

[2] http://beakkon.com/geek/node.js/why-node.js-single-thread-event-loop-javascript

[3] http://www.tbdata.org/archives/1285

[4] http://www.infoq.com/interviews/node-ryan-dahl

[5] http://www.infoq.com/cn/news/2011/08/enterprise-nodejs

[6] http://www.infoq.com/cn/news/2010/11/nodejs-joyent

[7] http://www.infoq.com/cn/news/2011/06/node-exe

[8] http://nodenode.com/post/1176414531/node-js-a-short-history

[9] http://www.infoq.com/cn/news/2011/05/nodeparty-hangzhou

【编者按】:本专栏欢迎有志于宣传和推广 Node.js 的布道师、开发人员和技术专家投稿,有意者请通过邮件与本专栏主持人崔康(cuikang[at]infoq.com)联系。


感谢张凯峰对本文的审校。

给 InfoQ 中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家加入到InfoQ 中文站用户讨论组中与我们的编辑和其他读者朋友交流。