一个前端工程师眼里的NodeJS

JavaScript单线程的误解

  在我接触JavaScript(无论浏览器还是NodeJS)的时间里,总是遇到有朋友有多线程的需求。而在NodeJS方面,有朋友甚至直接说到,NodeJS是单线程的,无法很好的利用多核CPU。

  诚然,在前端的浏览器中,由于前端的JavaScript与UI占据同一线程,执行JavaScript确实为UI响应造成了一定程度上的麻烦。但是,除非用到超大的循环语句执行JavaScript,或是用阻塞式的Ajax,或是太过频繁的定时器执行外,JavaScript并没有给前端应用带来明显的问题,所以也很少有朋友抱怨JavaScript是单线程而不能很好利用多核CPU的问题,因为没有因此出现性能瓶颈。

  但是,我们可以用Ajax和Web Worker回应这个误解。当Ajax请求发送之后,除非是同步请求,否则其余的JavaScript代码会很快被执行到。在Ajax发送完成,直到接收到响应的这段时间里,这个网络请求并不会阻塞JavaScript的执行,而网络请求已经发生,这是必然的事。那么,答案就很明显了,JavaScript确实是执行在单线程上的,但是,整个Web应用执行的宿主(浏览器)并非以单线程的方式在执行。而Web Worker的诞生,就是直接为了解决JavaScript与UI占用同一线程造成的UI响应问题的,它能新开一条线程去执行JavaScript。

  同理,NodeJS中的JavaScript也确实是在单线程上执行,但是作为宿主的NodeJS,它本身并非是单线程的,NodeJS在I/O方面又动用到一小部分额外的线程协助实现异步。程序员没有机会直接创建线程,这也是有的同学想当然的认为NodeJS的单线程无法很好的利用多核CPU的原因,他们甚至会说,难以想象由多人一起协作开发一个单线程的程序。

  NodeJS封装了内部的异步实现后,导致程序员无法直接操作线程,也就造成所有的业务逻辑运算都会丢到JavaScript的执行线程上,这也就意味着,在高并发请求的时候,I/O的问题是很好的解决了,但是所有的业务逻辑运算积少成多地都运行在JavaScript线程上,形成了一条拥挤的JavaScript运算线程。NodeJS的弱点在这个时候会暴露出来,单线程执行运算形成的瓶颈,拖慢了I/O的效率。这大概可以算得上是密集运算情况下无法很好利用多核CPU的缺点。这条拥挤的JavaScript线程,给I/O形成了性能上限。

  但是,事情又并非绝对的。回到前端浏览器中,为了解决线程拥挤的情况,Web Worker应运而生。而同样,Node也提供了child_process.fork来创建Node的子进程。在一个Node进程就能很好的解决密集I/O的情况下,fork出来的其余Node子进程可以当作常驻服务来解决运算阻塞的问题(将运算分发到多个Node子进程中上去,与Apache创建多个子进程类似)。当然child_process/Web Worker的机制永远只能解决单台机器的问题,大的Web应用是不可能一台服务器就能完成所有的请求服务的。拜NodeJS在I/O上的优势,跨OS的多Node之间通信的是不算什么问题的。解决NodeJS的运算密集问题的答案其实也是非常简单的,就是将运算分发到多个CPU上。请参考文章后的multi-node的性能测试,可以看到在多Node进程的情景下,响应请求的速度被大幅度提高。

  在文章的写作中,Node最新发布的0.5.10版本新增了cluster启动参数。参数的使用方式如下:

