缘起……
新型的服务端正在进入我们的视野,让我们投入了关注的目光,例如近来的 NodeJS 算比较抢眼的一员。
之所以创造 NodeJS ,引用原作者 Ryan 之语,目标是为了可以更轻松地编写具有可伸缩性的网络程序。咋一想,这样的目标作为网络开发人员们何曾不想拥有。——于是看看Nodejs 是怎么实现的。首先由浅入深说下简单的概念:无论是复杂的业务逻辑,还是简单的“HelloWorld”也罢,客户端发送链接过来,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?
NodeJS 使用单线程就足以提供高速的并发能力?是的,实际上著名 Web 服务器 nginx 也是基于单线程的。然而拜 C++ 所赐,NodeJS 之于 V8 运行时却拥有多线程的运行环境。怎么理解这点呢?就是一门脚本语言去代替相对复杂的网络编程。NodeJS 带有 JS 的名称,即以 JS 为卖点,由此可见其简单性浮出水面了——这确实也就是开发 NodeJS 的初衷之一,众所周知,JS 是轻盈的,用来替 C++ 来跟开发人员打交道——再美好不过,但必须强调,JS 终究是编写中间件的脚本语言,底层发挥作用的依然为功不可没的 C++。为了实现这些设计目标,NodeJS 使用了 Google V8 并打包了其中的一些库:
- libev 实现了时间循环并封装了底层使用的具体的技术(如 select,epoll 等)。
- 作者自己写的 http-parser 等协议和其他等等。
其中 libev 正是实现多线程 NodeJS 的基础(edit on 2010-9-12: Are you sure to say so??? 有什么证据??)。JavaScript 仍旧发挥脚本语言的本色,一方面将 C++ 的复杂性屏蔽,一方面向程序员呈现优雅的 API。NodeJS 在适合一些较轻松的场合,例如一些分离器 Dispatcher、Request、BeansTalk、AMQP 消息应该没有问题。但依据国外一些博客文章分析就是,实际生产中可能会意外频频,发生一个错误就会挂起 NodeJS,所以单线程不太可靠或许是 NodeJS 一个先天的缺点。另外,编写 NodeJS 的扩展仍需要出来高深的 C++,恐怕须完善好 C 与 JS 之间的接口层,编写 NodeJS 扩展则才是我辈能力范围内的。写本文的时候,NodeJS 属新生事物,无须讳言,笔者并没有太多的一线经验。话说回来,究竟实际上有多少的情景允许我们一边计算,一边做其他的事情而稳定无虞的呢?希望可以有待更多的观察。
上述的几点,的确提到了“基于线程模型” v.s “基于事件模型”之争,目的就在于,除了明晰分辨它们的利弊之外,还不能不回答这样一个问题:既然“基于线程模型”消耗得那么厉害,那么为什么现在这么多的 Apaches、IIS 都运行得好好的?
基于事件的 Web 服务器相对是比较新的概念,可以做到比较好的性能,因而受到推崇一点不意外,像 NodeJS 那样的,——而传统的基于线程的模型服务器成熟程度高,况且仍不断地发展,例如 Apache 的 PHP 会派生出很多的 OS 线程来解决并发的问题,若一个请求挂起了其所在的线程,可以保证其他的线程也不会受到影响,不会冻结整个服务器进程,显得也比较合理。必须指出的是,像对于如何处理并发来选择“基于线程模型” v.s “基于事件模型”这样的讨论,业界一直存在,并不是说基于事件模型的一定优秀无敌,尚有许多一一斟酌讨论的地方,具体如何就不一一展开了。
那么,NodeJS 的优势到底在哪?
应对长链接的压力
如某网站 pv 非常可观,与用户互动频繁,那么它的线路总是处于高峰,自然它的网络进进出出肯定非常频繁,势必要求后台要赶快处理好前一个请求,以便接着有时间来处理一个请求,越快越好、越高效。好在,我们的请求大小都不是很大,通常几十字节(如 http://domain:80,一个 GET 操作,cookies 不大的话),控制线程在一个很小的单位,如此往返一个来回很快搞掂。那当然属于 I/O 最简单的情况了,稍为复杂的一些就是 POST 表单、文件上传等的任务。但好在不是每个链接皆如此,服务器还可以吃得消久一点的链接。可是,这时候,来问题了——
话说 Web2.0 时兴的元素,WebIM、WebGAME、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 开始认真考虑这点,在新版中提供适应长链接的场景,例如 Java 世界的 Jetty 就很早的时候提供了一个 JEE 容器的解决方案,与 Comet 的通讯协议对接上。每个 Server 的架构不一,然而如何改进和改进目标都有参考意义,但改进已是必然了,就要重新考虑 WebI/O,提供足够快而稳定性能适应长链接的场景。明显,不得不重新考虑服务端的设计了,然而,背后要考虑的事情就多了。总之,可以想象任务艰巨性,不仅要考虑前方 I/O 高并发,低响应时间的请求,还要考虑整套的服务供应者怎么去资源调控,具体如负载平衡(Load Balancing)、动态 DNS 切换、DB 的集群、多个文件镜像的问题,往往配合起来就有许多不可预料的问题发生。一个环节有问题真个系统的堵塞了。这一启承转合要处理好。
不是有 WebSocket 标准吗?HTML5 的世界尽管在移动平台上很热闹,普通浏览器升级却觉得是另一回事。如果现在一下子都是支持 Web Socket 的浏览器,那不用说准是皆大欢喜了,但事实和将来的预测表明 Web Socket 完全是另外一回事,咱和咱用户面对的仍旧那些僵硬不化的 IE6……所以说在 Web Socket 不现实的今天,将善于“长链接”的 NodeJS 派上用场便有很充分的理由。
p.s:……包括用 Flash Socket 组件那些 hack 的都不算。
发挥事件模型的威力
NodeJS 带来了一股清新之风,与其所使用 JS 乃密不可分的。这次,神奇的 JavaScript 又一次成为了胶水语言,为“基于事件驱动模型(Evnent-based)”开发埋下重要的伏笔。事件本质上一个时空不一致的非线性模型,或所谓的“异步(Asynchronization)”。事件发生的顺序按照外界对其发出的时刻而确定,有的在先,有的在后,有时也可以齐头并进,一起同时触发,——结束时也可以快的快、慢的慢。(呵呵,本人有些无聊,既然说到这儿,就突然想起小学课本,华罗庚那篇的《统筹方法》“……想泡壶茶喝。当时的情况是:开水没有。开水壶要洗,茶壶茶杯要洗;火已升了,茶叶也有了。怎么办?……”,实有异曲同工之妙!)。具体说,就是在一方面处理诸如数据库查询/存储、磁盘读写、网络延时那一类费时的任务,一方面处理内存中高速的运作,来作一个合理地平衡调度。当然,回归这一点的要求与多线性模型的 I/O 要求是无异的。总之不是直接的某个函数 method() 去执行(那是同步的方式,NodeJS 也支持),而是写回调 callback;如果换了是同步方式,就必须等待上一个任务结束,才能开始下一个任务。本来可以齐头并进的机会却白白浪费掉了。换言之,大多数操作往往是 I/O 的等待,不过 NodeJS 底层对于 JavaScript 该层面来说,由后台线程调用 JavaScript 函数,因此无碍 JS 代码本身执行,实现异步的操作,即“非阻塞”。例如下面摘自文档的一个例子:
var posix = require("posix") ,sys = require("sys"); var promise = posix.unlink("/tmp/hello"); promise.addCallback(function () { sys.puts("successfully deleted /tmp/hello"); });
如果删除文件成功,触发 success 事件执行 addCallback() 所定义的回调函数;即是删除文件失败,产生 wait 的信号,直至 timeout 的时限,也不会阻塞其他 JS 代码的执行。在 Node.js 的 API 中到处使用着事件的概念,包括许多方法都设有“同步”和“异步”的两种方式供选择,故所以我们不用担心写的代码会阻塞 Node.js 的 I/O。
个人认为,从感觉而言,两者之间还有一点的差异可能是,多线性模型不像编写事件那么自然。定义事件起来隐约会有一种写“DSL”的感觉,尤其在 JS 这个 Function First Class 的脚本帮助下。另外可以参考一下前一篇《Node.js 引言 》的博文,此处不再复述。
题外话:貌似 AJAX AIR in JS 呈现了也是一种异步调用方式(记得 SQL query 时语法相似)。
事件循环的 console 模拟图
实际上,NodeJS 不是第一家标榜事件的 Web Server,早在 NodeJs 之前,在各种语言中都有事件的实现,不能不提的就是 nginx。不过使用 JavaScript 的还属于头一遭吧?过去几年可以说是 JS 引擎发展的高峰期,就连最保守的微软也要 IE9 把落后的 JS 解释速度争回来,亲爱的服务端方面却又怎么按耐的住呢?自然,革新速度后,JSVM 引入到 ServerSide 的工作更是一件顺理成章的事。
话说回来基于事件理念的 Server。NodeJS 的 idea 最初启发自 Ruby 的 EventMachine 和 Python 的 Twisted,将包括各种 I/O 操作定义在回调函数中,通过事件不断轮询任务列表来触发那些 Callback,——并且 NodeJS 有创新的地方,就是提出新的思路来呈现事件机制。从原理上讲,NodeJS 不仅仅是一个库,而是尝试利用语言机制来构建的事件模型。EventMachine 或 Twisted 却不是这样,它们都是在代码开始和结束的时候插入回调函数来完成一个阻塞的调用,然后这个过程的启用,就用:
EventMachine::run()
而 NodeJS 没有这种代码顺序的限制,可以在定义代码之后再插入新的代码,继续参与事件。同时 NodeJS 也不会像 Twisted Python 那样提供“延时线程(deferto thread)”,实际是堵塞代码的“陷阱”。
尽管我们这里说的事件模型好像比较简单,但是许多的基础设施对异步操作的支持的不足的,尤其普通用户根本不会自己去创建业务事件。相关内容在介绍 NodeJS 的 Slide 有介绍(搜索 jsconf.pdf),说明为什么 NodeJS 出现之前没有类似 NodeJS 的“物体”出现,同时也说明设计 NodeJS 要克服的难关。
2013-2-7 Edit:
- 同步的好处:同步流程是最天然的控制过程顺序执行的方式,因为同步流程对结果的处理始终和前文保持在一个上下文内,所以同步流程对结果处理通常更为简单,可以就近处理,因此可以很容易捕获、处理异常。
- 异步流程在执行的过程中,可以释放占用的线程等资源,避免阻塞,等到结果产生再重新获取线程处理,因此,在此期间可以做更多额外的工作,例如结果记录等等。这样,异步流程可以等多次调用的结果出来后,再统一返回一次结果集合,提高响应效率。
结语
最后一点,谈谈 NodeJS 为什么选择 GoogleV8 的 JS 引擎而不是另一个著名引擎 SpiderMonkey 。抛开速度等的硬性指标不表,依然可能认为 SpiderMonkey 源码仍比较复杂的缘故,不好把玩,既然这样,人们自然就青睐 V8 了 。
本文介绍了一位 JS 爱好者对 NodeJS 以及后台初步感性的了解,没有深刻的认识,竟也成文,看官们可作一定取舍(trade-off),将就来读,或请积极献言,一同讨论。
参考:
WernerSchuster,http://www.infoq.com/news/2009/11/nodejs-evented-io
LouisSimoneau,Node.js is theNew Black
UDP& Dgram UNIX daemon Socket supporthttp://groups.google.com/group/nodejs/browse_thread/thread/665422a1dc28d874
PaulQuerna, Drinking the Node.js Kool-Aid