node.js成也异步,败也异步,评node.js的异步特性

node.js最大的卖点就是异步,声称以此获得更高的性能,而我认为,node之所以快并不是因为异步而快,而是因为V8而快。异步仅在资源占有明显优势,而引入异步却大大增加了编程的复杂度,且异步在http服务上并没有或者无法用在刀刃上。

以目前node来看,其作为单独http服务器的实现远远不如nginx,若以node建站,更普遍的选择是nginx 反向代理 node,一则由于node作为http服务器功能简单,实现更是差nginx几个台阶,其次因node单线程特性,若要发挥多核的优势,也必然会开2个以上的node进程,由此nginx + node几乎是首选模式,如果使用cluster,可以达到不错的性能,但也是因为简单而快,非异步的优势。相对于nginx + n * php-cgi模式,其工作模式近似看成 nginx + n*65535*node,n*65535*node为假想的大量worker,n为node进程数。

首先看传统的 nginx + 50*php-cgi 工作模式。
如果使用php-fpm进行fastcgi spawn,那么平每个空php-fpm基础占用物理内存6.4M,50个php-cgi则占用了 320M + 每个请求占用的内存。
而nginx + 4*node,占用的内存仅为 52M + 每个请求占用的内存 。

同样100个worker会有100个不同的持久化连接,而node可以通过各种connections pool,使其共享持久化连接,大大减少其总连接数,node可以在任意时刻把一个连接取出或放回连接池而几乎没有性能上的损失。

这也是node异步资源上的优势。而除此之外,异步并没有想像中的这么美。

一、 异步在http服务器上表现

1. 充分利用等待时间的谎言

先看完整一次http请求,花费的时间可能是
40ms 服务器从客户端连接并接收数据
5ms 各类数据传输与接收
2ms 数据处理
1ms rendering
10ms 服务器向客户端发送数据

由此可以近似看成 55ms等待时间 + 3ms 数据实际处理时间。
但由于nginx fastcgi buffer,upload等的存在,对于php-cgi实际等待时间仅为 5ms,如果数据库处理的很糟糕,等待时间则延长为 50ms,由于存在50个worker,那么相当于每个worker延长了1ms,所以node的充分利用等待时间本身就形成了一个伪命题。由于1ms的等待那么php-cgi将占用的cpu最大为75%,等待中的25%,正好可以给系统其它进程所利用,似乎也物尽其用。且若是CPU真得持续跑在50%以上了,那也到了不得不升级硬件的时候了。

2. 无法出众的http性能

node作为独立服务器,可以达到不俗的性能,而由若与nginx配合,对比nginx + php-cgi中并没有占到太多优势。在不同机器上快约-10%~25%不等,差距并不明显。若是完整用例的快,是因为V8的出色表现,和异步与否并没有太大的关系。

3. 艰难的异步之路

并不是说异步难写,而是相对于同步来讲,如今有各种异步实现模型,其跟本的目的也是简化异步实现。而异步再简化,再优雅都还是异步,易用性比起同步来讲仍然是天地之别。用繁复的异步换取不多的性能提升,实在是得不尝失。

二、 node异步的用武之地

批判了node的异步,但异步也并非一无是处,worker无法很好胜任的场合是异步的舞台。

各种需要要持久化连接的非http连接的服务器,如网游服务器。

大量远程api调用,假设每次请求都会有2~3次通过服务器的远程api调用,那么每个请求会有400ms以上的等待时间。而这类请求数量过多,会导致阻塞了普通动态页面请求。需要增大的worker数量,或者使用不同通道的worker。但显然没有异步来得实惠,而且这类请求逻辑通常不会复杂。

简言之,如果传统的线程池或者多进程worker,数量不得不达到100以上,那么就是异步的用武之地了。

三、 一组hello world的测试