node cluster server.js

  启动Node的时候,在附加了该参数的情况下,Node会检测机器上的CPU数量来决定启动多少进程实例,这些实例会自动共享相同的侦听端口。详情请参阅官方文档。在之前的解决运算密集问题中,工程师需要multi-node这样的库或者其他方案去手动分发请求,在cluster参数的支持下,可以释放掉工程师在解决此问题上的大部分精力。

  事件式编程

  延续上一节的讨论。我们知道NodeJS/JavaScript具有异步的特性,从NodeJS的API设计中可以看出来,任何涉及I/O的操作,几乎都被设计成事件回调的形式,且大多数的类都继承自EventEmitter。这么做的好处有两个,一个是充分利用无阻塞I/O的特性,提高性能;另一个好处则是封装了底层的线程细节,通过事件消息留出业务的关注点给编程者,从而不用关注多线程编程里牵扯到的诸多技术细节。

  从现实的角度而言,事件式编程也更贴合现实。举一个业务场景为例:家庭主妇在家中准备中餐,她需要完成两道菜,一道拌黄瓜,一道西红柿蛋汤。以PHP为例,家庭主妇会先做完拌黄瓜,接着完成西红柿蛋汤,是以顺序/串行执行的。但是现在突然出了一点意外,凉拌黄瓜需要的酱油用光了,需要她儿子出门帮她买酱油回来。那么PHP家庭主妇在叫她儿子出门打酱油的这段时间都是属于等待状态的,直到酱油买回来,才会继续下一道菜的制作。那么,在NodeJS的家庭主妇又会是怎样一个场景呢,很明显,在等待儿子打酱油回来的时间里,她可以暂停凉拌黄瓜的制作,而直接进行西红柿蛋汤的过程,儿子打完酱油回来,继续完成她的凉拌黄瓜。没有浪费掉等待的时间。实例伪代码如下:

var mother = new People();
var child = new People();
child.buySoy(function (soy) {
    mother.cook("cucumber", soy);
});
mother.cook("tomato");

  接下来,将上面这段代码转换为基于事件/任务异步模式的代码:

var proxy = new EventProxy();
var mother = new People();
proxy.bind("cook_cucumber", function (soy) {
    mother.cook("cucumber", soy);
});
proxy.bind("cook_tomato", function () {
    mother.cook("tomato");
});
var child = new People();
child.buySoy(function (soy) {
    proxy.trigger("cucumber", soy);
});
proxy.trigger("tomato");

  代码量多了很多,但是业务逻辑点都是很清楚的:通过bind方法预定义了cook_cucumber和cook_tomato两个任务。这里的bind方法可以认为是await的消息式实现,需要第一个参数来标识该任务的名字,流程在执行的过程中产生的消息会触发这些任务执行。可以看出,事件式编程中,用户只需要关注它所需要的几个业务事件点就可以,中间的等待都由底层为你调配好了。这里的代码只是举例事件/任务异步模式而用,在简单的场景中,第一段代码即可。做NodeJS的编程,会更感觉是在做现实的业务场景设计和任务调度,没有顺序保证,程序结构更像是一个状态机。

  个人觉得在事件式编程中,程序员需要转换一下思维,才能接受和发挥好这种异步/无阻塞的优势。同样,这种事件式编程带来的一个问题就在于业务逻辑是松散和碎片式的。这对习惯了顺序式,Promise式编程的同学而言,接受它是比较痛苦的事情,而且这种散布的业务逻辑对于非一开始就清楚设计的人而言,阅读存在相当大的问题。

  我提到事件式编程更贴近于现实生活,是更自然的,所以这种编程风格也导致你的代码跟你的生活一样,是一件复杂的事情。幸运的是,自己的生活要自己去面对,对于一个项目而言,并不需要每个人都去设计整个大业务逻辑,对于架构师而言,业务逻辑是明了的,借助事件式编程带来的业务逻辑松耦合的好处,在设定大框架后,将业务逻辑划分为适当的粒度,对每一个实现业务点的程序员而言,并没有这个痛苦存在。二八原则在这个地方非常有效。

  深度嵌套回调问题

  JavaScript/NodeJS对单个异步事件的处理十分容易,但容易出现问题出现的地方是“多个异步事件之间的结果协作”。以NodeJS服务端渲染页面为例,渲染需要数据,模板,本地化资源文件,这三个部分都是要通过异步来获取的,原生代码的写法会导致嵌套,因为只有这样才能保证渲染的时候数据,模板,本地化资源都已经获取到了。但问题是,这三个步骤之间实际是无耦合的,却因为原生代码没有promise的机制,将可以并行执行(充分利用无阻塞I/O)的步骤,变成串行执行的过程,直接降低了性能。代码如下:

