译者按:
在 Medium 上看到这篇文章,行文脉络清晰,阐述简明利落,果断点下翻译按钮。
第一小节背景铺陈略啰嗦,能够略过。刚开始我给这部分留了个 blah blah blah 直接翻后面的,翻完以后回头看,考虑完整性才把第一节给补上。接下来的内容干货满满,相信对 Node.js 运行机制有兴趣的读者必定会有所收获。html
原文:Architecture of Node.js’ Internal Codebase
做者:Aren Linode
首先,说点儿 JavaScript……git
StackOverflow 的联合创始人 Jeff Atwood 在他著名的编程博客 Coding Horror 上说:github
any application that can be written in JavaScript, will eventually be written in JavaScript.
任何能够用 JavaScript 写就的应用程序,最终都会以 JavaScript 写出来。数据库
JavaScrit 的边界和影响力在过去几年里迅猛发展,如今已是最流行的编程语言之一。2016 年爆栈网的开发者调查中,JavaScript 在最流行技术和最热门问答两项排名第一,其余方面也名列前茅。npm
Node.js 是一个服务器端 JavaScript 执行环境,提供了底层服务器功能环境,包括二进制数据操做、文件系统 I/O、数据库访问、网络访问等。它独一无二的特性使其在现存的多种成熟服务器语言中脱颖而出,而且通过了业界领先的科技公司如 Paypal、Tinder、Medium(是的,本文原文的那个博客系统)、LinkedIn 和 Netflex 的实战应用,甚至这些都发生在 Node.js 发布 1.0 以前。编程
我最近在 StackOverflow 上回答一个关于 Node.js 内部代码结构的问题,所以而萌生了写做本文的念头。设计模式
Node.js 的官方文档其实讲得并不清楚它是什么:服务器
一个基于 Chrome V8 引擎的 JavaScript 运行时。Node.js 采用事件驱动、非阻塞 I/O 模型……网络
要理解这段话和它背后的真正力量,咱们须要把 Node.js 拆分到组件,了解它们的关键技术,如何交互协做,最终构成了 Node.js 这个强大的运行时环境:
V8:Google 开源的高性能 JavaScript 引擎,以 C++ 实现。这也是集成在 Chrome 中的 JS 引擎。V8 将你写的 JavaScript 代码编译为机器码(因此它超级快)而后执行。V8 有多快?看看这个爆栈网的回答。
libuv:提供异步功能的 C 库。它在运行时负责一个事件循环(Event Loop)、一个线程池、文件系统 I/O、DNS 相关和网络 I/O,以及一些其余重要功能。
其余 C/C++ 组件和库:如 c-ares、crypto (OpenSSL)、http-parser 以及 zlib。这些依赖提供了对系统底层功能的访问,包括网络、压缩、加密等。
应用/模块(Application/Modules):这部分就是全部的 JavaScript 代码:你的应用程序、Node.js 核心模块、任何 npm install 的模块,以及你写的全部模块代码。你花费的主要精力都在这部分。
绑定(Bindings):Node.js 用了这么多 C/C++ 的代码和库,简单来讲,它们性能很好。不过,JavaScript 代码最后是怎么跟这些 C/C++ 代码互相调用的呢?这不是三种不一样的语言吗?确实如此,并且一般不一样语言写出来的代码也不能互相沟通,没有 binding 就不行。Binding 是一些胶水代码,可以把不一样语言绑定在一块儿使其可以互相沟通。在 Node.js 中,binding 所作的就是把 Node.js 那些用 C/C++ 写的库接口暴露给 JS 环境。这么作的目的之一是代码重用:这些功能已经有现存的成熟实现,不必只是由于换个语言环境就重写一遍,若是桥接调用一下就足够的话。另外一个缘由是性能:C/C++ 这样的系统编程语言一般都比其余高阶语言(Python、JavaScript、Ruby 等等)性能更高,因此把主要消耗 CPU 的操做以 C/C++ 代码来执行更加明智。
C/C++ Addons:Binding 仅桥接 Node.js 核心库的一些依赖,zlib、OpenSSL、c-ares、http-parser 等。若是你想在应用程序中包含其余第三方或者你本身的 C/C++ 库的话,须要本身完成这部分胶水代码。你写的这部分胶水代码就称为 Addon。能够把 Binding 和 Addon 视为链接 JavaScript 代码和 C/C++ 代码的桥梁。
I/O:输入/输出(Input/Output)的缩写,基本上代指那些主要由计算机 I/O 子系统处理的操做。重 I/O 操做(I/O-bound operations)一般会牵涉到磁盘或驱动器访问,例如数据库访问或文件系统相关操做。相似的概念还有重 CPU 操做(CPU-bound)、重内存操做(Memory-bound)等等。它们的区分是根据系统哪部分性能对这个操做有最大的影响。好比对于某项操做而言,CPU 运算能力提升能够带来最大的提高,这项操做就属于重 CPU 操做。
非阻塞/异步:当一项请求发来,应用程序会处理这个请求,其余操做须要等这个请求处理完成才能执行。这个流程的问题是:当大量请求并发时每一个请求都须要等待前一个完成,也就是说每一个请求都会阻塞后面的全部请求,最糟糕的是若是前一个请求花了很长时间(好比从数据库读取 3GB 的数据)后面全部请求都跟着悲剧了。解决办法能够是引入多处理器和(或)多线程架构,这些办法各有优劣。Node.js 采用了另外一种方式,再也不为每一个请求开启一个新的线程,而是全部请求都在单一的主线程中处理,也只作这么一件事情:处理请求——请求中包含的 I/O 操做如文件系统访问、数据库读写等,都会转发给由 libuv 管理的工做线程去执行。也就是说,请求中的 I/O 操做是异步处理的,而非在主线程上进行。这个办法就使得主线程从不会阻塞,由于全部耗时的任务都分配到了别处。你须要面对的只有惟一的主线程,全部 libuv 管理的工做线程都与你隔离开来,无需操心,Node.js 会处理好那部分。在这个架构之上重 I/O 操做变得格外高效,那些重 CPU、重内存的也同样。Node.js 提供了开箱即用的异步 I/O 调度,还有一些针对重 CPU 执行的处理,不过这已经超出本文话题范畴了。
事件驱动:基本上,全部现代系统都是主程序启动完毕以后,对每一个收到的请求开启一个进程,接下来根据不一样技术有不一样的处理方式,有时差别会截然不同。典型的实现是:针对一个请求开启一个线程,一步接一步执行任务操做,若是某个操做执行缓慢,这个线程上的后续操做都会随之挂起,直到全部操做完成,返回结果。而在 Node.js 中,全部的操做都注册为一个事件,等待主程序或者外部请求来触发。
(系统)运行时:Node.js 运行时是指全部这些代码(上述全部组件,包括底层和上层)提供给 Node.js 应用程序执行的环境。
咱们已经了解 Node.js 顶层组件各自的概貌,如今看看它们组合在一块儿的工做流程,能够更透彻地理解总体架构以及各部分如何协做交互。
一个 Node.js 应用启动时,V8 引擎会执行你写的应用代码,保持一份观察者(注册在事件上的处理函数)列表。当事件发生时,它的处理函数会被加进一个事件队列。只要这个队列还有等待执行的事件,事件循环就会持续把事件从队列中拿出,放进调用堆栈。须要注意的是,只有当前一个事件处理完毕(调用堆栈也已经清空),事件循环才会把下一个事件放进调用堆栈。
在调用堆栈中,全部的 I/O 请求都会转发给 libuv 处理。libuv 会维持一个线程池,包含四个工做线程(这是默认数量,也能够修改配置增长更多工做线程)。文件系统 I/O 请求和 DNS 相关请求都会放进这个线程池处理;其余的请求,如网络、平台特性相关的请求会分发给相应的系统处理单元(参见 libuv 设计概览)。
安排给线程池的这些 I/O 操做由 Node.js 的底层库执行,完成以后 libuv 把此事件放回事件队列,等待主线程执行后续操做。在 libuv 处理这些异步 I/O 操做期间,主线程不会等待处理结果,而是继续忙其余事情,只有当事件循环把 libuv 返回的事件放进调用堆栈以后,主线程才会继续处理这个事件的后续操做。这就是一个事件在 Node.js 中执行的整个生命周期。
mbp 曾经作过一个巧妙的比喻,把 Node.js 当作一家餐厅。我在此借用下他的例子,稍做修改来阐述下 Node.js 的执行状况:
把 Node.js 应用程序想象成一家星巴克,一个训练有素的前台服务生(惟一的主线程)在柜台前接受订单。当不少顾客同时光临的时候,他们排队(进入事件队列)等候接待;每当服务生接待一位顾客,服务生会把订单告知给经理(libuv),经理安排相应的专职人员去烹制咖啡(工做线程或者系统特性)。这个专职人员会使用不一样的原料和咖啡机(底层 C/C++ 组件)按订单要求制做咖啡或甜点,一般会有四个这样的专职人员保持在岗待命(线程池),高峰期的时候也能够安排更多(不过须要在一早就安排人员来上班,而不能中午临时通知)。服务生把订单转交给经理以后不须要等着咖啡制做完成,而是直接开始接待下一位顾客(事件循环放进调用堆栈的另外一个事件),你能够把当前调用堆栈里的事件当作是站在柜台前正在接受服务的顾客。
当咖啡完成时,会被发送到顾客队列的最后位置,等它移动到柜台前服务生会叫相应顾客的名字,顾客就来取走咖啡(最后这部分在真实生活中听起来有点怪,不过你从程序执行的角度理解就比较合乎情理了)。
以上就是 Node.js 的内部顶层组件架构概览,以及它的事件循环机制。本文依然是很是精简归纳,还有不少问题和细节没有展开,如重 CPU 操做的处理、Node.js 设计模式等,将来会有更多文章阐述这些内容(译注:在 Aren Li 的 Medium 专栏 Yet Another Node.js Blog 里)。