若是你对NodeJs系列感兴趣,欢迎关注微信公众号:前端神盾局php
这两天在整理NodeJs线程和进程相关的文章时,偶然search到下文(略微有所补充),内容详实,故分享出来。文末附有原文地址和做者。css
不少Node.js初学者都会有这样的疑惑,Node.js究竟是单线程的仍是多线程的?本文解释了Node.js对于单/多线程的关系和支持状况。同时本文还将列举一些让Node.js的web服务器线程阻塞的例子,最后会提供Node.js碰到这类cpu密集型问题的解决方案。html
阅读本文前,你须要对Node.js有一个初步的认识,熟悉Node.js
基本语法、cluster
模块、child_process
模块和express
框架;接触过apache
的http
压力测试工具ab
;了解通常web服务器对于静态文件的处理流程。前端
早期有不少关于Node.js争论的焦点都在它的单线程模型方面,在由Jani Hartikainen写的一篇著名的文章《PHP优于Node.js的五大理由》中,更有一条矛头直接指向Node.js单线程脆弱的问题。html5
若是PHP代码损坏,不会拖垮整个服务器。node
PHP代码只运行在本身的进程范围中,当某个请求显示错误时,它只对特定的请求产生影响。而在Node.js环境中,全部的请求均在单一的进程服务中,当某个请求致使未知错误时,整个服务器都会受到影响。python
Node.js和Apache+PHP还有一个很是不一样的地方就是进程的运行时间长短,固然这一点也被此文做为一个PHP优于Node.js的理由来写了。jquery
PHP进程短暂。linux
在PHP中,每一个进程对请求持续的时间很短暂,这就意味着你没必要为资源配置和内存而担心。而Node.js的进程须要运行很长一段时间,你须要当心并妥善管理好内存。好比,若是你忘记从全局数据中删除条目,这会轻易的致使内存泄露。git
在这里咱们并不想引发一次关于PHP和Node.js孰优孰劣的口水仗,PHP和Node.js各表明着一个互联网时代的开发语言,就如同咱们讨论跑车和越野车谁更好同样,它们都有本身所擅长和适用的场景。 咱们能够经过下面这两张图深刻理解一下PHP和Node.js对处理Http请求时的区别。
PHP的模型:
Node.js的模型:
因此你在编写Node.js代码时,要保持清醒的头脑,任何一个隐藏着的异常被触发后,都会将整个Node.js进程击溃。可是这样的特性也为咱们编写代码带来便利,好比一样要实现一个简单的网站访问次数统计,Node.js只须要在内存里定义一个变量var count=0;,每次有用户请求过来执行count++;便可。
var http = require('http');
var count = 0;
http.createServer(function (request, response) {
response.writeHead(200, {'Content-Type': 'text/plain'});
response.end((++count).toString())
}).listen(8124);
console.log('Server running at http://127.0.0.1:8124/');
复制代码
可是对于PHP来讲就须要使用第三方媒介来存储这个count值了,好比建立一个count.txt文件来保存网站的访问次数。
<?php
$counter_file = ("count.txt");
$visits = file($counter_file);
$visits[0]++;
$fp = fopen($counter_file,"w");
fputs($fp,"$visits[0]");
fclose($fp);
echo "$visits[0]";
?>
复制代码
Google的V8 Javascript引擎已经在Chrome浏览器里证实了它的性能,因此Node.js的做者Ryan Dahl选择了v8做为Node.js的执行引擎,v8赋予Node.js高效性能的同时也注定了Node.js和大名鼎鼎的Nginx同样,都是以单线程为基础的,固然这也正是做者Ryan Dahl设计Node.js的初衷。
Node.js的单线程(单纯从js的角度讨论,咱们能够认为Node.js是单线程的,但其实不是)具备它的优点,但也并不是十全十美,在保持单线程模型的同时,它是如何保证非阻塞的呢?
首先,单线程避免了传统PHP那样频繁建立、切换进程的开销,使执行速度更加迅速。 第二,资源占用小,若是有对Node.js的web服务器作过压力测试的朋友可能发现,Node.js在大负荷下对内存占用仍然很低,一样的负载PHP由于一个请求一个线程的模型,将会占用大量的物理内存,极可能会致使服务器因物理内存耗尽而频繁交换,失去响应。
单线程的js还保证了绝对的线程安全,不用担忧同一变量同时被多个线程进行读写而形成的程序崩溃。好比咱们以前作的web访问统计,由于单线程的绝对线程安全,因此不可能存在同时对count变量进行读写的状况,咱们的统计代码就算是成百的并发用户请求都不会出现问题,相较PHP的那种存文件记录访问,就会面临并发同时写文件的问题。 线程安全的同时也解放了开发人员,免去了多线程编程中忘记对变量加锁或者解锁形成的悲剧。
Node.js是单线程的,可是它如何作到I/O的异步和非阻塞的呢?其实Node.js在底层访问I/O仍是多线程的,有兴趣的朋友能够翻看Node.js的fs模块的源码,里面会用到libuv来处理I/O,因此在咱们看来Node.js的代码就是非阻塞和异步形式的。
阻塞/非阻塞与异步/同步是两个不一样的概念,同步不表明阻塞,可是阻塞确定就是同步了。
举个现实生活中的例子,我去食堂打饭,我选择了A套餐,而后工做人员帮我去配餐,若是我就站在旁边,等待工做人员给我配餐,这种状况就称之为同步;若工做人员帮我配餐的同时,排在我后面的人就开始点餐,这样整个食堂的点餐服务并无由于我在等待A套餐而中止,这种状况就称之为非阻塞。这个例子就简单说明了同步但非阻塞的状况。
再若是我在等待配餐的时候去买饮料,等听到叫号再回去拿套餐,此时个人饮料也已经买好,这样我在等待配餐的同时还执行了买饮料的任务,叫号就等于执行了回调,就是异步非阻塞了。
既然Node.js是单线程异步非阻塞的,是否是咱们就能够高枕无忧了呢?
仍是拿上面那个买套餐的例子,若是我在买饮料的时候,已经叫个人号让我去拿套餐,但是我等了很久才拿到饮料,因此我可能在大厅叫个人餐号以后好久才拿到A套餐,这也就是单线程的阻塞状况。
在浏览器中,js都是以单线程的方式运行的,因此咱们不用担忧js同时执行带来的冲突问题,这对于咱们编码带来不少的便利。
可是对于在服务端执行的Node.js,它可能每秒有上百个请求须要处理,对于在浏览器端工做良好的单线程js是否也能一样在服务端表现良好呢?
咱们看以下代码:
var start = Date.now();//获取当前时间戳
setTimeout(function () {
console.log(Date.now() - start);
for (var i = 0; i < 1000000000; i++){//执行长循环
}
}, 1000);
setTimeout(function () {
console.log(Date.now() - start);
}, 2000);
复制代码
最终咱们的打印结果是:(结果可能由于你的机器而不一样)
1000
3738
复制代码
对于咱们指望2秒后执行的setTimeout函数其实通过了3738毫秒以后才执行,换而言之,由于执行了一个很长的for循环,因此咱们整个Node.js主线程被阻塞了,若是在咱们处理100个用户请求中,其中第一个有须要这样大量的计算,那么其他99个就都会被延迟执行。
其实虽然Node.js能够处理数以千记的并发,可是一个Node.js进程在某一时刻其实只是在处理一个请求。
线程是cpu调度的一个基本单位,一个cpu同时只能执行一个线程的任务,一样一个线程任务也只能在一个cpu上执行,因此若是你运行Node.js的机器是像i5,i7这样多核cpu,那么将没法充分利用多核cpu的性能来为Node.js服务。
在C++、C#、python等其余语言都有与之对应的多线程编程,有些时候这颇有趣,带给咱们灵活的编程方式;可是也可能带给咱们一堆麻烦,须要学习更多的Api知识,在编写更多代码的同时也存在着更多的风险,线程的切换和锁也会形成系统资源的开销。
就像上面的那个例子,若是咱们的Node.js有建立子线程的能力,那问题就迎刃而解了:
var start = Date.now();
createThread(function () { //建立一个子线程执行这10亿次循环
console.log(Date.now() - start);
for (var i = 0; i < 1000000000; i++){}
});
setTimeout(function () { //由于10亿次循环是在子线程中执行的,因此主线程不受影响
console.log(Date.now() - start);
}, 2000);
复制代码
惋惜也能够说可喜的是,Node.js的核心模块并无提供这样的api给咱们,咱们真的不想多线程又回归回来。不过或许多线程真的可以解决咱们某方面的问题。
Jorge Chamorro Bieling是tagg(Threads a gogo for Node.js)
包的做者,他硬是利用phread
库和C语言让Node.js支持了多线程的开发,咱们看一下tagg
模块的简单示例:
var Threads = require('threads_a_gogo');//加载tagg包
function fibo(n) {//定义斐波那契数组计算函数
return n > 1 ? fibo(n - 1) + fibo(n - 2) : 1;
}
var t = Threads.create().eval(fibo);
t.eval('fibo(35)', function(err, result) {//将fibo(35)丢入子线程运行
if (err) throw err; //线程建立失败
console.log('fibo(35)=' + result);//打印fibo执行35次的结果
});
console.log('not block');//打印信息了,表示没有阻塞
复制代码
上面这段代码利用tagg包将fibo(35)这个计算丢入了子线程中进行,保证了Node.js主线程的舒畅,当子线程任务执行完毕将会执行主线程的回调函数,把结果打印到屏幕上,执行结果以下:
not block
fibo(35)=14930352
复制代码
斐波那契数列,又称黄金分割数列,这个数列从第三项开始,每一项都等于前两项之和:0、一、一、二、三、五、八、1三、2一、……。
注意咱们上面代码的斐波那契数组算法并非最优算法,只是为了模拟cpu密集型计算任务。
因为tagg
包目前只能在linux下安装运行,因此我fork
了一个分支,修改了部分tagg
包的代码,发布了tagg2
包。tagg2
包一样具备tagg
包的多线程功能,采用新的node-gyp
命令进行编译,同时它跨平台支持,mac
,linux
,windows
下均可以使用,对开发人员的api也更加友好。安装方法很简单,直接npm install tagg2
。
一个利用tagg2计算斐波那契数组的http服务器代码:
var express = require('express');
var tagg2 = require("tagg2");
var app = express();
var th_func = function(){//线程执行函数,如下内容会在线程中执行
var fibo =function fibo (n) {//在子线程中定义fibo函数
return n > 1 ? fibo(n - 1) + fibo(n - 2) : 1;
}
var n = fibo(~~thread.buffer);//执行fibo递归
thread.end(n);//当线程执行完毕,执行thread.end带上计算结果回调主线程
};
app.get('/', function(req, res){
var n = ~~req.query.n || 1;//获取用户请求参数
var buf = new Buffer(n.toString());
tagg2.create(th_func, {buffer:buf}, function(err,result){
//建立一个js线程,传入工做函数,buffer参数以及回调函数
if(err) return res.end(err);//若是线程建立失败
res.end(result.toString());//响应线程执行计算的结果
})
});
app.listen(8124);
console.log('listen on 8124');
复制代码
其中~~req.query.n
表示将用户传递的参数n取整,功能相似Math.floor
函数。
咱们用express
框架搭建了一个web服务器,根据用户发送的参数n的值来建立子线程计算斐波那契数组,当子线程计算完毕以后将结果响应给客户端。因为计算是丢入子线程中运行的,因此整个主线程不会被阻塞,仍是可以继续处理新请求的。
咱们利用apache
的http
压力测试工具ab来进行一次简单的压力测试,看看执行斐波那契数组35次,100客户端并发100个请求,咱们的QPS (Query Per Second)
每秒查询率在多少。
ab的全称是ApacheBench,是Apache附带的一个小工具,用于进行HTTP服务器的性能测试,能够同时模拟多个并发请求。
咱们的测试硬件:linux 2.6.4 4cpu 8G 64bit
,网络环境则是内网。
ab压力测试命令:
ab -c 100 -n 100 http://192.168.28.5:8124/?n=35
复制代码
Server Software:
Server Hostname: 192.168.28.5
Server Port: 8124
Document Path: /?n=35
Document Length: 8 bytes
Concurrency Level: 100
Time taken for tests: 5.606 seconds
Complete requests: 100
Failed requests: 0
Write errors: 0
Total transferred: 10600 bytes
HTML transferred: 800 bytes
Requests per second: 17.84 [#/sec](mean)
Time per request: 5605.769 [ms](mean)
Time per request: 56.058 [ms](mean, across all concurrent requests)
Transfer rate: 1.85 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 3 4 0.8 4 6
Processing: 455 5367 599.7 5526 5598
Waiting: 454 5367 599.7 5526 5598
Total: 461 5372 599.3 5531 5602
Percentage of the requests served within a certain time (ms)
50% 5531
66% 5565
75% 5577
80% 5581
90% 5592
95% 5597
98% 5600
99% 5602
100% 5602 (longest request)
复制代码
咱们看到Requests per second
表示每秒咱们服务器处理的任务数量,这里是17.84
。第二个咱们比较关心的是两个Time per request
结果,上面一行Time per request:5605.769
ms表示当前这个并发量下处理每组请求的时间,而下面这个Time per request:56.058 [ms](mean, across all concurrent requests)
表示每一个用户平均处理时间,由于咱们本次测试并发是100,因此结果正好是上一行的100分之1。得出本次测试平均每一个用户请求的平均等待时间为56.058 [ms]
。
另外咱们看下最后带有百分比的列表,能够看到50%的用户是在5531 ms
之内返回的,最慢的也不过5602 ms
,响应延迟很是的平均。
咱们若是用cluster
来启动4个进程,是否能够充分利用cpu达到tagg2
那样的QPS呢?咱们在一样的网络环境和测试机上运行以下代码:
var cluster = require('cluster');//加载clustr模块
var numCPUs = require('os').cpus().length;//设定启动进程数为cpu个数
if (cluster.isMaster) {
for (var i = 0; i < numCPUs; i++) {
cluster.fork();//启动子进程
}
} else {
var express = require('express');
var app = express();
var fibo = function fibo (n) {//定义斐波那契数组算法
return n > 1 ? fibo(n - 1) + fibo(n - 2) : 1;
}
app.get('/', function(req, res){
var n = fibo(~~req.query.n || 1);//接收参数
res.send(n.toString());
});
app.listen(8124);
console.log('listen on 8124');
}
复制代码
在终端屏幕上打印了4行信息:
listen on 8124
listen on 8124
listen on 8124
listen on 8124
复制代码
咱们成功启动了4个cluster以后,用一样的ab压力测试命令对8124端口进行测试,结果以下:
Server Software:
Server Hostname: 192.168.28.5
Server Port: 8124
Document Path: /?n=35
Document Length: 8 bytes
Concurrency Level: 100
Time taken for tests: 10.509 seconds
Complete requests: 100
Failed requests: 0
Write errors: 0
Total transferred: 16500 bytes
HTML transferred: 800 bytes
Requests per second: 9.52 [#/sec](mean)
Time per request: 10508.755 [ms](mean)
Time per request: 105.088 [ms](mean, across all concurrent requests)
Transfer rate: 1.53 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 4 5 0.4 5 6
Processing: 336 3539 2639.8 2929 10499
Waiting: 335 3539 2639.9 2929 10499
Total: 340 3544 2640.0 2934 10504
Percentage of the requests served within a certain time (ms)
50% 2934
66% 3763
75% 4527
80% 5153
90% 8261
95% 9719
98% 10308
99% 10504
100% 10504 (longest request)
复制代码
经过和上面tagg2
包的测试结果对比,咱们发现区别很大。首先每秒处理的任务数从17.84 [#/sec]
降低到了9.52 [#/sec]
,这说明咱们web服务器总体的吞吐率降低了;而后每一个用户请求的平均等待时间也从56.058 [ms]
提升到了105.088 [ms]
,用户等待的时间也更长了。
最后咱们发现用户请求处理的时长很是的不均匀,50%的用户在2934 ms
内返回了,最慢的等待达到了10504 ms
。虽然咱们使用了cluster
启动了4个Node.js进程处理用户请求,可是对于每一个Node.js进程来讲仍是单线程的,因此当有4个用户跑满了4个Node.js的cluster
进程以后,新来的用户请求就只能等待了,最后形成了先到的用户处理时间短,后到的用户请求处理时间比较长,就形成了用户等待时间很是的不平均。
你们看到这里是否是开始心潮澎湃,感受js一统江湖的时代来临了,单线程异步非阻塞的模型能够胜任大并发,同时开发也很是高效,多线程下的js能够承担cpu密集型任务,不会有主线程阻塞而引发的性能问题。
可是,不论tagg
仍是tagg2
包都是利用phtread
库和v8
的v8::Isolate Class
类来实现js多线程功能的。
Isolate表明着一个独立的v8引擎实例,v8的Isolate拥有彻底分开的状态,在一个Isolate实例中的对象不可以在另一个Isolate实例中使用。嵌入式开发者能够在其余线程建立一些额外的Isolate实例并行运行。在任什么时候刻,一个Isolate实例只可以被一个线程进行访问,能够利用加锁/解锁进行同步操做。
换而言之,咱们在进行v8的嵌入式开发时,没法在多线程中访问js变量,这条规则将直接致使咱们以前的tagg2
里面线程执行的函数没法使用Node.js的核心api,好比fs
,crypto
等模块。如此看来,tagg2
包仍是有它使用的局限性,针对一些可使用js原生的大量计算或循环可使用tagg2,Node.js核心api由于没法从主线程共享对象的关系,也就不能跨线程使用了。
最后,若是咱们非要让Node.js支持多线程,仍是提倡使用官方的作法,利用libuv库来实现。
libuv是一个跨平台的异步I/O库,它主要用于Node.js的开发,同时他也被Mozilla's Rust language, Luvit, Julia, pyuv等使用。它主要包括了Event loops事件循环,Filesystem文件系统,Networking网络支持,Threads线程,Processes进程,Utilities其余工具。
在Node.js核心api中的异步多线程大可能是使用libuv
来实现的,下一章将带领你们开发一个让Node.js支持多线程并基于libuv的Node.js包。
在支持html5的浏览器里,咱们可使用webworker
来将一些耗时的计算丢入worker
进程中执行,这样主进程就不会阻塞,用户也就不会有卡顿的感受了。在Node.js中是否也可使用这类技术,保证主线程的通畅呢?
cluster
能够用来让Node.js充分利用多核cpu的性能,同时也可让Node.js程序更加健壮,官网上的cluster
示例已经告诉咱们如何从新启动一个由于异常而奔溃的子进程。
想要像在浏览器端那样启动worker
进程,咱们须要利用Node.js核心api里的child_process
模块。child_process
模块提供了fork
的方法,能够启动一个Node.js文件,将它做为worker
进程,当worker
进程工做完毕,把结果经过send
方法传递给主进程,而后自动退出,这样咱们就利用了多进程来解决主线程阻塞的问题。
咱们先启动一个web服务,仍是接收参数计算斐波那契数组:
var express = require('express');
var fork = require('child_process').fork;
var app = express();
app.get('/', function(req, res){
var worker = fork('./work_fibo.js') //建立一个工做进程
worker.on('message', function(m) {//接收工做进程计算结果
if('object' === typeof m && m.type === 'fibo'){
worker.kill();//发送杀死进程的信号
res.send(m.result.toString());//将结果返回客户端
}
});
worker.send({type:'fibo',num:~~req.query.n || 1});
//发送给工做进程计算fibo的数量
});
app.listen(8124);
复制代码
咱们经过express监听8124端口,对每一个用户的请求都会去fork一个子进程,经过调用worker.send方法将参数n传递给子进程,同时监听子进程发送消息的message事件,将结果响应给客户端。
下面是被fork的work_fibo.js文件内容:
var fibo = function fibo (n) {//定义算法
return n > 1 ? fibo(n - 1) + fibo(n - 2) : 1;
}
process.on('message', function(m) {
//接收主进程发送过来的消息
if(typeof m === 'object' && m.type === 'fibo'){
var num = fibo(~~m.num);
//计算jibo
process.send({type: 'fibo',result:num})
//计算完毕返回结果
}
});
process.on('SIGHUP', function() {
process.exit();//收到kill信息,进程退出
});
复制代码
咱们先定义函数fibo
用来计算斐波那契数组,而后监听了主线程发来的消息,计算完毕以后将结果send
到主线程。同时还监听process
的SIGHUP
事件,触发此事件就进程退出。
这里咱们有一点须要注意,主线程的kill
方法并非真的使子进程退出,而是会触发子进程的SIGHUP
事件,真正的退出仍是依靠process.exit()
;。
下面咱们用ab 命令测试一下多进程方案的处理性能和用户请求延迟,测试环境不变,仍是100个并发100次请求,计算斐波那切数组第35位:
Server Software:
Server Hostname: 192.168.28.5
Server Port: 8124
Document Path: /?n=35
Document Length: 8 bytes
Concurrency Level: 100
Time taken for tests: 7.036 seconds
Complete requests: 100
Failed requests: 0
Write errors: 0
Total transferred: 16500 bytes
HTML transferred: 800 bytes
Requests per second: 14.21 [#/sec](mean)
Time per request: 7035.775 [ms](mean)
Time per request: 70.358 [ms](mean, across all concurrent requests)
Transfer rate: 2.29 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 4 4 0.2 4 5
Processing: 4269 5855 970.3 6132 7027
Waiting: 4269 5855 970.3 6132 7027
Total: 4273 5860 970.3 6136 7032
Percentage of the requests served within a certain time (ms)
50% 6136
66% 6561
75% 6781
80% 6857
90% 6968
95% 7003
98% 7017
99% 7032
100% 7032 (longest request)
复制代码
压力测试结果QPS约为14.21,相比cluster来讲,仍是快了不少,每一个用户请求的延迟都很平均,由于进程的建立和销毁的开销要大于线程,因此在性能方面略低于tagg2,不过相对于cluster方案,这样的提高仍是令咱们满意的。
使用child_process
模块的fork
方法确实可让咱们很好的解决单线程对cpu密集型任务的阻塞问题,同时又没有tagg2
包那样没法使用Node.js核心api的限制。
可是若是个人worker
具备多样性,每次在利用child_process
模块解决问题时都须要去建立一个worker.js
的工做函数文件,有点麻烦。咱们是否是能够更加简单一些呢?
在咱们启动Node.js程序时,node命令能够带上-e这个参数,它将直接执行-e后面的字符串,以下代码就将打印出hello world。
node -e "console.log('hello world')"
复制代码
合理的利用这个特性,咱们就能够免去每次都建立一个文件的麻烦。
var express = require('express');
var spawn = require('child_process').spawn;
var app = express();
var spawn_worker = function(n,end){//定义工做函数
var fibo = function fibo (n) {
return n > 1 ? fibo(n - 1) + fibo(n - 2) : 1;
}
end(fibo(n));
}
var spawn_end = function(result){//定义工做函数结束的回调函数参数
console.log(result);
process.exit();
}
app.get('/', function(req, res){
var n = ~~req.query.n || 1;
//拼接-e后面的参数
var spawn_cmd = '('+spawn_worker.toString()+'('+n+','+spawn_end.toString()+'));'
console.log(spawn_cmd);//注意这个打印结果
var worker = spawn('node',['-e',spawn_cmd]);//执行node -e "xxx"命令
var fibo_res = '';
worker.stdout.on('data', function (data) { //接收工做函数的返回
fibo_res += data.toString();
});
worker.on('close', function (code) {//将结果响应给客户端
res.send(fibo_res);
});
});
app.listen(8124);
复制代码
代码很简单,咱们主要关注3个地方。
第1、咱们定义了spawn_worker
函数,他其实就是将会在-e
后面执行的工做函数,因此咱们把计算斐波那契数组的算法定义在内,spawn_worker
函数接收2个参数,第一个参数n表示客户请求要计算的斐波那契数组的位数,第二个end
参数是一个函数,若是计算完毕则执行end,将结果传回主线程;
第2、真正当Node.js脚步执行的字符串其实就是spawn_cmd
里的内容,它的内容咱们经过运行以后的打印信息,很容易就能明白;
第3、咱们利用child_process
的spawn
方法,相似在命令行里执行了node -e "js code"
,启动Node.js工做进程,同时监听子进程的标准输出,将数据保存起来,当子进程退出以后把结果响应给用户。
如今主要的焦点就是变量spawn_cmd
到底保存了什么,咱们打开浏览器在地址栏里输入:
http://127.0.0.1:8124/?n=35
复制代码
下面就是程序运行以后的打印信息,
(function (n,end){
var fibo = function fibo (n) {
return n > 1 ? fibo(n - 1) + fibo(n - 2) : 1;
}
end(fibo(n));
}(35,function (result){
console.log(result);
process.exit();
}));
复制代码
对于在子进程执行的工做函数的两个参数n
和end
如今一目了然,n
表明着用户请求的参数,指望得到的斐波那契数组的位数,而end
参数则是一个匿名函数,在标准输出中打印计算结果真后退出进程。
node -e
命令虽然能够减小建立文件的麻烦,但同时它也有命令行长度的限制,这个值各个系统都不相同,咱们经过命令getconf ARG_MAX
来得到最大命令长度,例如:MAC OSX
下是262,144
byte,而个人linux
虚拟机则是131072
byte。
大部分多线程解决cpu密集型任务的方案均可以用咱们以前讨论的多进程方案来替代,可是有一些比较特殊的场景多线程的优点就发挥出来了,下面就拿咱们最多见的http web服务器响应一个小的静态文件做为例子。
以express
处理小型静态文件为例,大体的处理流程以下:
etag
来肯定是否响应304
给客户端,让客户端继续使用本地缓存。buffer
中,为响应做准备。MIME
类型,若是是相似html
,js
,css
等静态资源,还须要gzip压缩以后传输给客户端下面是一个正常成功的Node.js处理静态资源无缓存流程图:
这个流程中的(2),(3),(4)步都经历了从js到C++ ,打开和释放文件,还有调用了zlib
库的gzip
算法,其中每一个异步的算法都会有建立和销毁线程的开销,因此这样也是你们诟病Node.js处理静态文件不给力的缘由之一。
为了改善这个问题,我以前有利用libuv库开发了一个改善Node.js的http/https处理静态文件的包,名为ifile,ifile包,之因此能够加速Node.js的静态文件处理性能,主要是减小了js和C++的互相调用,以及频繁的建立和销毁线程的开销,下图是ifile包处理一个静态无缓存资源的流程图:
因为所有工做都是在libuv
的子线程中执行的,因此Node.js主线程不会阻塞,固然性能也会大幅提高了,使用ifile包很是简单,它可以和express无缝的对接。
var express = require('express');
var ifile = require("ifile");
var app = express();
app.use(ifile.connect()); //默认值是 [['/static',__dirname]];
app.listen(8124);
复制代码
上面这4行代码就可让express
把静态资源交给ifile
包来处理了,咱们在这里对它进行了一个简单的压力测试,测试用例为响应一个大小为92kb
的jquery.1.7.1.min.js
文件,测试命令:
ab -c 500 -n 5000 -H "Accept-Encoding: gzip" http://192.168.28.5:8124/static/jquery.1.7.1.min.js
复制代码
因为在ab命令中咱们加入了-H "Accept-Encoding: gzip"
,表示响应的静态文件但愿是gzip压缩以后的,因此ifile
将会把压缩以后的jquery.1.7.1.min.js
文件响应给客户端。结果以下:
Server Software:
Server Hostname: 192.168.28.5
Server Port: 8124
Document Path: /static/jquery.1.7.1.min.js
Document Length: 33016 bytes
Concurrency Level: 500
Time taken for tests: 9.222 seconds
Complete requests: 5000
Failed requests: 0
Write errors: 0
Total transferred: 166495000 bytes
HTML transferred: 165080000 bytes
Requests per second: 542.16 [#/sec](mean)
Time per request: 922.232 [ms](mean)
Time per request: 1.844 [ms](mean, across all concurrent requests)
Transfer rate: 17630.35 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 49 210.2 1 1003
Processing: 191 829 128.6 870 1367
Waiting: 150 824 128.5 869 1091
Total: 221 878 230.7 873 1921
Percentage of the requests served within a certain time (ms)
50% 873
66% 878
75% 881
80% 885
90% 918
95% 1109
98% 1815
99% 1875
100% 1921 (longest request)
复制代码
咱们首先看到Document Length
一项结果为33016
bytes说明咱们的jquery
文件已经被成功的gzip
压缩,由于源文件大小是92kb
;其次,咱们最关心的Requests per second:542.16 [#/sec](mean)
,说明咱们每秒能处理542个任务;最后,咱们看到,在这样的压力状况下,平均每一个用户的延迟在1.844 [ms]。
咱们看下使用express框架处理这样的压力会是什么样的结果,express测试代码以下:
var express = require('express');
var app = express();
app.use(express.compress());//支持gzip
app.use('/static', express.static(__dirname + '/static'));
app.listen(8124);
复制代码
代码一样很是简单,注意这里咱们使用:
app.use('/static', express.static(__dirname + '/static'));
复制代码
而不是:
app.use(express.static(__dirname));
复制代码
后者每一个请求都会去匹配一次文件是否存在,而前者只有请求url是/static开头的才会去匹配静态资源,因此前者效率更高一些。而后咱们执行相同的ab压力测试命令看下结果:
Server Software:
Server Hostname: 192.168.28.5
Server Port: 8124
Document Path: /static/jquery.1.7.1.min.js
Document Length: 33064 bytes
Concurrency Level: 500
Time taken for tests: 16.665 seconds
Complete requests: 5000
Failed requests: 0
Write errors: 0
Total transferred: 166890000 bytes
HTML transferred: 165320000 bytes
Requests per second: 300.03 [#/sec](mean)
Time per request: 1666.517 [ms](mean)
Time per request: 3.333 [ms](mean, across all concurrent requests)
Transfer rate: 9779.59 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 173 539.8 1 7003
Processing: 509 886 350.5 809 9366
Waiting: 238 476 277.9 426 9361
Total: 510 1059 632.9 825 9367
Percentage of the requests served within a certain time (ms)
50% 825
66% 908
75% 1201
80% 1446
90% 1820
95% 1952
98% 2560
99% 3737
100% 9367 (longest request)
复制代码
一样分析一下结果,Document Length:33064 bytes
表示文档大小为33064
bytes,说明咱们的gzip起做用了,每秒处理任务数从ifile
包的542
降低到了300
,最长用户等待时间也延长到了9367 ms
,可见咱们的努力起到了立竿见影的做用,js和C++互相调用以及线程的建立和释放并非没有损耗的。
可是当我在express的谷歌论坛里贴上这些测试结果,并宣传ifile
包的时候,express的做者TJ,给出了不同的评价,他在回复中说道:
请牢记你可能不须要这么高等级吞吐率的系统,就算是每个月百万级别下载量的npm网站,也仅仅每秒处理17个请求而已,这样的压力甚至于PHP也能够处理掉(又黑了一把php)。
确实如TJ所说,性能只是咱们项目的指标之一而非所有,一味的去追求高性能并非很理智。
ifile包开源项目地址:https://github.com/DoubleSpout/ifile
单线程的Node.js给咱们编码带来了太多的便利和乐趣,咱们应该时刻保持清醒的头脑,在写Node.js代码中切不可与PHP混淆,任何一个隐藏的问题均可能击溃整个线上正在运行的Node.js程序。
单线程异步的Node.js不表明不会阻塞,在主线程作过多的任务可能会致使主线程的卡死,影响整个程序的性能,因此咱们要很是当心的处理大量的循环,字符串拼接和浮点运算等cpu密集型任务,合理的利用各类技术把任务丢给子线程或子进程去完成,保持Node.js主线程的畅通。
线程/进程的使用并非没有开销的,尽量减小建立和销毁线程/进程的次数,能够提高咱们系统总体的性能和出错的几率。
最后请不要一味的追求高性能和高并发,由于咱们可能不须要系统具备那么大的吞吐率。高效,敏捷,低成本的开发才是项目所须要的,这也是为何Node.js可以在众多开发语言中脱颖而出的关键。