在四核 AMD Phenom 9650 下的一组最简单的hello world的测试结果
nginx + node sock
var http   = require('http');
http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Hello World\n');
}).listen("/tmp/node-"+process.argv[2]+".sock");
node + cluster
var http    = require('http');
var cluster = require('cluster')
var server  = http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Hello World\n');
});
cluster(server).listen(1337);
及php
<?php
echo 'hello world';
测试结果如下,单位(rps)
20并发与100并发
siege -c 20ab -c 100
node + cluster 12548 19194
nginx + 5*node sock 7260 12803
nginx + php-fpm 8400 11242
无并发
siege -c 1ab -c 1
node + cluster 6019 6535
nginx + 5*node sock 3024 3830
nginx + php-fpm 3839 4297
从测试结果可以发现,node + cluster 最快,nginx + php-fpm 次之,而nginx + node sock最慢。
也意味着 php-fpm 甚至快于 node sock,而cluster因其功能简单,node又没有php般完整http请求初始化,综合评价,node的底层实现不出众,异步优势更是无法得到充份的体现,

有人会说,hello world没用到异步,只是对入口的测试,对于单连接,同步在性能上并不会弱于异步,异步的优势只能在大规模持久连接或大延迟中才能得到体现。正如我上文充分利用等待时间的谎言所述,我对node与php下PostgreSQL查询性能比较,两者性能是几乎一致的。而php下memcache的实现就很不如意,比起异步还是同步更多取决于语言本身,不过这些不在本文讨论范围之内。本文主要讨论node的异步在http服务下情况,而非node vs php或者v8 vs php,

四、 小结

