一文聊聊Node多程序模型和專案部署

2022-12-23 22:00:59
如何實現多程序?如何部署node專案?下面本篇文章帶大家掌握Node.js 多程序模型和專案部署的相關知識,希望對大家有所幫助!

昨天有小夥伴問 express 專案該如何部署。於是整理了這篇文章,主要講述如何部署一個基於 nodejs 開發的伺服器端程式,供有需要的朋友們參考。

文章包含幾個部分:

  • 執行緒和程序
  • node.js 實現多程序
  • 伺服器安裝 Node.js 環境
  • 使用 PM2 管理 Node.js 專案
  • 使用 Nginx 實現介面服務的代理轉發

程序 VS 執行緒

程序

程序(process)是計算機作業系統分配和排程任務的基本單位。開啟工作管理員,可以看到其實在計算機的後臺執行著非常多的程式,每個程式都是一個程序。【相關教學推薦:、】

Snipaste_2022-07-28_21-39-23.png

現代瀏覽器基本都是多程序架構的,以 Chrome 瀏覽器為例,開啟「更多工具」 - 「工作管理員」,就能看到當前瀏覽器的程序資訊,其中一個頁面就是一個程序,除此之外,還有網路程序,GPU 程序等。

Snipaste_2022-07-28_21-41-20.png

多程序的架構,得以保證應用更穩定的執行。還是以瀏覽器為例,假如所有的程式都執行在一個程序中,如果網路出現故障,或者頁面渲染出錯問題,都會導致整個瀏覽器的崩潰。通過多程序的架構,哪怕網路程序崩潰了,它不會影響到已有頁面的展示,最壞也就是暫時不能接入網路。

執行緒

執行緒(thread)是作業系統能夠進行運算排程的最小單位。它被包含在程序之中,是程序中的實際運作單位。舉一個例子,一個程式好比是一家公司,下設多個部門,就是若干個程序;每個部門的通力合作使得公司正常執行,而執行緒就是員工,是具體幹活的人。

我們都知道 JavaScript 是一門單執行緒語言。這麼設計是因為早期 JS 主要用來編寫指令碼程式,負責實現頁面的互動效果。如果設計成多執行緒語言,一是沒有必要,二是多個執行緒共同操作一個 dom 節點,那麼瀏覽器該聽誰的?當然隨著技術的發展,現在的 JS 也支援了多執行緒,不過僅用來處理一些和 dom 操作無關的邏輯。

單程序存在的問題

單執行緒單程序帶來一個嚴重的問題,一個執行中的 node.js 程式,一旦主執行緒掛掉,那麼這個程序也就掛掉了,整個應用也就隨之掛掉。再者,現代計算機大都是多核 CPU,四核八執行緒,八核十六執行緒,都是很常見的裝置了。而 node.js 作為一個單程序的程式,白白浪費掉了多核 CPU 的效能。

針對這種情況,我們需要一個合適的多程序模型,將一個單程序的 node.js 程式變為多程序的架構。

Node.js 的多程序實現

Node.js 實現多程序架構有兩種常用方案,都是使用原生模組,分別是 child_process 模組和 cluster 模組。

child_process

child_process 是 node.js 的內建模組,看名字也能猜到它負責的是和子程序有關的事。

我們不再細說該模組的具體用法,實際上它大概只有六七個方法,還是非常容易理解的。我們使用其中的一個 fork 方法來演示如何實現多程序以及多程序之間的通訊。

先看下準備好的演示案例的目錄結構:

Snipaste_2022-07-29_19-30-54.png

我們使用 http 模組建立了一個 http server,當有 /sum 請求進來時,會通過 child_process 模組建立一個子程序,並通知子程序執行計算的邏輯,同時父程序也要監聽子程序發來的訊息:

// child_process.jsconst http = require('http')const { fork } = require('child_process')const server = http.createServer((req, res) => {
  if (req.url == '/sum') {
    // fork 方法接收一個模組路徑,然後開啟一個子程序,將模組在子程序中執行
    // childProcess 表示建立的子程序
    let childProcess = fork('./sum.js')

    // 發訊息給子程序
    childProcess.send('子程序開始計算')

    // 父程序中監聽子程序的訊息
    childProcess.on('message', (data) => {
      res.end(data + '')
    })

    // 監聽子程序的關閉事件
    childProcess.on('close', () => {
      // 子程序正常退出和報錯掛掉,都會走到這裡
      console.log('子程序關閉')
      childProcess.kill()
    })

    // 監聽子程序的錯誤事件
    childProcess.on('error', () => {
      console.log('子程序報錯')
      childProcess.kill()
    })
  }
    
  if (req.url == '/hello') {
    res.end('hello')
  }
  
  // 模擬父程序報錯
  if (req.url == '/error') {
     throw new Error('父程序出錯')
     res.end('hello')
   }
})

server.listen(3000, () => {
  console.log('Server is running on 3000')
})複製程式碼
登入後複製

sum.js 用來模擬子程序要執行的任務。子程序監聽父程序發來的訊息,處理計算任務,然後將結果傳送給父程序:

// sum.jsfunction getSum() {
  let sum = 0
  for (let i = 0; i < 10000 * 1000 * 100; i++) {
    sum += 1
  }

  return sum
}// process 是 node.js 中一個全域性物件,表示當前程序。在這裡也就是子程序。// 監聽主程序發來的訊息process.on('message', (data) => {
  console.log('主程序的訊息:', data)
    
  const result = getSum()
  // 將計算結果傳送給父程序
  process.send(result)
})複製程式碼
登入後複製

開啟終端,執行命令 node 1.child_process

Snipaste_2022-07-28_22-52-55.png

存取瀏覽器:

Snipaste_2022-07-28_22-53-38.png

接著來模擬子程序報錯的情況:

// sum.jsfunction getSum() {
  // ....}// 子程序執行5s後,模擬程序掛掉
 setTimeout(() => {
   throw new Error('報錯')
 }, 1000 * 5)

