之所以创造NodeJS ,引用原作者Ryan 语,目标就是为了可以更轻松地编写具有可伸缩性的网络程序。咋一看,这样的目标作为网络开发人员们何曾不想拥有。——于是看看Nodejs 是怎么实现的。首先由浅入深说下简单的概念:无论是复杂的业务逻辑,还是简单的“Hello World” 也罢,客户端发送链接过来,Web 服务器肯定要一一全单照收,不会拒“链接”于千里之外。当中所说的性能指标就是我们日常会提到的——“并发(Concurrency )”。Web 服务器是并发处理这些链接请求的。并发越高,服务器性能越好——到最终,大概是要解决著名C10K 问题。在处理并发的这个技术问题上,NodeJS 表现出来的,就是高并发、低消耗的佼佼者。
NodeJS 有一定性能优势,却引发了我们技术人员的浓厚兴趣。不免要问,NodeJS 是如何办到的?NodeJS 是开源项目,如果不打算直接通过源码了解,我们还是可以通过网上一点资讯了解。笔者收集了关于NodeJS 的几遍文章、博客,略有心得,将它们想表达NodeJS 的特点、缺点、相关原理、前期分析、选型等等各方的问题“共冶一炉”,说出个NodeJS 初步分析的大概。
如果各位看官不太了解服务端的运作的话,我们稍微回顾一下请求Request 这一环节的过程。现今多数的Web 服务器中,有一条新的链接就会申请一条线程来负责处理至到这个Request 周期结束,接着执行其他流程。可以想象,成千上万个链接便有成千上万条线程(Thread-spawning )。每条线程姑且以堆栈2MB 的消耗去计算,一条条线程它们的累加都是不小的数目。如何优化和改进本身就是一个大问题,此外,使用系统线程,必须考虑线程锁的问题,否则造成堵塞主进程又是一个令人操心的难题。NodeJS 则通过基于事件的异步模型绕开了基于线程模型的所带来的问题。NodeJS 使用JavaScript 单线程(Single-threaded )轮询事件,设计上比较简单,高并发时,不仅根本性的减少了线程创建和切换的开销(因而没有吓人的消耗),而且由于没有锁,也不会造成进程阻塞。每当有链接发起到服务端之后,NodeJS 会透过epoll 、 kqueue 、/dev/poll 或 select 指令通知操作系统,有新链接到达,应执行指定的回调函数(Callback )。每个链接从成本上说只消耗一个堆(heap allocation )。
NodeJS 使用单线程就足以提供高速的并发能力?是的,实际上著名nginx 也是基于单线程的。然而拜C++ 所赐,Node.js 却拥有多线程的运行环境。NodeJS 虽带有JS 的名称,以JS 为卖点,——确实也如此,轻盈的JS 替C/C++ 跟开发人员打交道,但必须强调,JS 终究是编写中间件的脚本语言,底层发挥作用的仍然是C/C++ 。为了实现这些设计目标,Node.js 使用了Google V8 并打包了其中的一些库:
libev 实现了时间循环并封装了底层使用的具体的技术(如select, epoll 等)。
作者自己写的http-parser 等协议和其他等等。
其中libev 正是实现多线程NodeJs 的基础(edit on 2010-9-12:Are you sure to say so???有什么证据??)。JavaScript 仍旧发挥脚本语言的本色,一方面将C/C++ 的复杂性屏蔽,一方面向程序员呈现优雅的API 。Node.js 在适合一些较轻松的场合,包括一些分离器Dispatcher 、Request 、BeansTalk 、AMQP 消息应该没有问题。但依据国外一些博客文章分析就是,实际生产中可能会意外频频,发生一个错误就会挂起node.js ,所以单线程不太可靠或许是nodejs 一个先天的缺点。另外,编写NodeJS 的扩展仍需要出来高深的C++ ,恐怕须完善好C 与JS 之间的接口层,编写NodeJS 扩展才是我辈能力范围内。写本文的时候,Node 属新生事物,无须讳言,笔者没有太多的直观经验。究竟实际上有多少的情景允许我们一边计算,一边做其他的事情而稳定无虞的呢?希望可以有待更多的观察。
上述的几点,的确提到了“基于线程模型”v.s “ 基于事件模型”之争,目的就在于,除了明晰分辨它们的利弊之外,还不能不回答这样一个问题:既然“基于线程模型”消耗得那么厉害,那么为什么现在这么多的Apaches 、IIS 都运行得好好的?
基于事件的Web 服务器相对是比较新的概念,可以做到比较好的性能,因而受到推崇一点不意外,像Node.js 那样的,——而传统的基于线程的模型服务器成熟程度高,况且仍不断地发展,例如Apache 的PHP 会派生出很多的OS 线程来解决并发的问题,若一个请求挂起了其所在的线程,可以保证其他的线程也不会受到影响,不会冻结整个服务器进程,显得也比较合理。必须指出的是,像对于如何处理并发来选择“基于线程模型”v.s “ 基于事件模型”这样的讨论,业界一直存在,并不是说基于事件模型的一定优秀无敌,尚有许多一一斟酌讨论的地方,具体如何就不一一展开了。
那么,Node.js 的优势到底在哪?
例如,某网站pv 非常可观,与用户互动频繁,那么它的线路总是处于高峰,自然它的网络进进出出肯定非常频繁,势必要求后台要赶快处理好前一个请求,以便接着有时间来处理一个请求,越快越好、越高效。好在,我们的请求大小都不是很大,通常几十字节(如http://domain:80 ,一个GET 操作,cookies 不大的话),控制线程在一个很小的单位,如此往返一个来回很快搞掂。那当然属于I/O 最简单的情况了,稍为复杂的一些就是POST 表单、文件上传等的任务。但好在不是每个链接皆如此,服务器还可以吃得消久一点的链接。可是,这时候,来问题了——
话说Web2.0 时兴的元素,Web IM 、Web GAME 、Web 协作……无一不需求长链接为其服务的。长链接,或长轮询,是企图突破现有HTTP v1.1 链接模型,把无态(Stateless )的点对点链接变为人们理想的有态(Stateful ),也就是Request/Response 互不分离,总是在线有沟通着。实际情形HTTP 并没有提供这种的API 或者说服务。当前我们大抵采用折衷的方法:打开一HTML 页面立刻发送服务端的AJAX 请求,就算是没有内容的请求都好,没有关系,服务器就千万别像普通AJAX 那样接收请求,处理流程后就返回Repsonse ,不要立刻返回内容而是等待,换言之,就是保持链接。只是在有消息发出的时候才返回Response 然后浏览器渲染Response 内容。例如,有好友发悄悄话给你,通过服务器发送到你浏览器上显示,然后立刻发起新的请求,让彼此之间的链接一直保持下去。
介绍前面的这么多,无非想说明,客户端与服务端一旦链接后,除非用户关闭浏览器,否则是不会断开keep -alive 链接的。这样,对于同时维系着数十条或者数百条(聊天室)的connection 的服务器,一直非空闲,还要顾上各方面资源(CPU usage 、consuming memory…… ),显然不是一件容易事情,甚至如项目“开心网”那样成千上万笔connection 场景就是对服务端极大的考验,如果占用的线程不能得到迅速释放,将会给服务器带来灾难性的后果!
于是一些Web Serever 认真考虑到这点,在新版中提供适应长链接的场景,例如Jetty 很早的时候就提供一个J2EE 容器的解决方案,与Comet 的通讯协议对接上。每个Server 的架构不一,然而如何改进和改进目标都有参考意义,但改进已是必然了,就要重新考虑Web I/O ,提供足够快而稳定性能适应长链接的场景。明显,不得不重新考虑服务端的设计了,然而,背后要考虑的事情就多了。总之,可以想象任务艰巨性,不仅要考虑前方I/O 高并发,低响应时间的请求,还要考虑整套的服务供应者怎么去资源调控,具体如负载平衡(Load Balancing )、动态DNS 切换、DB 的集群、多个文件镜像的问题,往往配合起来就有许多不可预料的问题发生。一个环节有问题真个系统的堵塞了。这一启承转合要处理好。
不是有WebSocket 标准吗?HTML5 的世界尽管在移动平台上很热闹,普通浏览器升级却觉得是另一回事。如果现在一下子都是支持WebSocket 的浏览器,那不用说准是皆大欢喜了,但事实和将来的预测表明Web Socket 完全是另外一回事,咱和咱用户面对的仍旧那些僵硬不化的IE6…… 所以说在WebSocket 不现实的今天,将善于“长链接”的Nodejs 派上用场便有很充分的理由。
p.s :……包括用flash socket 组件那些hack 的都不算。
Node.Js 带来了一股清新之风,与其使用JS 乃是密不可分的。这次,神奇的JavaScript 又一次成为了胶水语言,为“基于事件驱动模型(Evnent-based )”开发埋下重要的伏笔。事件本质上一个时空不一致的非线性模型,或所谓的“异步(Asynchronization )”。事件发生的顺序按照外界对其发出的时刻而确定,有的在先,有的在后,有时也可以齐头并进,一起同时触发,——结束时也可以快的快、慢的慢。(呵呵,本人有些无聊,突然想起小学课本,华罗庚那篇的《统筹方法 》“……想泡壶茶喝。当时的情况是:开水没有。开水壶要洗,茶壶茶杯要洗;火已升了,茶叶也有了。怎么办?……”,实有异曲同工之妙!)。具体说,就是在一方面处理诸如数据库查询/ 存储、磁盘读写、网络延时那一类费时的任务,一方面处理内存中高速的运作,来作一个合理地平衡调度。当然,回归这一点的要求与多线性模型的I/O 要求是无异的。总之不是直接的某个函数method() 去执行(那是同步的方式,Node.js 也支持),而是写回调callback ;如果换了是同步方式,就必须等待上一个任务结束,才能开始下一个任务。本来可以齐头并进的机会却白白浪费掉了。换言之,大多数操作往往是I/O 的等待,不过NodeJS 底层对于JavaScript 该层面来说,由后台线程调用JavaScript 函数,因此无碍JS 代码的执行,实现异步的操作,即“非阻塞”。例如下面摘自文档的一个例子:
如果删除文件成功,触发success 事件执行addCallback() 所定义的回调函数;即是删除文件失败,产生wait 的信号,直至timeout 的时限,也不会阻塞其他JS 代码的执行。在Node.js 的API 中,到处使用事件的概念,包括许多方法都设有“同步”和“异步”的两种方式供选择,故所以我们不用担心写的代码会阻塞Node.js 的I/O 。
个人认为,从感觉而言,两者之间还有一点的差异可能是,多线性模型不像编写事件那么自然。定义事件起来隐约会有一种写“DSL” 的感觉,尤其在JS 这个Function First 的脚本帮助下。另外可以参考一下前一篇《node.js引言 》的博文 ,此处不再复述。
题外话:貌似AJAX AIR in Js 呈现了也是一种异步调用方式(记得SQLquery 时语法相似)。
事件循环的console模拟图
实际上,NodeJs 不是第一家标榜事件的Web Server ,早在NodeJs 之前,在各种语言中都有事件的实现,不能不提的就是nginx 。不过使用JavaScript 还属于头一遭吧。过去几年可以说是JS 引擎发展的高峰期,就连最保守的微软也要IE9 把落后的JS 解释速度争回来,亲爱的服务端方面却又怎么按耐的住呢?自然,革新速度后,JS VM 引入到Server side 的工作便是一个顺理成章的事。
话说回来基于事件理念的Server 。Node.js 最初得益于Ruby 的Event Machine 和Python 的Twisted ,将包括各种I/O 操作定义在回调函数中,通过事件不断轮询任务列表来触发那些Callback ,——并且NodeJS 有创新的地方,就是提出新的思路来呈现事件机制。从原理上讲,Node.js 不仅仅是一个库,而是尝试利用语言机制来构建的事件模型。Event Machine 或Twisted 不是这样,它们都是在代码开始和结束的时候插入回调函数来完成一个阻塞的调用,然后这个过程的启用,就用:
而Node.js 没有这种代码顺序的限制,可以在定义代码之后再插入新的代码,继续参与事件。同时node.js 也不会Twisted Python 那样提供“延时线程(defer to thread )”,实际是堵塞代码的“陷阱”。
尽管我们这里说的事件模型好像比较简单,但是许多的基础设施对异步操作的支持的不足的,尤其普通用户根本不会自己去创建业务事件。相关内容在介绍NodeJS 的Slide 有介绍(搜索jsconf.pdf ),说明为什么Nodejs 出现之前没有类似NodeJS 的“物体”出现,同时也说明设计nodejs 要克服的难关。
最后一点,谈谈node.js 为什么选择Google V8 的JS 引擎而不是另一个著名的SpiderMonkey 引擎。抛开速度等的硬性指标不表,可能是SpiderMonkey 源码仍然比较复杂的缘故,不好把玩,人们自然就青睐于虽然是C++ 的V8 了。
本文介绍了一位JS 爱好者对NodeJS 以及后台初步感性的了解,没有深刻的认识,竟也成文,看官们可作取舍(trade-off ),将就来读,并请积极献言,一同讨论。