Node.js Core

设计高性能Web服务器的要点在于非阻塞I/O和事件驱动

Node最大的特点是异步式I/O(非阻塞I/O)与事件紧密结合的编程模式,此模式与传统同步式I/O线性的编程思维不同,因为控制流很大程度要依靠事件和回调函数来组织,一个逻辑要拆分为若干单元。

Node是JavaScript运行时环境,为JavaScript提供了一个异步I/O编程框架。Node的指导思想:CPU执行指令是非常快速的,但I/O操作相对而言是极其缓慢的。Node解决的是给CPU执行的算法容易,I/O请求却很频繁的情况。

同步/异步

当请求到来时,相对于传统的进程或线程同步处理的方式。Node只在主线程中处理请求,如果遇到I/O操作则以异步方式发起调用,主线程立即返回,继续处理之后的任务。由于异步,一次客户端请求的处理方式由流式变为阶段式。而使用Node编写的JavaScript代码都运行在主线程中。

例如:假设一次客户端请求分为三个阶段

  1. 执行函数A
  2. 一次I/O操作
  3. 执行函数B

同步式处理

//同步式处理客户端请求
function request(){
  $result = stageA();//执行函数A
  $data = readfile();//读取文件,执行一次I/O操作
  stageB($result, $data);//执行函数B,将前两部的结果作为参数。
}
同步式处理流程

同步式处理中每个请求使用一个线程或进程处理,一次请求处理完毕后线程被回收。上图同步式处理只显示了两个线程。如果客户端更多,线程数量会随之增加。

异步式处理

//异步式处理流程
function request(){
  var result = stageA();//执行函数A
  // 发起异步读取,此时主线程立即返回,处理后续任务。
  readfileAsync(function(data){
    //在随后的循环中执行回调函数
    stageB(result, data);
  });
}
异步式处理

Node异步执行中,Node使用一个主线程解决了所有问题,异步处理流程中,每一个方块代表了一个阶段任务的执行。

Node高性能的来源得益于异步的运行方式,如何理解异步对性能的性能的提升了。打个比方目前出入车辆管理规定,外地来的车辆进京需要办理进京证,而办进京证需等待一定时间。如果每个人都自己跑去办理,就好像开启多个线程同步处理,办理窗口有限,就得排队。而把这件事委托给第三方,就好比不开启线程或进程,将耗时的I/O请求委托给操作系统。这种情况下人们从办证的任务中解放出来,因而能继续做其他事情。若来了一个任务,交给第三方去处理,则第三方就有一个接单队列,只需要拿到所有的单,去办理地点逐个办理即可。


操作系统中一个杰出的设计是线程,操作系统把CPU处理时间分片后划分出许多短暂的时间片,在时间T1执行一个线程的指令,到时间T2再执行下一个线程的指令,各个线程轮流执行,结果好像是所有线程都在并行前进。这样,编程时可以创建多个线程,在同一期间执行,各个线程可以并行的完成不同的任务。

同步与异步

在单线程中,计算机是一台严格意义上的冯诺依曼式机器,一段代码调用了另一段代码时,只能采用同步调用。简单俩说,必须等待这段代码执行完毕并返回结果后,调用方才能继续向下执行。

有了多线程的支持后,可以采用异步调用,也就是说,调用方和被调方可以属于不同的线程,调用方启动被调方线程后,不等待对象返回结果就继续执行后续代码。

计算中有些处理时比较耗时的,调用这种处理代码时,调用方如果苦苦等待会严重影响程序的性能。

异步调用虽然原理并不复杂,但在使用中容易出现莫名其妙的问题,特别是不同线程共享代码或贡献数据时容易出现问题。因此,要设计一个安全高效的的编程方式需要比较多的设计经验,因此最好不要滥用异步。


JavaScript的异步处理上,ES5的回调函数callback使我们陷入地狱,ES6的承诺promise使我们脱离魔障,ES7的异步等待async-await终于带领我们走向了光明。其实async-awaitpromisegenerator的语法糖,是为了编码时更加流畅,同时增强代码的可读性。async用来表示函数是异步的,使用async定义的函数会返回一个promise对象,因此可使用then方法添加回调函数。await 可以理解为async wait的缩写,await必须出现在async函数内部,因此它是不能够单独使用的。await后面可以跟任何JS表达式,作用是用来等待promise对象的状态被resolved。如果await异步等待的是promise对象则会造成异步函数停止执行并且等待promise的解决,如果等待的是正常的表达式则会立即执行。

