如何構建一個高可用的node環境

主要解決問題:

  • 故障恢復
  • 多核利用
  • 多進程共享端口



cluster (集羣)

cluster可以多核監聽同一個端口。實現多進程共享端口,這個在node底層已經做好了。

folk(child_process.fork)方式不能實現多進程共享端口,還需要nginx去做多個端口的負載均衡,一般來説用cluster要好點,folk方式適用與多個程序之間。

建立一個cluster.js文件

var cluster = require('cluster'); //cluster庫
var os = require('os'); // 獲取CPU 的數量
var numCPUs = os.cpus().length;
var process = require('process') // 管理進程用的

console.log('numCPUs:', numCPUs) // 打印cpu數量 ①
var workers = {};
if (cluster.isMaster) { // 這裏是進入主進程,第一次啓動的時候,運行這裏
    // 主進程分支
    cluster.on('death', function (worker) { ②
        // 當一個工作進程結束時,重啓工作進程 delete workers[worker.pid]; 這裏主要是為了讓代碼即使報錯,也不會影響服務器運行。故障恢復
        worker = cluster.fork();
        workers[worker.pid] = worker;
    });
    // 初始開啓與CPU 數量相同的工作進程  多核利用 ③
    for (var i = 0; i < numCPUs; i++) {
        var worker = cluster.fork(); // 複製進程,有多少個核,複製多少個子進程,複製的過程會重新運行一遍該文件(因為是複製進程,代碼也會複製份在子進程運行)。
        workers[worker.pid] = worker;
    } 
} else { // 這裏是子進程開啓的時候,就是主進程folk之後,會走到這裏。所以這裏會啓動與cpu相同數量的子進程服務。
    // 子進程啓動服務器,多進程共享3000端口 ④
    var app = require('./app');
    app.use(async (ctx, next) => {
        console.log('worker' + cluster.worker.id + ',PID:' + process.pid)
        next()
    })
    app.listen(3000);
}

// 當主進程被終止時,關閉所有工作進程
process.on('SIGTERM', function () { ⑤
    for (var pid in workers) {
        process.kill(pid);
    }
    process.exit(0);
});

直接看代碼,這樣看可能看不太懂。我們用一個流程圖來展示。我在上面代碼標記了①-⑤ ,5個代碼塊。

nodejs node_modules共用_#node


這裏看運行情況,啓動後,打印了5次cpu數量(主進程一次,子進程4次),①這段代碼執行了5次。

然後我們通過訪問localhost:3000,得到當前訪問的是第三子進程。

nodejs node_modules共用_nginx_02


更優雅的部署node (pm2)

  • 內建負載均衡(使⽤用Node cluster 集羣模塊、子進程)
  • 線程守護,keep alive
  • 0秒停機重載,維護升級的時候不不需要停機.
  • 現在 Linux (stable) & MacOSx (stable) & Windows (stable).多平台⽀支持
  • 停⽌止不不穩定的進程(避免⽆無限循環)
  • 控制枱檢測 https://id.keymetrics.io/api/oauth/login#/register
  • 提供 HTTP API

命令行部署方法:

npm install -g pm2
pm2 start app.js --watch -i 2 // watch 監聽⽂文件變化
// -i 啓動多少個實例例
pm2 stop all
pm2 list
pm2 start app.js -i max # 根據機器器CPU核數,開啓對應數⽬目的進程

nodejs node_modules共用_#docker-compose_03

process.yml文件部署方法:

apps:
  - script : app.js
    instances: 2
    watch  : true
    env    :
      NODE_ENV: production

運行pm2 start process.yml

nodejs node_modules共用_#node_04


pm2設置為開機啓動 pm2 startup

可以看到兩種方式的效果是一樣的,但是大多會選擇以yml文件來啓動。


docker概念

  • Docker 屬於 Linux 容器的一種封裝,提供簡單易用的容器使用接口
  • 1)提供一次性的環境。比如,本地測試他人的軟件、持續集成的時候提供單元測試和構建的環境。
  • 2)提供彈性的雲服務。因為 Docker 容器可以隨開隨關,很適合動態擴容和縮容。
  • 3)組建微服務架構。通過多個容器,一台機器可以跑多個服務,因此在本機就可以模擬出微服務架構。
  • 4)image可以創建容器,每個容器有自己的容器端口,我們需要把它映射到主機端口
  • 5)Docker Compose是 docker 提供的一個命令行工具,用來定義和運行由多個容器組成的應用。使用 compose,我們可以通過 YAML 文件聲明式的定義應用程序的各個服務,並由單個命令完成應用的創建和啓動。

特點

  • ⾼高效的利利⽤用系統資源
  • 快速的啓動時間
  • 一致的運⾏行行環境
  • 持續交付和部署
  • 更更輕鬆的遷移

對⽐傳統虛擬機總結

特性

容器器

虛擬機

啓動

秒級

分鐘級

硬盤使⽤

一般為 MB

一般為 GB

性能

接近原⽣

弱於

系統支持量

單機⽀持上千個容器

一般⼏十個