本文不否认node快,但快与异步与否并没有太大关系,只因node简单,更重要的是其背后的v8引擎。对于逻辑复杂的网站,抛开异步是更佳的选择。
共11条评论
  1. heywap @ 2011-11-23 10:40:11 回复

    据我所知,异步根本不能减少程序的运行时间
    不会因为你启动了一个异步,查询数据库的时间就会从10ms减少到5ms
    无论是同步还是异步,执行的时间依然是10ms
    异步的作用仅仅是提升了应用程序的吞吐量。这才是关键

    你的观点我基本上同意,但是这句话应该修改一下
    node之所以快并不是因为异步而快
    我认为说到node“快”的命题不是成立的

    如果说sqlserver2005以后的产品是“默认安全”的
    那么可以这样描述node

    默认低复杂度(征对线程开发来说)
    默认高吞吐量(线程池、异步)

    • JiangMiao @ 2011-11-23 13:14:42

      只要是非可持续连接,即非websocket,非网游tcp服务器等的传统的http连接,高吞吐量上node不占优势,前端nginx同样可以很好的吃下任何数量的并发连接,并合理分配到数十至数百个工作进程进行处理。

      由于系统到一定时候,数据库多数会采用独立服务器,而node众对异步上的支持在于查询数据库到得到结果中会有10ms的等待时间。而这10ms异步的话可以干些另的而非傻等着。

      node快肯定能成立,任何使用node与php的开发过完整应用都可以得到此结论,也正因为如此大家若受官方的文档所介绍影响而把功劳归结于异步,也是本文所要反驳的。因为除了异步部份没有明显优势外,由于v8引擎是php的数倍性能,比如node使用js渲染页面,只需0.1ms,10倍于纯php脚本的执行。页面达到一定复杂度,语言上的性能优势会逐渐突显。不接任何前端的node虽与nginx差据甚远但还是可以发挥出1倍以上之于的nginx+php-fpm的性能。反之,只要接任何前端,就只剩下语言优势了。

      关于复杂度,我一点不觉得node低复杂度,我们对复杂度的理解可能不一样。我认为异步开发是复杂的,异步流程,异常处理,资源释放等都需要特别对待。
      node是单线程,没有线程池,在cluster等帮助下可以多进程,但无论未来node怎么进化,只要v8不变,linux下只能是多进程协作,比如在现有node外加上一个壳作主控制进程,原node fork多份作工作进行以利用多核特性。

  2. heywap @ 2011-11-23 15:07:13 回复

    我们对复杂度的理解可能不一样
    是的,我很认同这样的话。

    对于一般的前端开发人员来说,可以使用相同的语法可以开发后端应用,应该来说是个很大的跨越。
    我同样认同异步开发是复杂的,线程,线程池,资源争用等等问题,正是因为这些我才说一个前端开发人员可以很快的开发一个后端应用,我所以说是低复杂度的。

    您可能有一些误区,因为js只能运行在单线程下,所以node执行js也是单线程的,这不是node的错
    虽然node是单线程执行,但是v8中可以使用多线程,所以node执行js是单线程,但是io操作是多线程的。据我所知,事实上就是是使用线程池。

    • JiangMiao @ 2011-11-23 17:23:04

      相同的语法开发后端本也是我想驳的一条,因为后端和前端区别不仅仅是语法,语法比起后端的知识体系根本是微不足道。但考虑到这的确是优势,一者两者用的库都相同,比如underscore前后端通用,再者不需要深入学习后端就可以用一些如socket.io之类的进行前后端交互,的确有可取之处,也是一大跨越。但是我还是想说句,后端没有捷近,对于js熟手,php语法只要30分钟内就可以掌握,可这又有何用,一切只是开始。

      同步会遇到各种资源争用的冲突这只是前端开发人员对后端恐惧的说词或环境搭设不利所至,在现在温室环境,不到大规模应用后端很难会遇到上述情况。况且进程同步手段繁多,而且IPC,mutex等进程,线程间调度是必备技能,虽然在传统网站开发基本上用不到,比如wordpress全代码都不见有进程控制相关的代码。

      V8不可能多线程,是由V8自身架构所决定的,Context与Handle堆都是全局变量,多线程调用V8函数,必然会使其冲突,虽然用isolate可以进入临界域避免冲突,但从外围来看将与同步单线程无异,多线程应用V8是非常不明智的,频繁进isolate只会降低性能。由于Linux中AIO实现不理想在文件IO中采用线程池,这是无奈之举。而Socket IO使用epoll或kqueue,根本不需要用到多线程,异步本身的设定就是用于取代多线程,最主要归根到最后node对于event的处理需要调用v8函数,那将仍然是单线程。并不是有多个线程在运行就是多线程。拿v8举例,单线程是共识,而实际上v8也有2个线程,一个工作线程,另一个线程进行gc等处理。

  3. heywap @ 2011-11-23 21:56:36 回复

    听您一席话很受教。。
    这里有一篇文档http://cnodejs.org/cman/addons.html这里有说到线程池的使用。
    希望有机会可向您多请教一些问题。

    • JiangMiao @ 2011-11-24 14:44:54

      文档中的libeio使用线程池进行IO处理实际上是aio的实现或许叫模拟更合适,主要用于解决异步文件IO问题。libev IO事件通知机制的跨平台库,一般单线程使用,各个平台有各自实现,比如Linux可以使用epoll,BSD使用kqueue等封装,可以支持Socket IO,inotify,计时器等,也是node的核心。那个文档已经有点老了,新技术就是变化快,现在joyent把eio和ev封装改进成libuv了。
      请教不敢当,技术方面问题我还是乐于探讨。

  4. Heron Huang @ 2012-01-08 16:25:09 回复

    没看到正确的技术论点,看到更多的是两个人相互吹捧。
    不过可惜了,Node不是单线程的. Javascipt是单线程的没错,但是Node.JS的avaScipt实现的只是回调模块,不是执行主线,所有的回调是底层的多线程C模块发起的,这才是Node异步的精髓。

    • JiangMiao @ 2012-01-08 18:40:05

      Node是否多线程参见上面对于libuv的描述。
      实际上不需要任何异步理论知识,也可从种种迹象看出Node的单线程特性。
      1. Node中没有thread相关的API。
      2. 没有Cluster的Node即单进程的Node只能利用到一个CPU,这也是为什么会有Cluster的原因。

    • JiangMiao @ 2012-01-08 18:43:42

      现在cluster并到Node中,从文档中也可以读到对Node单线程的描述
      http://nodejs.org/docs/v0.6.7/api/cluster.html
      ‘A single instance of Node runs in a single thread. To take advantage of multi-core systems the user will sometimes want to launch a cluster of Node processes to handle the load.’

    • Leric @ 2012-02-14 11:23:37

      Node是单进程单线程的,这个没什么疑问。所以程序里任何变量都不需要考虑锁的问题,因为不会被打断。

  5. Leric @ 2012-02-14 11:20:32 回复

    在我的测试里,Hello World程序Node比Nginx+PHP-FPM快3倍,用上Express加Jade的速度也是PHP裸奔的150%,这部分是V8的作用。
    1000并发下Node最慢的相应速度是3s,PHP大量出错
    100并发下Node最慢相应不到1s,PHP10s
    这部分是异步的作用。

发表评论

电子邮件地址不会被公开。 必填项已被标记为 *

*

您可以使用这些 HTML 标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>