非阻塞I/O

什么是阻塞(block)呢?线程在执行中若遇到磁盘读写或网络通信(统称为I/O操作),通常要耗费较长的时间,此时操作系统会剥夺这个线程的CPU控制权,使其暂停执行,同时将资源让渡给其他工作线程,这种线程调度的方式称为阻塞。

  • 传统同步式I/O(Synchronous I/O)或阻塞式I/O(Blocking I/O)
  • 异步式I/O(Asynchronous I/O)或非阻塞式I/O(Non-blocking I/O)

当I/O操作完毕后,操作系统将这个线程的阻塞状态解除,恢复其对CPU的控制权,令其继续执行。这种I/O模式即传统的同步式I/O(Synchronous I/O)或阻塞式I/O(Blocking I/O)。

异步式I/O(Asynchronous I/O)或非阻塞式I/O(Non-blocking I/O)则针对所有I/O操作不采用阻塞的策略。当线程遇到I/O操作时,不会阻塞的方式等待I/O操作的完成或数据的返回,而只是将I/O请求发送给操作系统,继续执行下一条语句。当操作系统完成I/O操作时,以事件的形式通知执行I/O操作的线程,线程会在特定时候处理这个事件。为了处理异步I/O,线程必须有事件循环,不断地检查有没有未处理的事件,依次予以处理。

阻塞模式下,一个线程只能处理一项任务,要想提高吞吐量必须通过多线程。而非阻塞模式下,一个线程永远在执行计算操作,这个线程所使用的CPU核心利用率永远是100%,I/O以事件的方式通知。

为什么Node.js使用单线程、非阻塞的事件编程模型?

在阻塞模式下,多线程往往能提高系统吞吐量,因为一个线程阻塞时还有其他线程在工作,多线程可让CPU资源不被阻塞中的线程浪费。而在非阻塞模式下,线程不会被I/O阻塞,永远在利用CPU。多线程带来的好处仅仅是在多核CPU的情况下利用更多的核,而Node.js的单线程也能带来同样的好处。这就是为什么Node.js使用单线程、非阻塞的事件编程模型。

多线程同步式I/O
单线程异步式I/O

单线程事件驱动的异步式I/O比传统的多线程阻塞式I/O好在哪里呢?

简而言之,异步式I/O就是少了多线程的开销。对操作系统来说,创建一个线程的代价是十分昂贵的,需要给它分配内存、列入调度,同时在线程切换时还需执行内存换页,CPU的缓存被清空,切换回来时还要重新从内存中读取数据,破坏了数据的局部性。

同步异步

异步I/O

关于异步I/O典型的场景是AJAX调用,其收到响应在是发送AJAX结束之后输出的。在调用AJAX后,后续代码时被立即执行的,而收到响应的执行时间是不被预期的。我们只知道将在这个异步请求结束后执行,但并不知道具体的时间点。异步调用中对于结果值的捕获是符合“Don't call me, I will call you.”的原则的,这也是注重结果不关心过程的一种表现。

$.post(url, data, function(res){
  console.log('收到响应');
});
console.log('发送AJAX结束');
AJAX调用

Node中异步I/O非常常见,以读取文件为例。

var fs = require('fs');
fs.readFile(path, function(err,res){
  console.log('文件读取完毕');
});
console.log('发起文件读取');
异步I/O

Node.js为什么使用单线程?

Node.js保持了JS在浏览器中单线程的特点,在Node中JS与其余线程是无法共享任何状态的。单线程最大好处是不用像多线程编程那样处处在意状态的同步问题,这里没有死锁的存在,也没有线程上下文交换带来的性能上的开销。

同样,单线程也有自身的弱点,具体表现在

  • 无法利用多核CPU
  • 错误会引起整个应用退出,应用的健壮性值得考验。
  • 大量计算占用CPU导致无法继续调用异步I/O