var render = function (template, data) {
    _.template(template, data);
};
$.get("template", function (template) { // something
    $.get("data", function (data) { // something
        $.get("l10n", function (l10n) { // something
            render(template, data);
        });
    });
});

  面对这样的代码,许多工程师都表示不爽。这个弱点也形成了对NodeJS推广的一个不大不小的障碍。对于追求性能和维护性的同学,肯定不满足于以上的做法。本人对于JavaScript的事件和回调都略有偏爱,并且认为事件,回调,并行,松耦合是可以达成一致的。以下一段代码是用EventProxy实现的:

var proxy = new EventProxy();
var render = function (template, data, l10n) {
    _.template(template, data);
};
proxy.assign("template", "data", "l10n", render);
$.get("template", function (template) { // something
    proxy.trigger("template", template);
});
$.get("data", function (data) { // something
    proxy.trigger("data", data);
});
$.get("l10n", function (l10n) { // something
    proxy.trigger("l10n", l10n);
});

  代码量看起来比原生实现略多,但是从逻辑而言十分清晰。模板、数据、本地化资源并行获取,性能上的提高不言而喻,assign方法充分利用了事件机制来保证最终结果的正确性。在事件,回调,并行,松耦合几个点上都达到期望的要求。

  关于更多EventProxy的细节可参考其官方页面

  深度回调问题的延伸

  EventProxy解决深度回调的方式完全基于事件机制,这需要建立在事件式编程的认同上,那么必然也存在对事件式编程不认同的同学,而且习惯顺序式,promise式,向其推广bind/trigger模式实在难以被他们接受。JscexStreamline.js是目前比较成熟的同步式编程的解决方案。可以通过同步式的思维来进行编程,最终执行的代码是通过编译后的目标代码,以此通过工具来协助用户转变思维。

  结语

  对于优秀的东西,我们不能因为其表面的瑕疵而弃之不用,总会有折衷的方案来满足需求。NodeJS在实时性方面的功效有目共睹,即便会有一些明显的缺点,但是随着一些解决方案的出现,相信没有什么可以挡住其前进的脚步。

  附录(多核环境下的并发测试)

  服务器环境:

  • 网络环境:内网
  • 压力测试服务器:
  • 服务器系统:Linux 2.6.18
  • 服务器配置:Intel(R) Xeon(TM) CPU 3.40GHz 4 CPUS
  • 内存:6GB
  • NodeJS版本: v0.4.12

  客户端测试环境:

  • 发包工具:apache 2.2.19自带的ab测试工具
  • 服务器系统:Linux 2.6.18
  • 服务器配置:Pentium(R) Dual-Core CPU E5800 @ 3.20GHz 2CPUS
  • 内存:1GB

  单线程Node代码:

