node如何實作多進程?如何部署node專案?以下這篇文章帶大家掌握Node.js 多進程模型與專案部署的相關知識,希望對大家有幫助!
昨天有小夥伴問 express 專案該如何部署。於是整理了這篇文章,主要講述如何部署一個基於 nodejs 開發的服務端程序,供有需要的朋友們參考。
文章包含幾個部分:
進程(process)是電腦作業系統分配和調度任務的基本單位
。開啟任務管理器,可以看到其實在電腦的後台運行著非常多的程序,每個程序都是一個進程。 【相關教學推薦:
nodejs影片教學】
#現代瀏覽器基本上都是多進程架構的,以Chrome 瀏覽器為例,打開“更多工具” - “任務管理器”,就能看到當前瀏覽器的進程信息,其中一個頁面就是一個進程,除此之外,還有網路進程,GPU進程等。多進程的架構,得以保證應用更穩定的運作。還是以瀏覽器為例,如果所有的程式都運行在一個進程中,如果網路故障,或是頁面渲染出錯問題,都會導致整個瀏覽器的崩潰。透過多進程的架構,即使網路進程崩潰了,它不會影響到已有頁面的展示,最壞也就是暫時無法連接網路。
執行緒(thread)是作業系統能夠進行運算調度的最小單位
。它被包含在進程之中,是進程中的實際運作單位。舉一個例子,一個程序好比是一家公司,下設多個部門,就是若干個進程;每個部門的通力合作使得公司正常運行,而線程就是員工,是具體幹活的人。 我們都知道 JavaScript 是一門單執行緒語言。這麼設計是因為早期 JS 主要用來編寫腳本程序,負責實現頁面的互動效果。如果設計成多執行緒語言,一是沒有必要,二是多個執行緒共同操作一個 dom 節點,那麼瀏覽器該聽誰的?當然隨著技術的發展,現在的 JS 也支援了多線程,不過僅用來處理一些和 dom 操作無關的邏輯。
單執行緒單一進程帶來一個嚴重的問題,一個運行中的node.js 程序,一旦主執行緒掛掉,那麼這個進程也就掛掉了,整個應用程式也就隨之掛掉。再者,現代電腦大都是多核心 CPU,四核心八線程,八核心十六線程,都是很常見的設備了。而 node.js 作為一個單一進程的程序,白白浪費掉了多核心 CPU 的效能。 針對這種情況,我們需要一個合適的多進程模型,將一個單一進程的 node.js 程式變成多進程的架構。
Node.js 的多進程實作Node.js 實作多進程架構有兩種常用方案,都是使用原生模組,分別是
模組和
cluster模組。
child_processchild_process
是 node.js 的內建模組,看名字也能猜到它負責的是和子程序有關的事。
我們不再細說該模組的具體用法,實際上它大概只有六、七個方法,還是非常容易理解的。我們使用其中的一個
fork
// 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 { console.log('主进程的消息:', data) const result = getSum() // 将计算结果发送给父进程 process.send(result) })复制代码
開啟終端,執行指令node 1.child_process
:
存取瀏覽器:
接著來模擬子程序報錯的情況:
// sum.jsfunction getSum() { // ....}// 子进程运行5s后,模拟进程挂掉 setTimeout(() => { throw new Error('报错') }, 1000 * 5) process.on('message', (data) => { // ...})复制代码
再次存取瀏覽器, 5秒後觀察控制台:
子程序已經掛掉了,然後再訪問另一個url :/hello
,
可見,父行程仍能正確處理請求,說明子程序報錯,並不會影響父程序的執行。
接著我們來模擬父進程報錯的場景,註解掉sum.js
模組的類比報錯,然後重新啟動服務,瀏覽器存取/error
:
發現父程序掛掉後,整個node.js 程式自動退出了,服務完全崩潰,沒有挽回的餘地。
可見,透過 child_process
的 fork
方法實作 node.js 的多進程架構並不複雜。進程間的通訊主要透過 send
和 on
方法,從這個命名上也能知道,其底層應該是一個發布訂閱模式。
但是它有一個嚴重的問題,雖然子進程不影響父進程,但是一旦父進程出錯掛掉,所有的子進程會被」一鍋端掉「 。所以,這個方案適用於將一些複雜耗時的運算,fork 出一個單獨的子程序去做。更準確的來說,這種用法是用來取代多執行緒的實現,而非多進程。
使用 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 { 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) }) }复制代码
啟動服務:
可以看到,cluster
模組建立了非常多的子進程,好像是每個子進程都運行著同一個web服務。
要注意的是,此時並非是這些子程序共同監聽同一個埠。連接埠的監聽仍然是由 createServer 方法所建立的 server 去負責,將請求轉送給各個子程序。
我們寫一個請求腳本,來請求上面的服務,看下效果。
// request.jsconst http = require('http')for (let i = 0; i <blockquote><p>http 模組不僅可以建立 http server,還能用來傳送 http 請求。 Axios支援瀏覽器和伺服器環境,在伺服器端就是使用 http 模組發送 http 請求。 </p></blockquote><p>使用<code>node</code> 指令執行該文件,再看下原來的控制台:</p><p style="text-align:center;"><img src="https://img.php.cn/upload/article/000/000/024/6acfb36f7a6b8401b888460a3a451efa-9.png" alt="一文聊聊Node多進程模型和專案部署" style="max-width:90%" loading="lazy"></p>##列印了具體處理請求的不同子進程的進程ID。 <p></p>這就是透過 <p>cluster<code> 模組實現的 nodd.js 的多進程架構。 </code></p><p>当然,我们在部署 node.js 项目时不会这么干巴巴的写和使用 <code>cluster</code> 模块。有一个非常好用的工具,叫做 <strong>PM2</strong>,它是一个基于 cluster 模块实现的进程管理工具。在后面的章节中会介绍它的基本用法。</p><h3 data-id="heading-8">小结</h3><p>到此为止,我们花了一部分篇幅介绍 node.js 中多进程的知识,其实仅是想要交代下<strong>为什么需要使用 pm2 来管理 node.js 应用</strong>。本文由于篇幅有限,再加上描述不够准确/详尽,仅做简单介绍。如果是第一次接触这一块内容的朋友,可能没有太明白,也不打紧,后面会再出一篇更细节的文章。</p><h2 data-id="heading-9">部署实践</h2><h3 data-id="heading-10">准备一个 express 项目</h3><p>本文已经准备了一个使用 express 开发的示例程序,<a href="https://www.php.cn/link/7e6bd4cd5be9faae7e6adca908e1995b" target="_blank" rel="nofollow noopener noreferrer" title="https://github.com/hsyq/ndoe-deploy/tree/main/express-demo" ref="nofollow noopener noreferrer">点此访问</a>。</p><p>它主要实现了一个接口服务,当访问 <code>/api/users</code> 时,使用 <code>mockjs</code> 模拟了10条用户数据,返回一个用户列表。同时会开启一个定时器,来模拟报错的情况:</p><pre class="brush:php;toolbar:false">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复制代码
打开浏览器,访问用户列表接口:
五秒钟后,服务器会挂掉:
后面我们使用 pm2 来管理应用后,就可以解决这个问题。
通常完成一个 vue/react 项目后,我们都会先执行打包,再进行发布。其实前端项目要进行打包,主要是因为程序最终的运行环境是浏览器,而浏览器存在各种兼容性问题和性能问题,比如:
.vue
,.jsx
,.ts
文件,需要编译而使用 express.js 或者 koa.js 开发的项目,并不存在这些问题。并且,Node.js 采用 CommonJS 模块化规范,有缓存的机制;同时,只有当模块在被用到时,才会被导入。如果进行打包,打包成一个文件,其实就浪费了这个优势。所以针对 node.js 项目,并不需要打包。
本文以 CentOS 系统为例进行演示。
为了方便切换 node 的版本,我们使用 nvm 来管理 node。
Nvm(Node Version Manager) ,就是 Node.js 的版本管理工具。通过它,可以让 node 在多个版本之间进行任意切换,避免了需要切换版本时反复的下载和安装的操作。
Nvm的官方仓库是 github.com/nvm-sh/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 已经安装成功了。
现在就可以使用 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
目录 下:
一般云服务器仅开放了 22 端口用于远程登录。而常用的80,443等端口并未开放。另外,我们准备好的 express 项目运行在3000端口上。所以需要先到云服务器的控制台中,找到安全组,添加几条规则,开放80和3000端口。
在开发阶段,我们可以使用 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的数量,然后尽可能多地启动进程。
此时应用就启动好了。PM2 会以守护进程的形式管理应用,这个表格展示了应用运行的一些信息,比如运行状态,CPU使用率,内存使用率等。
在本地的浏览器中访问接口:
Cluster 模式是一个多进程多实例的模型,请求进来后会分配给其中一个进程处理。正如前面我们看过的 cluster
模块的用法一样,由于 pm2 的守护,即使某个进程挂掉了,也会立刻重启该进程。
回到服务器终端,执行 pm2 logs
命令,查看下 pm2 的日志:
可见,id 为1的应用实例挂掉了,pm2 会立刻重启该实例。注意,这里的 id 是应用实例的 id,并非进程 id。
到这里,一个 express 项目的简单部署就完成了。通过使用 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 管理的所有应用:
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 的安装目录下),并且能监听文件的变化,自动重启服务。
更多有用的配置可以参考 PM2 官方文档,点此访问。
上面我们直接将 nodejs 项目的3000端口暴露了出去。一般我们都会使用 nginx 做一个代理转发,只对外暴露 80 端口。
首先服务器中需要安装 nginx ,有三种方式:
我这里的系统是 CentOS 8,已经更换了可用的 yum 源,可以直接安装 nginx。如果你的操作系统为 CentOS 7 或者其他发行版,可以搜索适合的安装方法。
使用 yum 安装:
# yum install -y nginx复制代码
然后启动 nginx:
# systemctl start nginx复制代码
打开浏览器访问服务器地址,可以看到 nginx 默认的主页:
为项目新建一个配置文件:
# 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
加载:
修改完配置文件,一定要重启服务:
# systemctl restart nginx复制代码
然后本地打开浏览器,去掉原来的3000端口号,直接访问完整的 url:
到这里,就完成了接口转发的配置。从用户的角度出发,这个也叫反向代理。
首先我们比较系统的讲解了为何需要在 node.js 项目中开启多进程,以及两种实现方式:
child_process
模块的 fork
方法cluster 模块
的 fork
方法之后,又讲解了如何在 Linux 服务器中安装 node 环境,以及部署一个 node.js 项目的大致流程,并着重介绍了 pm2 的使用:
最后,讲解了使用 nginx 实现接口的代理转发,将用户请求转发到本地的3000端口的服务。
至此,我们完成了本文的目标,将一个 express 项目部署到服务器,并能稳定可靠的运行。
下篇文章,我们会使用 Github Actions 实现 CI/CD,让项目的部署更加便捷高效。
本文演示代码,已上传至 Github,点击访问。
更多node相关知识,请访问:nodejs 教程!
以上是一文聊聊Node多進程模型和專案部署的詳細內容。更多資訊請關注PHP中文網其他相關文章!