- 原文地址:dev.to/jorge_rockr…
- 原文做者:Jorge Ramón
- 译者:五月君,公众号 “Nodejs技术栈” 做者
Node.js 是当前用来构建可扩展的、高效的 REST API's 的最流行的技术之一。它还能够用来构建混合移动应用、桌面应用甚至用于物联网领域。html
我真的很喜欢它,我已经使用 Node.js 工做了 6 年。这篇文章试图成为了解 Node.js 工做原理的终极指南。node
Web 应用程序是用一个 client/server(客户端/服务器)模式所编写的,其中 client 将向 server 请求资源而且 server 将会根据这个资源以响应。server 仅在 client 请求时作出响应,并在每次响应后关闭链接。git
这种模式是有效的,由于对服务器的每个请求都须要时间和资源(内存、CPU 等)。服务器必须完成上一个请求,才能接受下一个请求。github
因此,服务器在必定的时间内只处理一个请求?这不彻底是,当服务器收到一个新请求时,这个请求将会被一个线程处理。数据库
简而言之,线程是 CPU 为执行一小段指令所花费的时间和资源。 话虽如此,服务器一次要处理多个请求,每一个线程一个(也能够称为 thread-per-request 模式)。编程
注:thread-per-request 意为每个请求一个线程。json
要同时处理 N 个请求,服务器就须要 N 个线程。若是如今有 N+1 个请求,它就必须等待,直到 N 个线程中的任何一个可用。api
在多线程服务器示例中,服务器同时最多容许 4 个请求(线程)当接下来收到 3 个请求时,这些请求必须等待直到这 4 个线程中的任何一个可用。数组
解决此限制的一种方法是向服务器添加更多资源(内存,CPU内核等),但这可能根本不是一个好主意...缓存
固然,会有技术限制。
服务器中的线程数不只仅是这里惟一的问题。也许你想知道为何一个线程不能同时处理 2 个或更多的请求?这是由于阻塞了 Input/Output 操做。
假设你正在开发一个在线商店应用,而且它须要一个页面,用户能够在其中查看您的全部产品。
用户访问 yourstore.com/products 服务器将从数据库中获取你的所有产品来呈现一个 HTML 文件,这很简单吧?
可是,后面会发生什么?...
1. 当用户访问 /products 时,须要执行特定的方法或函数来知足请求,所以会有一小段代码来解析这个请求的 url 并定位到正确的方法或函数。线程正在工做。✔️
2. 该方法或函数以及第一行将被执行。线程正在工做。✔️
3. 由于你是一名优秀的开发者,你会保存全部的系统日志在一个文件中,要确保路由执行了正确的方法/函数,你的日志要增长一个字符串 “Method X executing!!”(某某方法正在执行),这是一个阻塞的 I/O 操做。线程正在等待。❌
4. 日志已被保存而且下一行将被执行。线程正在工做。✔️
5. 如今是时候去数据库并获取全部产品了,一个简单的查询,例如 SELECT * FROM products 操做,可是您猜怎么着?这是一个阻塞的 I/O 操做。线程正在等待。❌
6. 你会获得一个全部的产品列表,但要确保将它们记录下来。线程正在等待。❌
7. 使用这些产品,是时候渲染模版了,可是在渲染它以前,你应该先读取它。线程正在等待。❌
8. 模版引擎完成它的工做,并将响应发送到客户端。线程再次开始工做。✔️
9. 线程是自由的(空闲的),像鸟儿同样。🕊️
I/O 操做有多慢?这得须要看状况。
让咱们检查如下表格:
操做 | CPU 时钟周期数 |
---|---|
CPU 寄存器 | 3 ticks |
L1 Cache(一级缓存) | 8 ticks |
L2 Cache(二级缓存) | 12 ticks |
RAM(随机存取存储器) | 150 ticks |
Disk(磁盘) | 30,000,000 ticks |
Network(网络) | 250,000,000 ticks |
译者备注:时钟周期也称(tick、clock cycle、clock period 等),指一个硬件在被使用过程当中,被划分为多个时间周期,当咱们须要比较不一样硬件的性能时,就在不一样硬件之上测试同一个软件,观察它们的时钟周期时间和周期数,若是时钟周期时间越长、周期数越多,就意味着这个硬件须要的性能较低。
磁盘和网络操做太慢了。您的系统进行了多少次查询或外部 API 调用?
在恢复过程当中,I/O 操做使得线程等待且浪费资源。
早在 2000 年代初期,服务器和客户端机器运行缓慢。这个问题是在一台服务器机器上同时运行 10,000 个客户端连接。
为何咱们传统的 “thread-per-request” 模式不可以解决这个问题?如今让咱们作一些数学运算。
本地线程实现为每一个线程分配大约 1 MB 的内存,因此 10K 线程就须要 10GB 的 RAM,请记住这仅仅是在 2000 年代初期!!
现在,服务器和客户端的计算能力比这更好,几乎任何编程语言和框架都解决了这个问题。实际,该问题已更新为在一台服务器上处理 10 million(1000 万) 个客户端连接(也称 C10M 问题)。
剧透提醒 🚨🚨🚨!!
Node.js 解决了这个 C10K 问题... 可是为何?
JavaScript 服务端早在 2000 年代并非什么新鲜事,它基于 “thread-per-request” 模式在 Java 虚拟机之上有一些实现,例如,RingoJS、AppEngineJS。
可是,若是那不能解决 C10K 问题,为何 Node.js 能够?好吧,由于它是单线程的。
Node.js 是一个构建在 Google Chrome's JavaScript 引擎(V8 引擎)之上的服务端平台,可将 JavaScript 代码编译为机器代码。
Node.js 基于事件驱动、非阻塞 I/O 模型,从而使其轻巧和高效。它不是一个框架,也不是一个库,它是一个运行时。
一个简单的例子:
// Importing native http module
const http = require('http');
// Creating a server instance where every call
// the message 'Hello World' is responded to the client
const server = http.createServer(function(request, response) {
response.write('Hello World');
response.end();
});
// Listening port 8080
server.listen(8080);
复制代码
Node.js 是非阻塞 I/O,这意味着:
让咱们写一个例子,在每一次 /home 请求时,服务器将响应一个 HTML 页面,不然服务器响应一个 'Hello World' 文本。要响应 HTML 页面,首先要读取这个文件。
home.html
<html>
<body>
<h1>This is home page</h1>
</body>
</html>
复制代码
index.js
const http = require('http');
const fs = require('fs');
const server = http.createServer(function(request, response) {
if (request.url === '/home') {
fs.readFile(`${ __dirname }/home.html`, function (err, content) {
if (!err) {
response.setHeader('Content-Type', 'text/html');
response.write(content);
} else {
response.statusCode = 500;
response.write('An error has ocurred');
}
response.end();
});
} else {
response.write('Hello World');
response.end();
}
});
server.listen(8080);
复制代码
若是这个请求的 url 是 /home,咱们使用 fs 本地模块读取这个 home.html 文件。
传递给 http.createServer 和 fs.readFile 的函数称为回调。这些功能将在未来的某个时间执行(第一个功能将在收到一个请求时执行,第二个功能将在文件读取而且缓冲以后执行)。
在读取文件时,Node.js 仍然能够处理请求,甚至再次读取文件,all at once in a single thread... but how?!
事件循环是 Node.js 背后的魔力,简而言之,事件循环其实是一个无限循环,而且是线程里惟一可用的。
Libuv 是一个实现此模式的 C 语言库,是 Node.js 核心模块的一部分。阅读关于 Libuv 的更多内容 here。
事件循环须要经历 6 个阶段,全部阶段的执行被称为 tick。
好的,因此只有一个线程而且该线程是一个 EventLoop,可是 I/O 操做由谁来执行呢?
注意 📢📢📢!!!
当 Event Loop 须要执行 I/O 操做时,它将从一个池(经过 Libuv 库)中使用系统线程,当这个做业完成时,回调将排队等待在 “pending callbacks” 阶段被执行。
那不是很完美吗?
Node.js 彷佛很完美,你能够用它来构建任何你想要的东西。
让咱们构建一个 API 来计算质数。
质数又称素数。一个大于 1 的天然数,除了 1 和它自身外,不能被其余天然数整除的数叫作质数;
给一个数 N,这个 API 必须计算并在一个数组中返回 N 个天然数。
primes.js
function isPrime(n) {
for(let i = 2, s = Math.sqrt(n); i <= s; i++)
if(n % i === 0) return false;
return n > 1;
}
function nthPrime(n) {
let counter = n;
let iterator = 2;
let result = [];
while(counter > 0) {
isPrime(iterator) && result.push(iterator) && counter--;
iterator++;
}
return result;
}
module.exports = { isPrime, nthPrime };
复制代码
index.js
const http = require('http');
const url = require('url');
const primes = require('./primes');
const server = http.createServer(function (request, response) {
const { pathname, query } = url.parse(request.url, true);
if (pathname === '/primes') {
const result = primes.nthPrime(query.n || 0);
response.setHeader('Content-Type', 'application/json');
response.write(JSON.stringify(result));
response.end();
} else {
response.statusCode = 404;
response.write('Not Found');
response.end();
}
});
server.listen(8080);
复制代码
primes.js 是质数功能实现,isPrime 检查给予的参数 N 是否为质数,若是是一个质数 nthPrime 将返回 n 个质数
index.js 建立一个服务并在每次请求 /primes 时使用这个库。经过 query 传递参数。
获取 20 前的质数,咱们发起一个请求 http://localhost:8080/primes?n=2
假设有 3 个客户端访问这个惊人的非阻塞 API:
当咱们的第三个客户端发送请求时,客户端将会被阻塞,由于质数库会占用大量的 CPU。主线程忙于执行密集型的代码将没法作其它任何事情。
可是 Libuv 呢?若是你记得这个库使用系统线程帮助 Node.js 作一些 I/O 操做以免主线程阻塞,那你是对的,这个能够帮助咱们解决这个问题,可是使用 Libuv 库咱们必需要使用 C++ 语言编写。
值得庆祝的是 Node.js v10.5 引入了工做线程。
如文档所述:
工做线程对于执行 CPU 密集型的 JavaScript 操做很是有用。 它们在 I/O 密集型的工做中用途不大。 Node.js 的内置的异步 I/O 操做比工做线程效率更高。
如今修复咱们的初始化代码:
primes-workerthreads.js
const { workerData, parentPort } = require('worker_threads');
function isPrime(n) {
for(let i = 2, s = Math.sqrt(n); i <= s; i++)
if(n % i === 0) return false;
return n > 1;
}
function nthPrime(n) {
let counter = n;
let iterator = 2;
let result = [];
while(counter > 0) {
isPrime(iterator) && result.push(iterator) && counter--;
iterator++;
}
return result;
}
parentPort.postMessage(nthPrime(workerData.n));
复制代码
index-workerthreads.js
const http = require('http');
const url = require('url');
const { Worker } = require('worker_threads');
const server = http.createServer(function (request, response) {
const { pathname, query } = url.parse(request.url, true);
if (pathname === '/primes') {
const worker = new Worker('./primes-workerthreads.js', { workerData: { n: query.n || 0 } });
worker.on('error', function () {
response.statusCode = 500;
response.write('Oops there was an error...');
response.end();
});
let result;
worker.on('message', function (message) {
result = message;
});
worker.on('exit', function () {
response.setHeader('Content-Type', 'application/json');
response.write(JSON.stringify(result));
response.end();
});
} else {
response.statusCode = 404;
response.write('Not Found');
response.end();
}
});
server.listen(8080);
复制代码
index-workerthreads.js 在每一个请求中将建立一个 Worker 实例,在一个工做线程中加载并执行 primes-workerthreads.js 文件。当这个质数列表计算完成,这个 message 消息将会被触发,接收信息并赋值给 result。因为这个 job 已完成,将会再次触发 exit 事件,容许主线程发送数据给到客户端。
primes-workerthreads.js 变化小一点。它导入 workerData(从主线程传递参数),parentPort 这是咱们向主线程发送消息的方式。
如今让咱们再次作 3 个客户端例子,看看会发生什么:
主线程再也不阻塞 🎉🎉🎉🎉🎉!!!!!
它的工做方式与预期的同样,可是生成工做线程并非最佳实践,建立新线程并不便宜。必定先建立一个线程池。
Node.js 是一项功能强大的技术,值得学习。
个人建议老是很好奇,若是您知道事情的进展,您将作出更好的决定。
伙计们,到此为止。但愿您对 Node.js 有所了解。
感谢您的阅读,下一篇文章中相见。❤️