Node.js® 是一个基于 Chrome V8 引擎 的 JavaScript 运行时。

Node.js是一个能够在服务器端运行JavaScript开放源代码跨平台JavaScript 运行环境

Node.js采用了事件驱动非阻塞异步输入输出模型等技术来提高性能,可优化应用程序的传输量和规模。这些技术通常用于数据密集的实时应用程序。

Node.js以单线程运行,使用非阻塞I/O调用,这样既可以支持数以万计的并发连线,又不会因多线程本身的特点而带来麻烦。众多请求只使用单线程的设计意味着可以用于创建高并发应用程序。Node.js应用程序的设计目标是任何需要操作I/O的函数都使用回调函数

这种设计的缺点是,如果不使用clusterStrongLoop Process Managerpm2等模块,Node.js就难以处理多核或多线程等情况。

关键词

nvm:Node.js版本控制器,用于切换多版本的Node.js环境。

npm:(全称 Node Package Manager,即“node包管理器”)是Node.js默认的、以JavaScript编写的软件包管理系统

Node.js Express 框架: 相当于Python的Flask框架

Node.js多进程:clusterpm2

Node.js核心内置模块:

1
2
3
4
5
6
7
8
http:提供HTTP服务器功能。
url:解析URL。
fs:与文件系统交互。
querystring:解析URL的查询字符串。
child_process:新建子进程。
util:提供一系列实用小工具。
path:处理文件路径。
crypto:提供加密和解密功能,基本上是对OpenSSL的包装。

阻塞对比非阻塞一览

本概论涵盖了在 Node.js 中 阻塞 and 非阻塞 的区别,同时也会牵涉到时间轮询和 libuv 方面,不需要先行了解这些方面的知识也可以继续阅读。我们假定读者对于 JavaScript 语言和 Node.js 的回调机制有一个基本的了解。

“I/O” 指的是系统磁盘和由 libuv 支持的网络之间的交互。

阻塞

阻塞 是说 Node.js 中其它的 JavaScript 命令必须等到一个非 JavaScript 操作完成之后才可以执行。这是因为当 阻塞 发生时,事件机制无法继续运行JavaScript。

在 Node.js 中,JavaScript由于 CPU 密集操作而表现不佳。而不是等待非 JavaScript操作 (例如I/O)。这被称为 阻塞。在 Node.js 基本类库中,使用 libuv 的同步方法大多数都是 阻塞 的。原生方法也可能是 阻塞 的。

所有在 Node.js 中提供的 I/O 方法也包括异步版本,它们都是 非阻塞 的,接受回调函数。一些方法同时也具备 阻塞 功能,它们的名字结尾都以 Sync 结尾。

代码比较

阻塞 方法执行起来是 同步地,但是 非阻塞 方法执行起来是 异步地。 如果你使用文件系统模块读取一个文件,同步方法看上去如下:

1
2
const fs = require('fs');
const data = fs.readFileSync('/file.md'); // blocks here until file is read

这是一个与之功能等同的 异步 版本示例:

1
2
3
4
const fs = require('fs');
fs.readFile('/file.md', (err, data) => {
if (err) throw err;
});

第一个示例看上去比第二个似乎简单些,但是有一个缺陷:第二行语句会 阻塞 其它 JavaScript 语句的执行直到整个文件全部读取完毕。注意在同步版本的代码中,任何异常都会抛出,会导致整个程序崩溃。在异步版本示例代码中,它由作者来决定是否抛出异常。

让我们扩展一点我们的同步代码:

1
2
3
4
const fs = require('fs');
const data = fs.readFileSync('/file.md'); // blocks here until file is read
console.log(data);
// moreWork(); will run after console.log

这是一个类似的,但是功能上不等同的异步代码示例版本:

1
2
3
4
5
6
const fs = require('fs');
fs.readFile('/file.md', (err, data) => {
if (err) throw err;
console.log(data);
});
// moreWork(); will run before console.log

