理解NodeJS 实现高并发原理

前言

我们都知道Node.js 给我们的标签是:非阻塞I/O、事件驱动、高效、轻量,这也是官网的描述。

Node.js® is a JavaScript runtime built on Chrome’s V8 JavaScript engine. Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient.

大家刚接触Node.js的时候,可能会有这样的疑惑:
1、浏览器运行的Javascript怎么 能与操作系统进行如此底层的交互?
2、nodejs 真的是单线程吗?
3、如果是单线程,他是如何实现高并发请求的?
4、nodejs的 事件驱动是如何实现的?

为了回答这些问题,我将从下面几个方面对NodeJS进行讲解:

  • Node.js的诞生以及简介
  • Node.js单线程实现高并发原理
    单线程
    非阻塞I/O (non-blocking I/O)
    事件驱动/事件循环
  • Node.js适用场景

Node.js的诞生

Ryan Dahl

Ryan Dahl,高性能Web服务器的专家,为了解决Web 服务器的高并发性能问题,几经探索,几经挫折之后,他觉得解决问题的关键是通过事件驱动和异步I/O来达成目的,但是当时没有很好工具。
在他快绝望的时候,V8引擎来了。2008年Google发明了Chrome浏览器,使用V8引擎来解析Js程序,非常快,并且V8引擎性能好,都是异步I/O, 闭包特性方便。V8满足他关于高性能Web服务器的想象:

  1. 历史遗留问题少,都是异步I/O
  2. 强大的编译和快速执行效率(通过运用大量算法和技巧)
  3. 使用V8引擎来解析Js程序,非常快,性能足够好,执行效率远超Python和ruby等脚本语言
  4. JavaScript语言的闭包特性非常方便。

2009年的2月,按新的想法他提交了项目的第一行代码,这个项目的名字最终被定名为“node”。
2009年5月,Ryan Dahl正式向外界宣布他做的这个项目。
2009年底,Ryan Dahl在柏林举行的JSConf EU会议上发表关于Node.js的演讲,之后Node.js逐渐流行于世。

Node.js的简介

  • Node.js 是一个构建在Chrome浏览器V8引擎上的JavaScript 运行环境
    1. 底层是Chrome V8引擎 , 使用C++开发的。
    2. V8引擎本身就是用于Chrome浏览器的JS解释部分,但是Ryan Dahl把V8搬到了服务器上,用于做服务器的软件。

  • Node.js 使用了事件驱动、非阻塞I/O模型,这些都使它轻量、好用。

  • Node.js 的包生态(npm), 是世界上最大的开源库生态系统

  • Node.js 自身哲学,是花最小的硬件成本,追求更高的并发,更高的处理性能

  • Node.js是一个让JavaScript运行在服务器端的开发平台,它让JavaScript的触角伸到了服务器端,可以与PHP、JSP、Python、Ruby平起平坐。
  • Node.js不是一种独立的语言,与PHP、ASP.Net、JSP、Python、Perl、Ruby的“既是语言,也是平台”不同,Node.js的使用JavaScript进行编程,运行在Chrome V8引擎上。
  • 与PHP、JSP等相比(PHP、ASP.Net、JSP、.net都需要运行在服务器程序上),Node.js跳过了Apache、Nginx、IIS等HTTP服务器,它自己不用建设在任何服务器软件之上。Node.js的许多设计理念与经典架构(LAMP = Linux + Apache + MySQL + PHP)有着很大的不同,可以提供强大的伸缩能力。

要想理解NodeJS实现高并发的原理,我们必须先了解一下NodeJS的底层架构。

Nodejs 架构分析

Nodejs 架构分析

从这张图上,我们可以看到,NodeJS底层框架由Node.js 标准库、Node bindings、 底层库三部分组成。

1. Node.js 标准库
这部分是由 Javascript编写的,也就是我们使用过程中直接能调用的 API,在源码中的 lib 目录下可以看到。

2. Node bindings

  • 这一部分是Javascript 能够直接调用C/C++代码的关键。
  • 主要作用是把nodejs底层实现的C/C++库暴露给Javascript 环境。 Nodejs 通过一层 C++ Binding,把 JS 传入 V8, V8 解析后交给 libuv 发起 asnyc I/O, 并等待消息循环调度。
  • 可以将其理解为一个桥,桥这头是js,桥那头是C/C++,通过这个桥可以让js调用C/C++。

