Node.js在官网上是这样定义的:“一个搭建在Chrome JavaScript运行时上的平台,用于构建高速、可伸缩的网络程序。Node.js采用的事件驱动、非阻塞I/O模型使它既轻量又高效,是构建运行在分布式设备上的数据密集型实时程序的完美选择。”Web站点早已不仅限于内容的呈现,很多交互性和协作型环境也逐渐被搬到了网站上,而且这种需求还在不断地增长。这就是所谓的数据密集型实时(data-intensive real-time)应用程序,比如在线协作的白板,多人在线游戏等,这种web应用程序需要一个能够实时响应大量并发用户请求的平台支撑它们,这正是Node.js擅长的领域。

用Node.js处理I/O密集型任务相当简单,只需要调用它准备好的异步非阻塞函数就行了。然而数据密集型实时(data-intensive real-time)应用程序并不是只有I/O密集型任务,当碰到CPU密集型任务时,比如要对数据加解密(node.bcrypt.js),数据压缩和解压(node-tar),或者要根据用户的身份对图片做些个性化处理,这时候该怎么办呢?我们先来了解下Node.js自身的编程模型。

Node.js的先天条件

网络编程策略

上世纪90年代提出了一个著名的C10K问题。大概意思是当用户数超过1万时,很多没设计好的网络服务程序性能将急剧下降,甚至瘫痪。这时候升级硬件也不管用了,问题的根源是系统处理请求的策略,有再多的硬件资源它也用不起来。后来人们总结出了四种典型的网络编程策略:

 

相关赞助商

全球架构师峰会,12月18-19日,北京·国际会议中心,8折报名

 
  1. 服务器为每个客户端请求分配一个线程/进程,使用阻塞式I/O。Java就是这种策略,Apache也是,这种策略还是很多交互式应用的首选。因为阻塞,这种策略很难实现高性能,但非常简单,可以实现复杂的交互逻辑。
  2. 服务器用一个线程处理所有客户端请求,使用非阻塞的I/O及事件机制。node.js采用的就是这种策略。这种策略实现起来比较简单,方便移植,也能提供足够的性能,但无法充分利用多核CPU资源。
  3. 服务器会分配多个线程来处理请求,但每个线程只处理其中一组客户端的请求,使用非阻塞的I/O及事件机制。这是对第二种策略的简单改进,在多线程并发上容易出现bug。
  4. 服务器会分配多个线程来处理请求,但每个线程只处理其中一组客户端的请求,使用异步I/O。这种策略在支持异步I/O的操作系统上性能非常高,但实现起来很难,主要用在windows平台上。

因为大多数网站的服务器端都不会做太多的计算,它们只是接收请求,交给其它服务(比如文件系统或数据库),然后等着结果返回再发给客户端。所以聪明的Node.js针对这一事实采用了第二种策略,它不会为每个接入请求繁衍出一个线程,而是用一个主线程处理所有请求。避开了创建、销毁线程以及在线程间切换所需的开销和复杂性。这个主线程是一个非常快速的event loop,它接收请求,把需要长时间处理的操作交出去,然后继续接收新的请求,服务其他用户。下图描绘了Node.js程序的请求处理流程:

主线程event loop收到客户端的请求后,将请求对象、响应对象以及回调函数交给与请求对应的函数处理。这个函数可以将需要长期运行的I/O或本地API调用交给内部线程池处理,在线程池中的线程处理完后,通过回调函数将结果返回给主线程,然后由主线程将响应发送给客户端。那么event loop是如何实现这一流程的呢?这要归功于Node.js平台的V8引擎libuv

 

Event Loop和Tick

每个Node程序的主线程都有一个event loop,JavaScript代码全在这个单线程下运行。所有的I/O操作以及对本地API的调用,或者是异步的(借助程序所在平台的机制),或者运行在另外的线程中。这全都是通过libuv处理的。所以当socket上有数据过来,或本地API函数返回时,需要有种同步的方式调用对刚发生的这一特定事件感兴趣的JavaScript函数。