process.on('message', (data) => {
  // ...})複製程式碼
登入後複製

再次存取瀏覽器,5秒之後觀察控制檯:

Snipaste_2022-07-28_22-57-11.png

子程序已經掛掉了,然後再存取另一個 url :/hello

Snipaste_2022-07-28_22-58-00.png

可見,父程序依然能正確處理請求,說明子程序報錯,並不會影響父程序的執行

接著我們來模擬父程序報錯的場景,註釋掉 sum.js 模組的模擬報錯,然後重新啟動服務,瀏覽器存取 /error

Snipaste_2022-07-28_23-04-05.png

發現父程序掛掉後,整個 node.js 程式自動退出了,服務完全崩潰,沒有挽回的餘地。

可見,通過 child_processfork 方法實現 node.js 的多程序架構並不複雜。程序間的通訊主要通過 sendon 方法,從這個命名上也能知道,其底層應該是一個釋出訂閱模式。

但是它存在一個嚴重的問題,雖然子程序不影響父程序,但是一旦父程序出錯掛掉,所有的子程序會被」一鍋端掉「 。所以,這種方案適用於將一些複雜耗時的運算,fork 出一個單獨的子程序去做。更準確的來說,這種用法是用來代替多執行緒的實現,而非多程序。

cluster

使用 child_process 模組實現多程序,貌似不堪大用。所以一般更推薦使用 cluster 模組來實現 node.js 的多程序模型。

cluster,叢集的意思,這個名詞相信大家都不陌生。打個比方,以前公司只有一個前臺,有時候太忙就沒辦法及時接待訪客。現在公司分配了4個前臺,即使有三個都在忙,也還有一個能接待新來的訪客。叢集大致也就是這個意思,對於同一件事,合理的分配給不同的人去幹,以此來保證這件事能做到最好。

cluster 模組的使用也比較簡單。如果當前程序是主程序,則根據 CPU 的核數建立合適數量的子程序,同時監聽子程序的 exit 事件,有子程序退出,就重新 fork 新的子程序。如果不是子程序,則進行實際業務的處理。

const http = require('http')const cluster = require('cluster')const cpus = require('os').cpus()if (cluster.isMaster) {
  // 程式啟動時首先走到這裡,根據 CPU 的核數,建立出多個子程序
  for (let i = 0; i < cpus.length; i++) {
    // 建立出一個子程序
    cluster.fork()
  }

  // 當任何一個子程序掛掉後,cluster 模組會發出'exit'事件。此時通過再次呼叫 fork 來來重新啟動程序。
  cluster.on('exit', () => {
    cluster.fork()
  })
} else {
  // fork 方法執行建立子程序,同時會再次執行該模組,此時邏輯就會走到這裡
  const server = http.createServer((req, res) => {
    console.log(process.pid)
    res.end('ok')
  })

  server.listen(3000, () => {
    console.log('Server is running on 3000', 'pid: ' + process.pid)
  })
}複製程式碼
登入後複製

啟動服務:

Snipaste_2022-07-29_00-14-17.png

可以看到,cluster 模組建立出了非常多的子程序,好像是每個子程序都執行著同一個web服務。

需要注意的是,此時並非是這些子程序共同監聽同一個埠。埠的監聽依然是由 createServer 方法建立的 server 去負責,將請求轉發給各個子程序。

我們編寫一個請求指令碼,來請求上面的服務,看下效果。

// request.jsconst http = require('http')for (let i = 0; i < 1000; i++) {
  http.get('http://localhost:3000')
}複製程式碼
登入後複製

http 模組不僅可以建立 http server,還能用來傳送 http 請求。Axios支援瀏覽器和伺服器環境,在伺服器端就是使用 http 模組傳送 http 請求。

使用 node 命令執行該檔案,再看下原來的控制檯:

Snipaste_2022-07-29_00-30-53.png

列印出了具體處理請求的不同子程序的程序ID。

這就是通過 cluster 模組實現的 nodd.js 的多程序架構。

當然,我們在部署 node.js 專案時不會這麼幹巴巴的寫和使用 cluster 模組。有一個非常好用的工具,叫做 PM2,它是一個基於 cluster 模組實現的程序管理工具。在後面的章節中會介紹它的基本用法。

小結

到此為止,我們花了一部分篇幅介紹 node.js 中多程序的知識,其實僅是想要交代下為什麼需要使用 pm2 來管理 node.js 應用。本文由於篇幅有限,再加上描述不夠準確/詳盡,僅做簡單介紹。如果是第一次接觸這一塊內容的朋友,可能沒有太明白,也不打緊,後面會再出一篇更細節的文章。

部署實踐

準備一個 express 專案

本文已經準備了一個使用 express 開發的範例程式,。

它主要實現了一個介面服務,當存取 /api/users 時,使用 mockjs 模擬了10條使用者資料,返回一個使用者列表。同時會開啟一個定時器,來模擬報錯的情況:

const express = require('express')const Mock = require('mockjs')const app = express()

app.get("/api/users", (req, res) => {
  const userList = Mock.mock({
    'userList|10': [{
      'id|+1': 1,
      'name': '@cname',
      'email': '@email'
    }]
  })
  
  setTimeout(()=> {
      throw new Error('伺服器故障')
  }, 5000)

  res.status(200)
  res.json(userList)
})

app.listen(3000, () => {
  console.log("服務啟動: 3000")
})複製程式碼
登入後複製

本地測試一下,在終端中執行命令:

node server.js複製程式碼
登入後複製

開啟瀏覽器,存取使用者列表介面:

Snipaste_2022-07-29_14-36-21.png

五秒鐘後,伺服器會掛掉:

Snipaste_2022-07-29_15-57-35.png

後面我們使用 pm2 來管理應用後,就可以解決這個問題。

討論:express 專案是否需要打包

通常完成一個 vue/react 專案後,我們都會先執行打包,再進行釋出。其實前端專案要進行打包,主要是因為程式最終的執行環境是瀏覽器,而瀏覽器存在各種相容性問題和效能問題,比如:

  • 高階語法的不支援,需要將 ES6+ 編譯為 ES5 語法
  • 不能識別 .vue.jsx.ts 檔案,需要編譯
  • 減少程式碼體積,節省頻寬資源,提高資源載入速度
  • ......

而使用 express.js 或者 koa.js 開發的專案,並不存在這些問題。並且,Node.js 採用 CommonJS 模組化規範,有快取的機制;同時,只有當模組在被用到時,才會被匯入。如果進行打包,打包成一個檔案,其實就浪費了這個優勢。所以針對 node.js 專案,並不需要打包。

伺服器安裝 Node.js

本文以 CentOS 系統為例進行演示。

NVM

為了方便切換 node 的版本,我們使用 nvm 來管理 node。

Nvm(Node Version Manager) ,就是 Node.js 的版本管理工具。通過它,可以讓 node 在多個版本之間進行任意切換,避免了需要切換版本時反覆的下載和安裝的操作。

Nvm的官方倉庫是 。因為它的安裝指令碼存放在 githubusercontent 站點上,經常存取不了。所以我在 gitee 上新建了它的,這樣就能從 gitee 上存取到它的安裝指令碼了。

通過 curl 命令下載安裝指令碼,並使用 bash 執行指令碼,會自動完成 nvm 的安裝工作:

# curl -o- https://gitee.com/hsyq/nvm/raw/master/install.sh | bash複製程式碼
登入後複製

當安裝完成之後,我們再開啟一個新的視窗,來使用 nvm :

[root@ecs-221238 ~]# nvm -v0.39.1複製程式碼
登入後複製

可以正常列印版本號,說明 nvm 已經安裝成功了。

安裝 Node.js

現在就可以使用 nvm 來安裝和管理 node 了。

檢視可用的 node 版本:

# nvm ls-remote複製程式碼
登入後複製

安裝 node:

# nvm install 18.0.0複製程式碼
登入後複製

檢視已經安裝的 node 版本:

[root@ecs-221238 ~]# nvm list->      v18.0.0default -> 18.0.0 (-> v18.0.0)
iojs -> N/A (default)
unstable -> N/A (default)
node -> stable (-> v18.0.0) (default)
stable -> 18.0 (-> v18.0.0) (default)複製程式碼
登入後複製

選擇一個版本進行使用:

# nvm use 18.0.0複製程式碼
登入後複製

需要注意的一點,在 Windows 上使用 nvm 時,需要使用管理員許可權執行 nvm 命令。在 CentOS 上,我預設使用 root 使用者登入的,因而沒有出現問題。大家在使用時遇到了未知錯誤,可以搜尋一下解決方案,或者嘗試下是否是許可權導致的問題。

在安裝 node 的時候,會自動安裝 npm。檢視 node 和 npm 的版本號:

[root@ecs-221238 ~]# node -vv18.0.0[root@ecs-221238 ~]# npm -v8.6.0複製程式碼
登入後複製

預設的 npm 映象源是官方地址:

[root@ecs-221238 ~]# npm config get registryhttps://registry.npmjs.org/複製程式碼
登入後複製

切換為國內淘寶的映象源:

[root@ecs-221238 ~]# npm config set registry https://registry.npmmirror.com複製程式碼
登入後複製

到此為止,伺服器就已經安裝好 node 環境和設定好 npm 了。

專案上傳到伺服器

方法有很多,或者從 Github / GitLab / Gitee 倉庫中下載到伺服器中,或者本地通過 ftp 工具上傳。步驟很簡單,不再演示。

演示專案放到了 /www 目錄 下:

Snipaste_2022-07-29_15-27-55.png

伺服器開放埠

一般雲伺服器僅開放了 22 埠用於遠端登入。而常用的80,443等埠並未開放。另外,我們準備好的 express 專案執行在3000埠上。所以需要先到雲伺服器的控制檯中,找到安全組,新增幾條規則,開放80和3000埠。

222.png

等等.png

使用 PM2 管理應用

在開發階段,我們可以使用 nodemon 來做實時監聽和自動重新啟動,提高開發效率。在生產環境,就需要祭出大殺器—PM2了。

基本使用

首先全域性安裝 pm2:

# npm i -g pm2複製程式碼
登入後複製

執行 pm2 -v 命令檢視是否安裝成功:

[root@ecs-221238 ~]# pm2 -v5.2.0複製程式碼
登入後複製

切換到專案目錄,先把依賴裝上:

cd /www/express-demo
npm install複製程式碼
登入後複製

然後使用 pm2 命令來啟動應用。

pm2 start app.js -i max// 或者pm2 start server.js -i 2複製程式碼
登入後複製

PM2 管理應用有 fork 和 cluster 兩種模式。在啟動應用時,通過使用 -i 引數來指定範例的個數,會自動開啟 cluster 模式。此時就具備了負載均衡的能力。

-i :instance,範例的個數。可以寫具體的數位,也可以設定成 max,PM2會自動檢查可用的CPU的數量,然後儘可能多地啟動程序。

Snipaste_2022-07-29_20-44-08.png

此時應用就啟動好了。PM2 會以守護行程的形式管理應用,這個表格展示了應用執行的一些資訊,比如執行狀態,CPU使用率,記憶體使用率等。

在原生的瀏覽器中存取介面:

Snipaste_2022-07-29_18-00-59.png

Cluster 模式是一個多程序多範例的模型,請求進來後會分配給其中一個程序處理。正如前面我們看過的 cluster 模組的用法一樣,由於 pm2 的守護,即使某個程序掛掉了,也會立刻重新啟動該程序。

回到伺服器終端,執行 pm2 logs 命令,檢視下 pm2 的紀錄檔:

Snipaste_2022-07-29_20-18-28.png

可見,id 為1的應用範例掛掉了,pm2 會立刻重新啟動該範例。注意,這裡的 id 是應用範例的 id,並非程序 id。

到這裡,一個 express 專案的簡單部署就完成了。通過使用 pm2 工具,基本能保證我們的專案可以穩定可靠的執行。

PM2 常用命令小結

這裡整理了一些 pm2 工具常用的命令,可供查詢參考。

# Fork模式pm2 start app.js --name app # 設定應用的名字為 app# Cluster模式# 使用負載均衡啟動4個程序pm2 start app.js -i 4     

# 將使用負載均衡啟動4個程序,具體取決於可用的 CPUpm2 start app.js -i 0   

# 等同於上面命令的作用pm2 start app.js -i max 

 # 給 app 擴充套件額外的3個程序pm2 scale app +3# 將 app 擴充套件或者收縮到2個程序pm2 scale app 2              # 檢視應用狀態# 展示所有程序的狀態pm2 list  # 用原始 JSON 格式列印所有程序列表pm2 jlist# 用美化的 JSON 列印所有程序列表pm2 prettylist  # 展示特定程序的所有資訊pm2 describe 0# 使用儀表盤監控所有程序pm2 monit             

# 紀錄檔管理# 實時展示所有應用的紀錄檔pm2 logs          # 實時展示 app 應用的紀錄檔 pm2 logs app# 使用json格式實時展示紀錄檔,不輸出舊紀錄檔,只輸出新產生的紀錄檔pm2 logs --json# 應用管理# 停止所有程序pm2 stop all# 重新啟動所有程序pm2 restart all       

# 停止指定id的程序pm2 stop 0     

# 重新啟動指定id的程序pm2 restart 0         

# 刪除id為0程序pm2 delete 0# 刪除所有的程序pm2 delete all         
複製程式碼
登入後複製

每一條命令都可以親自嘗試一下,看看效果。

這裡特別展示下 monit 命令,它可以在終端中啟動一個面板,實時展示應用的執行狀態,通過上下箭頭可以切換 pm2 管理的所有應用:

Snipaste_2022-07-29_20-25-21.png

進階:使用 pm2 組態檔

PM2 的功能十分強大,遠不止上面的這幾個命令。在真實的專案部署中,可能還需要設定紀錄檔檔案,watch 模式,環境變數等等。如果每次都手敲命令是十分繁瑣的,所以 pm2 提供了組態檔來管理和部署應用。

可以通過以下命令來生成一份組態檔:

[root@ecs-221238 express-demo]# pm2 init simpleFile /www/express-demo/ecosystem.config.js generated複製程式碼
登入後複製

會生成一個ecosystem.config.js 檔案:

module.exports = {
  apps : [{
    name   : "app1",
    script : "./app.js"
  }]
}複製程式碼
登入後複製

也可以自己建立一個組態檔,比如 app.config.js

const path = require('path')module.exports = {  // 一份組態檔可以同時管理多個 node.js 應用
  // apps 是一個陣列,每一項都是一個應用的設定
  apps: [{
    // 應用名稱
    name: "express-demo",

    // 應用入口檔案
    script: "./server.js",

    // 啟動應用的模式, 有兩種:cluster和fork,預設是fork
    exec_mode: 'cluster',

    // 建立應用範例的數量
    instances: 'max',

    // 開啟監聽,當檔案變化後自動重新啟動應用
    watch: true,

    // 忽略掉一些目錄檔案的變化。
    // 由於把紀錄檔目錄放到了專案路徑下,一定要將其忽略,否則應用啟動產生紀錄檔,pm2 監聽到變化就會重新啟動,重新啟動又產生紀錄檔,就會進入死迴圈
    ignore_watch: [
      "node_modules",
      "logs"
    ],
    // 錯誤紀錄檔存放路徑
    err_file: path.resolve(__dirname, 'logs/error.log'),

    // 列印紀錄檔存放路徑
    out_file: path.resolve(__dirname, 'logs/out.log'),

    // 設定紀錄檔檔案中每條紀錄檔前面的日期格式
    log_date_format: "YYYY-MM-DD HH:mm:ss",
  }]
}複製程式碼
登入後複製

讓 pm2 使用組態檔來管理 node 應用:

pm2 start app.config.js複製程式碼
登入後複製

現在 pm2 管理的應用,會將紀錄檔放到專案目錄下(預設是放到 pm2 的安裝目錄下),並且能監聽檔案的變化,自動重新啟動服務。

Snipaste_2022-07-29_20-46-36.png

更多有用的設定可以參考 PM2 官方檔案,。

Nginx 代理轉發介面

上面我們直接將 nodejs 專案的3000埠暴露了出去。一般我們都會使用 nginx 做一個代理轉發,只對外暴露 80 埠。

安裝 Nginx

首先伺服器中需要安裝 nginx ,有三種方式:

  • 下載原始碼編譯安裝
  • 使用 docker 安裝
  • 使用包管理工具安裝

我這裡的系統是 CentOS 8,已經更換了可用的 yum 源,可以直接安裝 nginx。如果你的作業系統為 CentOS 7 或者其他發行版,可以搜尋適合的安裝方法。

使用 yum 安裝:

# yum install -y nginx複製程式碼
登入後複製

然後啟動 nginx:

# systemctl start nginx複製程式碼
登入後複製

開啟瀏覽器存取伺服器地址,可以看到 nginx 預設的主頁:

Snipaste_2022-07-29_18-03-09.png

設定介面轉發

為專案新建一個組態檔:

# vim /etc/nginx/conf.d/express.conf複製程式碼
登入後複製

監聽80埠,將所有請求轉發給伺服器原生的3000埠的程式處理:

server {
    listen       80;
    server_name  ironfan.site;
    location / {
          proxy_pass http://localhost:3000;
    }
}複製程式碼
登入後複製

conf 目錄下的組態檔,會被主組態檔 /etc/nginx/nginx.conf 載入:

Snipaste_2022-07-29_18-20-23.png

修改完組態檔,一定要重新啟動服務:

# systemctl restart nginx複製程式碼
登入後複製

然後本地開啟瀏覽器,去掉原來的3000埠號,直接存取完整的 url:

Snipaste_2022-07-29_18-18-50.png

到這裡,就完成了介面轉發的設定。從使用者的角度出發,這個也叫反向代理。

總結

首先我們比較系統的講解了為何需要在 node.js 專案中開啟多程序,以及兩種實現方式:

  • child_process 模組的 fork 方法
  • cluster 模組fork 方法

之後,又講解了如何在 Linux 伺服器中安裝 node 環境,以及部署一個 node.js 專案的大致流程,並著重介紹了 pm2 的使用:

  1. 上傳專案到伺服器中
  2. 安裝專案依賴
  3. 使用 pm2 管理應用

最後,講解了使用 nginx 實現介面的代理轉發,將使用者請求轉發到原生的3000埠的服務。

至此,我們完成了本文的目標,將一個 express 專案部署到伺服器,並能穩定可靠的執行

下篇文章,我們會使用 Github Actions 實現 CI/CD,讓專案的部署更加便捷高效。

本文演示程式碼,已上傳至 Github,點選存取

更多node相關知識,請存取:!

以上就是一文聊聊Node多程序模型和專案部署的詳細內容,更多請關注TW511.COM其它相關文章!