3.底层库

  • V8: Google 推出的 Javascript VM,也是 Node.js 为什么使用的是 Javascript的关键,它为 Javascript提供了在非浏览器端运行的环境,它的高效是 Node.js 之所以高效的原因之一。
  • Libuv:它为 Node.js 提供了跨平台,线程池,事件池,异步 I/O 等能力, Node.js高效的异步编程模型很大程度上归功于libuv的实现。。
  • C-ares:提供了异步处理 DNS 相关的能力。
  • http_parser、OpenSSL、zlib 等:提供包括 http 解析、SSL、数据压缩等其他的能力。

与操作系统交互

举个简单的例子,我们想要打开一个文件,并进行一些操作,可以写下面这样一段代码:

var fs = require('fs');
fs.open('./test.txt', "w", function(err, fd) {    
    //..do something
});

这段代码的调用过程大致可描述为:lib/fs.js → src/node_file.cc → uv_fs

流程

具体来说,当我们调用 fs.open 时,Node.js 通过 process.binding 调用 C/C++ 层面的 Open 函数,然后通过它调用 Libuv 中的具体方法 uv_fs_open,最后执行的结果通过回调的方式传回,完成流程。

我们在 Javascript中调用的方法,最终都会通过 process.binding 传递到 C/C++ 层面,最终由他们来执行真正的操作,Node.js 即这样与操作系统进行互动。

Node.js 单线程

相对于Java,PHP或者.net 等经典服务器端语言中,用户每一次请求都会为用户创建单独的线程,而每一个客户端连接创建一个线程,需要耗费2MB的内存。也就是说。理论上一个8GB的服务器可以同时连接用户数为4000个左右,要存在高并发支持更多的用户,必须要额外增加服务器的数量或者增加服务器内存数,而Web应用程序的硬件成本当然也就上升了。

Node.js 不为每个客户连接创建一个新的线程,而仅仅使用一个线程(thread)。当有用户连接了,就触发一个内部事件,通过非阻塞I/O、事件驱动机制,让 Node.js 程序宏观上也是并行的。使用 Node.js,一个8GB内存的服务器,可以同时处理超过4万用户的连接。理论上,一个8G内存的服务器,可以同时容纳3到4万用户的连接

图1.1 多线程

图1.2单线程

可以通过图1.1与图1.2看出Node.js中单线程的好处是CPU的利用率永远是100%。什么意思呢?我们假设有图1.1中的五个并发业务。一个CPU平均分配给五个业务。其中每一个业务都是先计算再I/O再计算。所谓的I/O你可以简单的理解为读取数据。那么进行I/O的时候,分配给这个线程的CPU是不工作的,得等到I/O结束继续进行计算2 CPU才又开始工作,所以这一段进行I/O的时间段,这一段线程被白白的阻塞掉了。但是在单线程的工作机制中就不一样。在进行完业务一的计算1之后,遇见I/O操作,那么CPU便马上调取业务二的计算1,依此类推,等到I/O操作结束之后再马上调取业务一的计算二。所以,在单线程中,CPU的利用率永远处于100%。

另外,单线程的带来的好处,还有操作系统完全不再有线程创建、销毁的时间开销
坏处,就是一个用户造成了线程的崩溃,整个服务都崩溃了,其他人也崩溃了。

单线程程序,当并行极大的时候,CPU理论上计算能力是100%。
多线程程序,比如PHP是这样的:CPU经常会等待I/O结束,CPU的性能就白白消耗:

只要I/O越多,NodeJS宏观上越并行。如果计算多,NodeJS宏观上越不能并行,此时网页打开速度严重变慢。

因为NodeJS想在破的机器上也能够高效运行,所以采用了单线程的模式,既然是单线程就必须异步I/O。

非阻塞I/O (non-blocking I/O)

例如,当在访问数据库取得数据的时候,需要一段时间。在传统的单线程处理机制中,在执行了访问数据库代码之后,整个线程都将暂停下来,等待数据库返回结果,才能执行后面的代码。也就是说,I/O阻塞了代码的执行,极大地降低了程序的执行效率。

由于Node.js中采用了非阻塞型I/O机制,因此在执行了访问数据库的代码之后,将立即转而执行其后面的代码,把数据库返回结果的处理代码放在回调函数中,从而提高了程序的执行效率。