在发生事件的线程中直接调用JS函数是不安全的,因为那样也会遇到常规多线程程序遇到的问题,竞态条件、非原子操作的内存访问等等。所以要以一种线程安全的方式把事件放在队列中,如果写成代码,大致应该是这样的:

lock (queue) {
queue.push(event);
}

然后在执行JavaScript的主线程中(即event loop的c代码):

while (true) {
// tick开始 lock (queue) {
var tickEvents = copy(queue);
// 将当前队列中的条目复制的线程自有的内存中
queue.empty(); // ..清空共享的队列
} for (var i = 0; i < tickEvents.length; i++) {
InvokeJSFunction(tickEvents[i]);
} // tick结束
}

while (true) (在真正的node源码中并不是这样的;这里只是为了说明)表示event loop。里面的for为队列中的每个事件调用JS函数。Event loop在每个tick中都会调用与外部事件相关联的零个或多个回调函数,一旦队列被清空,并且最后一个函数返回后,tick就结束了。然后回到开始(下一个tick),重新开始检查其它线程在JavaScript运行时加到队列中的事件。

那么这个队列中的东西都是谁放进来的呢?

  • process.nextTick
  • setTimeout/setInterval
  • I/O (来自fs、net等)
  • crypto中的CPU密集型函数,比如crypto streams、pbkdf2和PRNG
  • 所有使用libuv工作队列异步调用C/C++库的本地模块

当Event loop遇到CPU密集型任务

因为event loop在处理所有的任务/事件时,都是沿着事件队列顺序执行的,所以在其中任何一个任务/事件本身没有完成之前,其它的回调、监听器、超时、nextTick()的函数都得不到运行的机会,因为被阻塞的event loop根本没机会处理它们,此时程序最好的情况是变慢,最糟的情况是停滞不动,像死掉一样。所以当Node.js遇到高CPU占用率的任务时,event loop会被阻塞住,形成下面这种局面:

被阻塞的event loop

下面给出两段代码,看一下event loop被阻塞住时的具体表现。

这段代码中的event loop以最快的速度运转,不断地向控制台中输出.

代码清单1. 快速行进的event loop

(function spinForever () {
process.stdout.write(".");
process.nextTick(spinForever);
})();

然后我们在这段代码中再加上一个计算斐波那契数列的任务。

代码清单2. 被高CPU占用率计算阻塞的event loop

function fibo (n) {
return n > 1 ? fibo(n - 1) + fibo(n - 2) : 1;
} (function fiboLoop () {
process.stdout.write(fibo(45).toString());
process.nextTick(fiboLoop);
})(); (function spinForever() {
process.stdout.write(".");
process.nextTick(spinForever);
})();

计算斐波那契数列是一个CPU密集型的任务,event loop要在计算结果出来后才有机会进入下一个tick,执行输出.的简单任务,感觉就像服务器死掉了一样。在我的机器上计算斐波那契数列时,取值45可以明显感觉到程序的停滞,你可以根据自己的CPU性能调节该值。

process.nextTick()

在Node 0.8(及之前)的版本中,process.nextTick()中指定的函数通常会比其它任何I/O先被调用,然而并不能保证一定会这样。但很多开发人员(包括Node.js的内部团队)开始用process.nextTick实现“稍后再做,但要在任何真正的I/O执行之前”。然而在负载比较大时,因为I/O很多,可能导致nextTick被别的东西占先,从而引发一些很怪异的错误。所以在v.0.10之后,netxtTick的语义被改了,那些函数变成在每次从C++进入JavaScript的调用之后马上运行。也就是说,如果你的JavaScript代码调用了process.nextTick,只要代码即将运行完成时,在回到event loop之前那个回调就会被调用。

然而还有很多程序用递归调用process.nextTick,以免长期运行的任务抢占了I/O event loop。为了不把这些程序都搞垮,Node现在会输出一个废弃警告,提示你在这些任务中使用setImmediate。不过对我们这个例子来说,这两个版本之间的差异没有影响。

被闲置的CPU内核