第一个示例代码中, console.log 将在 moreWork() 之前被调用。在第二个例子中, fs.readFile() 因为是 非阻塞 的,所以 JavaScript 会继续执行, moreWork() 将被首先调用。moreWork() 无需等待文件读完而先行执行完毕,这对于高效吞吐来说是一个绝佳的设计。

并行和吞吐

在 Node.js 中 JavaScript 的执行是单线程的,所以并行与事件轮询能力(即在完成其它任务之后处理 JavaScript 回调函数的能力)有关。任何一个企图以并行的方式运行的代码必须让事件轮询机制以非 JavaScript 操作来运行,像 I/O 操作。

举个例子,让我们思考一个案例:案例中每个对服务器的请求消耗 50 毫秒完成,其中的 45 毫秒又是可以通过异步操作而完成的数据库操作。选择 非阻塞 操作可以释放那 45 毫秒用以处理其它的请求操作。这是在选择 阻塞非阻塞 方法上的重大区别。

Node.js 中的事件轮询机制和其它语言相比而言有区别,其它语言需要创建线程来处理并行任务。

把阻塞和非阻塞代码混在一起写的危险

在处理 I/O 问题时,有些东西必须避免。下面让我们看一个例子:

1
2
3
4
5
6
const fs = require('fs');
fs.readFile('/file.md', (err, data) => {
if (err) throw err;
console.log(data);
});
fs.unlinkSync('/file.md');

在以上的例子中, fs.unlinkSync() 极有可能在 fs.readFile() 之前执行,所以在真正准备开始读取文件前此文件就已经被删除了。一个更好的处理方法就是彻底让使它变得 非阻塞化,并且保证按照正确顺序执行:

1
2
3
4
5
6
7
8
const fs = require('fs');
fs.readFile('/file.md', (readFileErr, data) => {
if (readFileErr) throw readFileErr;
console.log(data);
fs.unlink('/file.md', (unlinkErr) => {
if (unlinkErr) throw unlinkErr;
});
});

以上代码在 fs.readFile() 用异步方式调用 fs.unlink(),这就保证了执行顺序的正确。

第一个应用

app.js # require命令用于指定加载模块,加载时可以省略脚本文件的后缀名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const http = require('http');

const hostname = '127.0.0.1';
const port = 3000;

const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('Hello World\n');
});

server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});

然后使用 node app.js 运行程序,访问 http://localhost:3000,你就会看到一个消息,写着“Hello World”。

Run Node.js in Docker

创建 Node.js 应用

首先,创建一个新文件夹以便于容纳需要的所有文件,并且在此其中创建一个 package.json 文件,描述你应用程序以及需要的依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"name": "docker_web_app",
"version": "1.0.0",
"description": "Node.js on Docker",
"author": "Neo <Neo42@mail.com>",
"main": "server.js",
"scripts": {
"start": "node app2mongo.js"
},
"dependencies": {
"express": "^4.16.3",
"mongodb": "^3.1.6"
}
}

配合着你的 package.json 请运行 npm install。如果你使用的 npm 是版本 5 或者之后的版本,这会自动生成一个 package-lock.json 文件,它将一起被拷贝进入你的 Docker 镜像中。

安装 MongoDB driver for Node.js.

1
npm install mongodb --save

App2mongo.js # 操作测试Mongodb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
const insertDocuments = function(db, callback) {
// Get the documents collection
const collection = db.collection('documents');
// Insert some documents
collection.insertMany([
{Neo : 42}, {a : 2}, {a : 3}
], function(err, result) {
assert.equal(err, null);
assert.equal(3, result.result.n);
assert.equal(3, result.ops.length);
console.log("Inserted 3 documents into the collection Neo");
callback(result);
});
}

const findDocuments = function(db, callback) {
// Get the documents collection
const collection = db.collection('documents');
// Find some documents
collection.find({}).toArray(function(err, docs) {
assert.equal(err, null);
console.log("Found the following records");
console.log(docs)
callback(docs);
});
}