var http = require(‘http‘);
var server = http.createServer(function (request, response) {
    var j = 0;
    for (var i = 0; i & lt; 100000; i++) {
        j += 2 / 3;
    }
    response.end(j + ‘‘);
});
server.listen(8881);
console.log(‘Server running at http://10.1.10.150:8881/‘);

  四进程Node代码:

var http = require(‘http‘);
var server = http.createServer(function (request, response) {
    var j = 0;
    for (var i = 0; i & lt; 100000; i++) {
        j += 2 / 3;
    }
    response.end(j + ‘‘);
});
var nodes = require("./lib/multi-node").listen({
    port: 8883,
    nodes: 4
}, server);
console.log(‘Server running at http://10.1.10.150:8883/‘);

  这里简单介绍一下multi-node这个插件,这个插件就是利用require("child_process").spawn()方法来创建多个子线程。由于浮点计算和字符串拼接都是比较耗CPU的运算,所以这里我们循环10W次,每次对j加上0.66666。最后比较一下,开多子进程node到底比单进程node在CPU密集运算上快多少。

  以下是测试结果:


Comm.


500/30


500/30


1000/30


1000/30


3000/30


3000/30


Type


单进程


多子进程


单进程


多子进程


单进程


多子进程


RPS


2595


5597


2540


5509


2571


5560


TPQ


0.38


0.18


0.39


0.19


0.39


0.18


80% REQ


72


65


102


85


157


142


Fail


0


0


0


0


0


0

  说明:

  • 单进程:只开一个node.js进程。
  • 多子进程:开一个node.js进程,并且开3个它的子进程。
  • 3000/30:代表命令 ./ab -c 3000 -t 30 http://10.1.10.150:8888/。3000个客户端,最多发30秒,最多发5W个请求。
  • RPS:代表每秒处理请求数,并发的主要指标。
  • TPQ:每个请求处理的时间,单位毫秒。
  • Fail:代表平均处理失败请求个数。
  • 80% Req:代表80%的请求在多少毫秒内返回。

  从结果及图1~3上看:开多个子进程可以显著缓解node.js的CPU利用率不足的情况,提高node.js的CPU密集计算能力。

  图1:单个进程的node.js在压力测试下的情况,无法充分利用4核CPU的服务器性能。

  图2:多进程node,可以充分利用4核CPU榨干服务器的性能。

  图3:多子进程截图,可以看到一共跑了4个进程。

时间: 2024-08-06 15:25:53

一个前端工程师眼里的NodeJS的相关文章

作为一个前端工程师,CSS是最基本的入门课,你都能做出来吗?

CSS在很多程序员看来那只不过是雕虫小技, 实际上CSS3发展到今天已经强大到超乎你的想象, 当然对于低端浏览器使用者来说,还是以前那样——囧 有时候搞CSS开发确实需要很多想象力,下面一组纯CSS3特效足以描述这一点 以下内容转载自[http://design.yesky.com/show/466/11497966_3.shtml] 设计欣赏:纯CSS3「绘制」的图形图标 IE 标志 作者: Andreas Jacob 在 Firefox 3.6+ 以及Safari 5 上显示最佳.使用了圆角

一个前端工程师到底需要掌握哪些技能?

作为一名前端想要晋升,需要什么条件? 现在在用 React,要不要也学学 Vue? 有必要学习 Node.js/Flutter/ 函数式吗? 这几个问题看似毫无关联,但是其实它们本质上都是同一个问题,这个问题就是"一个前端工程师到底需要掌握哪些技能?" 其实在行业里面,对前端工程师的能力模型有一个基本的认知,不同公司的定义可能有细微差别,但是它的内核是一致的. 这里我给出一张图,是我自己理解的前端工程师的技能模型. 为了方便你理解,我把这些技能具体放进前端工程师的不同级别里,你可以对照

做为一个前端工程师,是往node方面转,还是往HTML5方面转

文章背景:问题本身来自于知乎,但是我感觉这个问题很典型,有必要把问题在整理一下,重新分享出来. 当看到这个问题之前,我也碰到过很多有同样疑惑的同学,他们都有一个共同的疑问该学php还是nodejs,包括我自己也曾有过同样的经历.幸运的是,通过我内心的挣扎和对当前市场供需关系以及行业发展前景的综合衡量评估后,我还是在接触了java, php之后回到js这片天空上来了.一方面是客观因素,一方面是兴趣导致的,前端太好玩了,值得为此奉献一点激情或青春. 其实这也是一个开发者成长必须经历的一个洗礼,一个磨

如何称为一个优秀的前端工程师?

最近在实验室很受打击,导师给的任务没有很好的完成,我能感受到导师的失望,同时对自己也失去了自信心.自信心是非常重要的一件事情.所以我需要思考我到底在不就的将来想从事什么工作.最近也是在一直不断地摸索中. 这几天想学前端,我也知道像我这样浮躁是做不成事情的,我尽量能够让自己稳定下来.希望好好学习前端吧. 首先我认为需要明白前端是什么,我认为的前端,是与人交互的接口.这是我当前的理解.在计算机中功能模块的解耦在有些地方能简化开发,使得产品容易维护等优点.所以现在讲究前端后端的分离.在前端中,HTML

前端工程师如何快速的开发一个微信JSSDK应用

亲们,订阅号出来已经很久了,作为一个前端工程师或者全栈工程师,你是不是错过了什么?大概许多攻城狮同砚还没有反应过来订阅号怎么回事,就马上要被微信的应用号秀一脸了.在应用号还没有正式出来之前,我们赶紧一起来看看怎样给自己的订阅号加个网页功效吧. 一.订阅号网页与平凡的HTML5网页的区别 可能会有很多同学还没有弄清楚普通的HTML网页与订阅号网页的差别,我在这里简朴的说明一下. 订阅号的网页就是微信的网页,普通的网页是W3C范例下的网页. 订阅号的网页通常是以遵守W3C的网页规范为前提的,但是也有

一个web前端工程师到底需要掌握哪些技能?有何晋升?

对于前端基础需要学习哪些内容,之前文章已经有写过不少了,本篇重在谈论: 作为一名前端想要晋升,需要什么条件?现在在用 React,要不要也学学 Vue?有必要学习 Node.js/Flutter/ 函数式吗?这几个问题看似毫无关联,但是其实它们本质上都是同一个问题,这个问题就是"一个前端工程师到底需要掌握哪些技能?" 其实在行业里面,对前端工程师的能力模型有一个基本的认知,不同公司的定义可能有细微差别,但是它的内核是一致的. 这里我给出一张图,是我自己理解的前端工程师的技能模型. 为了

前端工程师养成记:开发环境搭建(Sublime Text必备插件推荐)

为了让自己更像一个前端工程师,决定从开发环境开始武装自己.本文将介绍前段工程师开发的一些利器的安装步骤,主要包括了: 1.Node.js的安装 2.Grunt的安装及常用插件 3.Sublime Text的安装及必备插件 一.Node.js的安装 Node.js就是一堆前端工程师捧红的,所以装上这个嘛,主要不是自己需要使用Node.js而是一堆工具对他的依赖. Windows下安装步骤很简单: 1.去到http://nodejs.org/下载最新的安装包,安装. 2.在CMD下运行,node和n

前端工程师的技术进阶点,月薪5万难吗?难!

单纯讲技术进阶点意义不大,脱离场景都是耍流氓.我举个实际例子,今天的阿里大文娱优土,阿里接管后,底层替换差不多了,由内容为王转变为产品技术驱动. 这种情况下,前端如何进阶呢?业务很多,历史问题很多,老板迫切希望创新,赶超对手. 端上,我有pc.h5.小程序,播放器 api代理层,我有node,可以快速实现api保证,1.内部有直接用,2)内部没有,自己写,3)不够用,我自己包装,不会让api层限制我的发展 历史问题,我可以快速的想办法吃掉,尤其是老的php,这部分要做的是1)稳定改造2)提高团队

初级前端工程师

做为一个专职的页面重构者,我们从事的工作简单的说就是“将设计稿转换成WEB页面”,这一过程可以很简单到直接把PSD从PS里导出成网页:也可复杂到需要考虑页面中每个标签的使用,考虑“页面性能”.以“前端工程师”为目标的同学可能会不愿承认将页面重构这块分出来,但随着工种的细分,加上页面重构本身的专业性,独立为一个职业也不是不可能,至少我现在从事的就是一个专职的职位.如果你觉得一个前端工程师必须去画设计稿,可以不理会下面的内容. 单纯的页面重构,所涉及到的工作内容一般是“分析设计稿=>切图=>写HT