最开始,线程只是用于分配单个处理器处理时间的一种机制。但假如操作系统本身支持多个CPU/内核,那么每个线程都可以得到一个不同自己的CPU/内核,实现真正的“并行运算”。在这种情况下,多线程程序可以提高资源使用效率。Node.js是单线程程序,它只有一个event loop,也只占用一个CPU/内核。现在大部分服务器都是多CPU或多核的,当Node.js程序的event loop被CPU密集型的任务占用,导致有其它任务被阻塞时,却还有CPU/内核处于闲置的状态,造成资源的浪费。

你可以再次运行代码清单2中的代码,启动top(或者Windows的任务管理器)查看CPU的使用情况。我这台Mac上是一个双核的i7处理器,当node的CPU占用率在100%左右浮动时,系统的CPU占用率却只有28%左右。

既然Node.js程序几乎完全运行在单个CPU/内核上,所以我们需要做些额外的工作才能提升它的扩展性。Node.js提供了一组管理进程的API,还允许你给它编写本地扩展,所以有很多种不同的办法可以让程序的代码并行运行。

把CPU密集型任务分给子线程

自Node.js诞生之日起,就有人质疑它的单线程模型面对协作式多任务时的处理能力。但这个实际上并不是Node.js产生的新问题,在JavaScript中由来已久,可以采用Web Worker模式应对。因此我们的问题就变成了如何在Node.js程序中实现Web Worker模式,首先来看一个在Node.js中控制进程的API。

child_process.fork()

Node.js中有管理子进程的child_process模块,可以用fork()方法创建新的子进程实例。这个子进程是用IPC通道添加的,可以通过.send(message)函数发送消息给它,用.on('message')监听它发送的消息。而在子进程中,可以用process.on('message',callback)监听父进程发送的消息,并通过process.send(message)向父进程发送消息。接下来我们fork()一个子进程,把计算斐波那契数列的任务交给它,这需要两个文件。

代码清单3. 主进程文件forkParent.js

var cp = require('child_process');

var child = cp.fork(__dirname+'/forkChild.js');

child.on('message', function(m) {
process.stdout.write(m.result.toString());
}); (function fiboLoop () {
child.send({v:40});
process.nextTick(fiboLoop);
})(); (function spinForever () {
process.stdout.write(".");
process.nextTick(spinForever);
})();

在主进程中用cp.fork()创建了子进程child,并用child.on('message', callback)监听子进程发来的消息,输出计算结果。现在的fiboLoop()也不再执行具体的计算操作,只是用child.send({v:40});不停的发消息给子进程。

代码清单4. 计算斐波那契数列的子进程文件forkChild.js

function fibo (n) {
return n > 1 ? fibo(n - 1) + fibo(n - 2) : 1;
} process.on('message', function(m) {
process.send({ result: fibo(m.v) });
});

子进程文件很简单,还是原来那个计算用的函数,以及一个监听消息的process.on('message',callback),计算结果并用process.send(message, [sendHandle])发送消息给父进程。此外,父进程和子进程两者之间发送消息是同步的,所以两边是有来有往,工作开展地井然有序。运行node forkParent.js,结果跟我们预期的一样,输出.的任务不再受到阻塞,欢快地在屏幕上刷了一大堆.,然后每隔一段输出一个165580141。我们再用top查看一下资源的使用情况,会发现有两个node进程,CPU占用率也增加了很多。

实际上fork()得到的并不是进程,而是一个全新的Node.js程序实例。并且每个新实例至少需要30ms的启动时间和10M内存,也就是说通过fork()繁衍进程,不光是充分利用了CPU,也需要很多内存,所以不能fork()太多。如果你有兴趣,可以再fork()一个或几个进程,并创建跟这个(些)进程交互的函数,查看下资源占用情况。

cluster

使用cluster模块可以充分利用多核CPU资源,在Node.js的0.6版被纳入核心模块,但目前(V0.10.26)仍处于实验状态。借助cluster模块,Node.js程序可以同时在不同的内核上运行多个”工人进程“,每个”工人进程“做的都是相同的事情,并且可以接受来在同一个TCP/IP端口的请求。相对于在Ngnix或Apache后面启动几个Node.js程序实例而言,cluster用起来更加简单便捷。虽然cluster模块繁衍线程实际上用的也是child_process.fork,但它对资源的管理要比我们自己直接用child_process.fork管理得更好。下面是用cluster实现的代码:

代码清单5. 用cluster繁衍工人进程计算斐波那契数列

function fibo (n) {
return n > 1 ? fibo(n - 1) + fibo(n - 2) : 1;
} var cluster= require('cluster'); if (cluster.isMaster) {
cluster.fork();
} else {
(function fiboLoop () {
process.stdout.write(fibo(40).toString());
process.nextTick(fiboLoop);
})();
} (function spinForever () {
process.stdout.write(".");
process.nextTick(spinForever);
})();

代码很简单,如果是主进程,就fork()工人进程,这里也可以用循环遍历,根据CPU内核的个数繁衍相应数量甚至更多的进程;如果是工人进程,就执行fiboLoop。你可以自行用top查看一下资源占用情况,你会发现这种方式用得资源比上面那种方式要少。

虽然cluster模块可以充分利用资源,用起来也比较简单,但它只是解决了负载分配的问题。但其实做得也不是特别好,在0.10版本之前,cluster把负载分配的工作交给了操作系统,然而实践证明,最终负载都落在了两三个进程上,分配并不均衡。所以在0.12版中,cluster改用round-robin方式分配负载。详情请参见这里

第三方模块

除了Node.js官方提供的API,Node.js社区也为这个问题贡献了几个模块。

比如Mozilla Identity团队为Persona开发的node-compute-cluster。这个模块可以繁衍和管理完成特定计算的一组进程。你可以设定最大进程数,然后由node-compute-cluste根据负载确定进程数量。它还会追踪运行进程的数量,以及工作完成的平均时长等统计信息,方便你分析系统的处理能力。下面是一个简单的例子:

const computecluster = require('compute-cluster');

// 分配计算集群
var cc = new computecluster({ module: './worker.js' }); // 并行运行工作
cc.enqueue({ input: 35 }, function (error, result) {
console.log("35:", result);
});
cc.enqueue({ input: 40 }, function (error, result) {
console.log("40:", result);
});

文件worker.js中的代码应该响应message事件处理队列中的任务:

process.on('message', function(m) {
var output;
var output = fibo(m.input);
process.send(output);
});

还有功能强大的threads_a_gogo。参考文献中的第一篇文章介绍了一个拼字游戏解密程序LetterPwn,本文在很大程度上是受这篇文章的启发而写的,其中就是用threads_a_gogo管理CPU密集型计算线程的。由于篇幅所限,就不再展开介绍了。不过最后我们用threads_a_gogo线程池的例子作为结尾:

function fibo (n) {
return n > 1 ? fibo(n - 1) + fibo(n - 2) : 1;
} var numThreads= 10;
var threadPool= require('threads_a_gogo').createPool(numThreads).all.eval(fibo); threadPool.all.eval('fibo(40)', function cb (err, data) {
process.stdout.write(" ["+ this.id+ "]"+ data);
this.eval('fibo(40)', cb);
}); (function spinForever () {
process.stdout.write(".");
process.nextTick(spinForever);
})();