像浏览器中的JS与UI公用一个线程一样,JS长时间执行会导致UI的渲染和响应被中断。在Node中,长时间的CPU占用也会导致后续的异步I/O发不出调用,已完成的异步I/O的回调函数也会得不到及时执行。

最早解决这种大计算问题的方案是Google公司开发的Gears,它启用了一个完全能独立的进程,将需要计算的程序发送给这个进程,在结果得出后,通过事件将结果传递回来。这个模型将计算分发到其他进程上,以次来降低运算造成阻塞的几率。

后台H5制定了Web Workers的标准,Google放弃了Gears,全力支持Web Workers。Web Workers能够创建工作线程来进行计算,以解决JS大计算阻塞UI渲染的问题。工作线程为了不阻塞主线程,通过消息传递的方式来传递运行结果,这也使得工作进程不能访问主线程中的UI。

Node采用了与Web Workers相同的思路来解决单线程中大量计算的问题(child_process)。子进程的出现,意味着Node可从容地应对单线程在健壮性和无法利用多核CPU方面的问题。通过将计算分发到各个子进程,可将大量计算分解掉,然后在通过进程之间的事件消息来传递结果,这可以很好地保持应用模型的简单和低依赖。通过Master-Worker的管理方式,也可很好地管理各个工作进程,以达到更高的健壮性。

关于如何通过子进程来充分利用硬件资源和提升应用的健壮性,这是一个值得探究的话题。

事件循环

Node.js所有的异步I/O操作在完成时都会发送一个事件到事件队列。从开发看来事件由EventEmitter对象提供。

Node.js在什么时候会进入事件循环呢?Node.js程序由事件循环开始到事件循环结束,所有的逻辑都是事件的回调函数,所以Node.js始终在事件循环中,程序入口就是事件循环第一个事件的回调函数。事件的回调函数在执行过程中,可能会发出I/O请求或直接发射(emit)事件,执行完毕后再返回事件循环,事件循环会检查事件队列中有没有未处理的事件,直至程序结束。

事件循环机制

Node.js没有显式的事件循环,它对开发者不可见,由libev库实现。libev支持多种类型的事件,如ev_io、ev_timer、ev_signal、ev_idle等,在Node.js中均被EventEmitter封装。libev事件循环的每一次迭代,在Node.js中就是一次Tick,libev不断检查是否有活动的、可供检测的事件监听器,直至检测不到时才退出事件循环,进程结束。

异步I/O与事件驱动

Node.js采用异步式I/O与事件驱动的设计,对于高并发的解决方案,传统采用多线程模型,也就是为每个业务逻辑提供一个系统线程,通过系统线程切换来弥补同步式I/O调用时的时间开销。

Node.js采用单线程模型,对于所有I/O都采用异步式的请求方式,避免频繁的上下文切换。Node在执行过程中会维护一个时间队列,程序在执行时进入事件循环等待下一个事件到来,每个异步式I/O请求完成后会被推送到事件队列,等待程序进程进行处理。


事件循环

Node.js的异步机制是基于事件的,所有磁盘I/O、网络通信、数据库查询都以非阻塞的方式请求,返回的结果由事件循环来处理。

Node.js进程在同一时刻只会处理一个事件,完成后立即进入事件循环检查并处理后续的事件。其好处是CPU和内存在同一时刻集中处理一件事,同时尽可能让耗时的I/O操作并行执行。对于低速连接攻击,Node.js只是在事件队列中增加请求,等待操作系统的回应,因而不会有任何多线程开销,很大程度可提高Web应用的健壮性,防止恶意攻击。

异步事件模式的弊端是不符合开发者的常规线性思路,需要把一个完整的逻辑拆分为一个个事件,增加了开发和调试的难度。

Node技术架构
Node基于libuv实现跨平台的架构
Node与PHP的架构比较
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,757评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,478评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,540评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,593评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,903评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,329评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,659评论 2 309
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,383评论 0 195
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,055评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,337评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,864评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,227评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,820评论 3 231
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,999评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,750评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,365评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,260评论 2 258

推荐阅读更多精彩内容