三個核⼼心概念

  • 鏡像
  • 容器器
  • 倉庫

和pm2類似,docker也有兩種方式啓動,一種是命令行方式,一種是Dockerfile定製鏡像方式。

DockerFile參數 :

FROM

MAINTAINER

RUN

ADD&COPY

WORKDIR

VOLUME

EXPOSE

它依賴什麼鏡像

維護者信息

執行命令行命令

複製文件到指定路徑(ADD能解壓)

指定工作目錄

目錄掛載

容器的端口

常用的doker命令:

  • 查看docker版本:docker version
  • 顯示docker系統的信息:docker info
  • 檢索image:docker search image_name
  • 下載image : docker pull image_name
  • 已下載鏡像列表: docker images
  • 刪除鏡像: docker rmi image_name
  • 啓動容器:docker run image_name
docker構建一個nginx服務器
  1. 拉取官⽅方鏡像
拉取官⽅方鏡像 
   docker pull nginx
   查看鏡像
   docker images nginx
   啓動鏡像
   docker run -p 80:80  -d nginx
   查看進程
   docker ps
   docker ps -a // 查看全部
   停⽌
   docker stop id
   刪除鏡像
   docker rm id
  1. Dockerfile定製鏡像
#Dockerfile
FROM nginx:latest
RUN echo '<h1>Hello, docker</h1>' > /usr/share/nginx/html/index.html
# 定製鏡像
docker build -t mynginx .
# 運⾏
# -d 守護態運⾏
docker run -p 80:80 -d mynginx
Docker-Compose

Docker-Compose項目是Docker官方的開源項目,負責實現對Docker容器集羣的快速編排。
Docker-Compose將所管理的容器分為三層,分別是工程(project),服務(service)以及容器(container)。Docker-Compose運行目錄下的所有文件(docker-compose.yml,extends文件或環境變量文件等)組成一個工程,若無特殊指定工程名即為當前目錄名

docker-compose主要是可以集合多個服務,一起運行。比如一個項目有(前端、後台、數據庫、nginx)4個服務需要去啓動,如果單獨去啓動的話,我們需要運行4次docker。這裏我們能通過docker-compose,一起運行。

案例:nginx+node+pm2後台

nginx:

在nginx文件夾裏,裏面建立個conf.d文件夾。添加一個docker.conf文件

## nginx/conf.d/docker.conf

server {
    listen       80;
    location / {
        root   /var/www/html;
        index  index.html index.htm;
    }
    location ~ \.(gif|jpg|png)$ {
        root /static;
        index index.html index.htm;
    }
    location /api {
            proxy_pass  http://127.0.0.1:3000;
            proxy_redirect     off;
            proxy_set_header   Host             $host;
            proxy_set_header   X-Real-IP        $remote_addr;
            proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
    }
}

node:

// app.js
const Koa = require('koa')
const app = new Koa()
app.use(ctx => {
    ctx.body = 'Hello Docker'
})
app.listen(3000, () => {
    console.log('app started at http://localhost:3000/')
})

// process.yml

apps:
  - script : server.js
    instances: 2
    watch  : true
    env    :
      NODE_ENV: production


// Dockerfile

#Dockerfile
#制定node鏡像的版本
FROM node:10-alpine
#移動當前目錄下面的文件到app目錄下
ADD . /app/
#進入到app目錄下面,類似cd
WORKDIR /app
#安裝依賴
RUN npm install
#對外暴露的端口
EXPOSE 3000
#程序啓動腳本
CMD ["pm2-runtime", "start",  "process.yml"]

然後構建docker-compose.yml

## docker-compose.yml

version: '3.1'
services:
  app-pm2:
      container_name: app-pm2
      #構建容器
      build: ./node 
      ports:
        - "3000:3000"
  nginx:
    restart: always
    image: nginx
    ports:
      - 80:80
    volumes:
      - ./nginx/conf.d/:/etc/nginx/conf.d/  #本地配置文件寫入到nginx配置目錄
      - ./www/:/var/www/html/

然後創建一個www文件夾裏面放一個靜態html文件

//index.html

hello web!!

可以看到在docker-compose文件裏面,我們運行了兩個鏡像,一個是打包後的node名為app-pm2的鏡像,一個是nginx的鏡像。
同時我們把nginx的配置文件從本地寫到了docker運行的nginx目錄裏面。現在我們來看運行效果:

輸入:docker-compose up -d 後台啓動命令。

nodejs node_modules共用_docker_05


可以看到,兩個容器都被創建。現在我們先訪問80端口(nginx映射在80端口)。

nodejs node_modules共用_#node_06


訪問成功,成功訪問到www/index.html

然後我們訪問/api路徑,看是否能訪問到node服務器。

nodejs node_modules共用_#docker_07


訪問成功。這裏一個docker-compose的案例就成功運行了。裏面用了nginx反向代理3000端口接口到80端口/api路徑,同時用了pm2去啓動node接口服務器。

項目搭建好之後,就需要持續集成了,這裏具體請參考我之前寫的文章。Gitlab-CI/CD雲端構建發佈