数据密集型 和 cpu密集型 event loop的更多相关文章

  1. 转:Node.js软肋之CPU密集型任务

    文章来自于:http://www.infoq.com/cn/articles/nodejs-weakness-cpu-intensive-tasks Node.js在官网上是这样定义的:“一个搭建在C ...

  2. Node.js异步处理CPU密集型任务

    Node.js异步处理CPU密集型任务 Node.js擅长数据密集型实时(data-intensive real-time)交互的应用场景.然而数据密集型实时应用程序并非仅仅有I/O密集型任务,当碰到 ...

  3. 转:Node.js异步处理CPU密集型任务的新思路

    原文来自于:http://www.infoq.com/cn/articles/new-idea-of-nodejs-asynchronous-processing-tasks?utm_source=i ...

  4. python网络编程基础(线程与进程、并行与并发、同步与异步、阻塞与非阻塞、CPU密集型与IO密集型)

    python网络编程基础(线程与进程.并行与并发.同步与异步.阻塞与非阻塞.CPU密集型与IO密集型) 目录 线程与进程 并行与并发 同步与异步 阻塞与非阻塞 CPU密集型与IO密集型 线程与进程 进 ...

  5. 线程池线程数与(CPU密集型任务和I/O密集型任务)的关系

    近期看了一些JVM和并发编程的专栏,结合自身理解,来做一个关于(线程池线程数与(CPU密集型任务和I/O密集型任务)的关系)的总结: 1.任务类型举例: 1.1: CPU密集型: 例如,一般我们系统的 ...

  6. CPU平均负载率之stress模拟CPU密集型进程

    一.对CPU密集型进程进行模拟,具体如下: 第一个终端 在第一个终端运行 stress 命令,模拟一个 CPU 使用率 100% 的场景:stress --cpu 1 -- timeout 600 第 ...

  7. 什么是CPU密集型、IO密集型?

    CPU密集型(CPU-bound) CPU密集型也叫计算密集型,指的是系统的硬盘.内存性能相对CPU要好很多,此时,系统运作大部分的状况是CPU Loading 100%,CPU要读/写I/O(硬盘/ ...

  8. CPU密集型 VS IO密集型

    CPU密集型 CPU密集型也叫计算密集型,指的是系统的硬盘.内存性能相对CPU要好很多,此时,系统运作大部分的状况是CPU Loading 100%,CPU要读/写I/O(硬盘/内存),I/O在很短的 ...

  9. 多进程于多线程的区别,cpu密集型适合用什么

    多线程:在单个程序中同事运行多少个线程完成不同的工作,成为线程. 线程共享内存空间,进程的内存是独立的, 同一个进程的线程间可以直接交流: 两个进程想通信,必须通过一个中间代理来实现, 一个线程可以控 ...

随机推荐

  1. hive添加分区

    添加分区 alter table 表名 add partition (dt='2016-09-12'); select * from 表名 where dt = '2016-09-12' limit ...

  2. Something wrong with FTK OCR

    A case about business secret the suspect took lots of photos and screenshots from BOM, RD papers... ...

  3. 样式重置 取消input默认样式

    body, h1, h2, h3, h4, h5, h6, hr, p,blockquote, dl, dt, dd, ul, ol, li,pre, form, fieldset, legend, ...

  4. Easyui扩展或者重载(方法和属性)

    1: 使用$.fn.datagrid.defaults.editors重载默认值. 每个编辑器都有以下方法: 名称 属性 描述 init container, options 初始化编辑器并返回目标对 ...

  5. pthread_cond_wait避免线程空转

    多线程对同一块区域进行操作时,需要定义如下两种类型的变量: pthread_mutex_t xxx; pthread_cond_t yyy; pthread_mutex_t类型的变量,即锁,对公共区域 ...

  6. 比较合并工具vimdiff的主要用法归纳

    参考:https://www.ibm.com/developerworks/cn/linux/l-vimdiff/ vimdiff主要用法归纳如下:   1.打开文件 vimdiff file1 fi ...

  7. CSS代码使纯英文数字自动换行

    当一个定义了宽度的块状元素中填充的全部为纯英文或者纯数字的时候,在IE和FF中都会撑大容器,不会自动换行并且当数字或者英文中带有汉字时,会从汉字处换行,而纯汉字却可以自动换行.这个问题如何解决?先来认 ...

  8. Oracle中存储过程与函数的区别

    Oracle 获取信息一般用function 修改数据用存储过程(需要执行commit命令)

  9. MongoDb 2.4 beta新特性——全文索引

    期待已久的特性,但目前仍然在beta阶段,所以官方建议不要在生产环境使用.也因此需要手动打开这个特性. 在命令行指定 mongod --setParameter textSearchEnabled=t ...

  10. 分享:php 上传图片的代码

    转自:http://www.jbxue.com/article/6379.html php 上传图片的代码,很简单,实现了基本的文件类型.文件大小的检测,并实现了基本的水印与缩略功能,比较适合初学的朋 ...