当某个I/O执行完毕时,将以事件的形式通知执行I/O操作的线程,线程执行这个事件的回调函数。为了处理异步I/O,线程必须有事件循环,不断的检查有没有未处理的事件,依次予以处理。

阻塞模式下,一个线程只能处理一项任务,要想提高吞吐量必须通过多线程。而非阻塞模式下,一个线程永远在执行计算操作,这个线程的CPU核心利用率永远是100%。所以,这是一种特别有哲理的解决方案:与其人多,但是好多人闲着;还不如一个人玩命,往死里干活儿。

事件驱动/事件循环

Event Loop is a programming construct that waits for and dispatches events or messages in a program.

1、每个Node.js进程只有一个主线程在执行程序代码,形成一个执行栈(execution context stack)。

2、Node.js 在主线程里维护了一个"事件队列"(Event queue),当用户的网络请求或者其它的异步操作到来时,Node都会把它放到Event Queue之中,此时并不会立即执行它,代码也不会被阻塞,继续往下走,直到主线程代码执行完毕。

3、主线程代码执行完毕完成后,然后通过Event Loop,也就是事件循环机制,检查队列中是否有要处理的事件,这时要分两种情况:如果是非 I/O 任务,就亲自处理,并通过回调函数返回到上层调用;如果是 I/O 任务,就从 线程池 中拿出一个线程来处理这个事件,并指定回调函数,当线程中的 I/O 任务完成以后,就执行指定的回调函数,并把这个完成的事件放到事件队列的尾部,线程归还给线程池,等待事件循环。当主线程再次循环到该事件时,就直接处理并返回给上层调用。 这个过程就叫 事件循环 (Event Loop)。

4、期间,主线程不断的检查事件队列中是否有未执行的事件,直到事件队列中所有事件都执行完了,此后每当有新的事件加入到事件队列中,都会通知主线程按顺序取出交EventLoop处理。

总结:

  • 我们所看到的node.js单线程只是一个js主线程,本质上的异步操作还是由线程池完成的,node将所有的阻塞操作都交给了内部的线程池去实现,本身只负责不断的往返调度,并没有进行真正的I/O操作,从而实现异步非阻塞I/O,这便是node单线程和事件驱动的精髓之处了。

  • Nodejs之所以单线程可以处理高并发的原因,得益于libuv层的事件循环机制,和底层线程池实现。

  • Event loop就是主线程从主线程的事件队列里面不停循环的读取事件,驱动了所有的异步回调函数的执行,Event loop总共7个阶段,每个阶段都有一个任务队列,当所有阶段被顺序执行一次后,event loop 完成了一个 tick。

优缺点

node的优点:I/O密集型处理是node的强项,因为node的I/O请求都是异步的(如:sql查询请求、文件流操作操作请求、http请求...)

node的缺点:不擅长cpu密集型的操作

什么是cpu密集型操作(复杂的运算、图片的操作)

// 这就是一个cpu密集型的操作
for (let i = 0; i < 1000000; i++) {
 console.log(i);
}

适用场景

RESTful API: 请求和响应只需少量文本,并且不需要大量逻辑处理, 因此可以并发处理数万条连接。
聊天服务: 轻量级、高流量,没有复杂的计算逻辑。

参考文章

http://www.mamicode.com/info-detail-2517916.html
https://www.jb51.net/article/81262.htm

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

推荐阅读更多精彩内容

  • 一文浅析 Node.js 单线程高并发原理 Node 并非是真正意义上的单线程,它是主线程 "单线程",通过事件驱...
    聪明的奇瑞阅读 2,378评论 0 1
  • 简介 自从关心了nodejs以后,然后就开始关心javascript,然后就开始关心浏览器的内部原理,然后就没有然...
    吴玉宏阅读 1,431评论 0 4
  • 什么是NodeJS Node.js采用模块化结构,按照CommonJS规范定义和使用模块。模块与文件是一一对应关系...
    风起云涌Hal阅读 723评论 0 6
  • 1 nodejs 中的异步存在吗? 现在有点 javascript 基础的人都在听说过 nodejs ,而只要与 ...
    htoo阅读 6,471评论 4 51
  • 郝思嘉是《飘》的女主角,也是我最喜欢的人物,美丽,坚强,永远不会被现实打败,她的生命有着强大的张力。 喜欢谁是因为...
    白开水吹泡泡阅读 288评论 0 1