const MongoClient = require('mongodb').MongoClient;
const assert = require('assert');

// Connection URL
const url = 'mongodb://192.168.0.103:27017';

// Database Name
const dbName = 'Neo42';

// Use connect method to connect to the server
MongoClient.connect(url, function(err, client) {
assert.equal(null, err);
console.log("Connected correctly to server Neo42");

const db = client.db(dbName);

insertDocuments(db, function() {
findDocuments(db, function() {
client.close();
});
});
});

创建Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
FROM node:9

# Create app directory
WORKDIR /usr/src/app

# Install app dependencies
# A wildcard is used to ensure both package.json AND package-lock.json are copied
# where available (npm@5+)
COPY package*.json ./

RUN npm install
# If you are building your code for production
# RUN npm install --only=production

# Bundle app source
COPY . .

EXPOSE 8080
CMD [ "npm", "start" ]

构建DockerImage

1
docker build -t Neo:42/node-web-app .

Run Docker

1
docker run -p 8080:8080 -d Neo:42/node-web-app

运行输出

1
2
3
4
5
6
7
8
9
10
11
12
Connected correctly to server Neo42 # 连接数据库
Inserted 3 documents into the collection Neo # 插入doc
Found the following records # 查询
[ { _id: 5bb1e06dc2bf97000f0e1886, Neo: 42 },
{ _id: 5bb1e06dc2bf97000f0e1887, a: 2 },
{ _id: 5bb1e06dc2bf97000f0e1888, a: 3 },
{ _id: 5bb1e2bb0c25dc000fa94345, Neo: 42 },
{ _id: 5bb1e2bb0c25dc000fa94346, a: 2 },
{ _id: 5bb1e2bb0c25dc000fa94347, a: 3 },
{ _id: 5bb1e30bb78d09000f1ff341, Neo: 42 },
{ _id: 5bb1e30bb78d09000f1ff342, a: 2 },
{ _id: 5bb1e30bb78d09000f1ff343, a: 3 } ]

Node.js PM2 Runtime

PM2 is a Production Runtime and Process Manager for Node.js applications with a built-in Load Balancer. It allows you to keep applications alive forever, to reload them without downtime and facilitate common Devops tasks.

Advanced, production process manager for Node.js

PM2 Features:高效管理多进程、日志分析记录简单的监控、集群扩展、部署方便等…..

PM2群集模式和重新加载操作可以避免停机时间。

Using PM2 with Docker

1
2
3
4
5
6
7
neo_42
├── Dockerfile
├── ecosystem.config.js
├── package.json
└── src

1 directory, 3 files

生成ecosystem.config.js

1
pm2 init

app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
'use strict';

const express = require('express');

// Constants
const PORT = 8080;
const HOST = '0.0.0.0';

// App
const app = express();
app.get('/', (req, res) => {
res.send('Hello world\n');
});

app.listen(PORT, HOST);
console.log(`Running on http://${HOST}:${PORT}`);

Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
FROM keymetrics/pm2:latest-alpine

WORKDIR src/
# Bundle APP files
COPY src src/
COPY package.json .
COPY ecosystem.config.js .

# Install app dependencies
ENV NPM_CONFIG_LOGLEVEL warn
RUN npm install --production

# Expose the listening port of your app
EXPOSE 8080

# Show current folder structure in logs
#RUN ls -al -R

CMD [ "pm2-runtime", "start", "ecosystem.config.js" ]
#CMD ["pm2-runtime", "ecosystem.config.js", "--web"]

构建

1
docker build -t 'neo/pm2:v1' .

Run

1
docker run -p 80:8080 -d neo/pm2:v1

Pm2 Commands

1
2
docker exec -it 051 pm2  ls
docker exec -it 051 pm2 monit

Pm2 Commands参考http://pm2.keymetrics.io/docs/usage/quick-start/

参考资料