10分鐘快速過一遍PM2核心知識(shí)點(diǎn)
授權(quán)轉(zhuǎn)載自:Jiasm
https://juejin.im/post/6866081343454773262
近期有需求需要了解 PM2 一些功能的實(shí)現(xiàn)方式,所以趁勢(shì)看了一下 PM2 的源碼,也算是用了這么多年的 PM2,第一次進(jìn)入內(nèi)部進(jìn)行一些探索。
PM2 是一個(gè) 基于 node.js 的進(jìn)程管理工具,本身 node.js 是一個(gè)單進(jìn)程的語言,但是 PM2 可以實(shí)現(xiàn)多進(jìn)程的運(yùn)行及管理(當(dāng)然還是基于 node 的 API),還提供程序系統(tǒng)信息的展示,包括 內(nèi)存、CPU 等數(shù)據(jù)。
PM2 的核心功能概覽
源碼位置:
https://github.com/Unitech/pm2)
官方網(wǎng)站:
https://pm2.keymetrics.io/
PM2 的功能、插件非常的豐富,但比較核心的功能其實(shí)不多:
多進(jìn)程管理 系統(tǒng)信息監(jiān)控 日志管理
其他的一些功能就都是基于 PM2 之上的輔助功能了。
項(xiàng)目結(jié)構(gòu)
PM2 的項(xiàng)目結(jié)構(gòu)算是比較簡(jiǎn)潔的了,主要的源碼都在 lib 目錄下, God 目錄為核心功能多進(jìn)程管理的實(shí)現(xiàn),以及 API 目錄則是提供了各種能力,包括 日志管理、面板查看系統(tǒng)信息以及各種輔助功能,最后就是 Sysinfo 目錄下關(guān)于如何采集系統(tǒng)信息的實(shí)現(xiàn)了。
#?刪除了多個(gè)不相干的文件、文件夾
lib
├──?API?????#?日志管理、GUI?等輔助功能
├──?God?????#?多進(jìn)程管理邏輯實(shí)現(xiàn)位置
└──?Sysinfo?#?系統(tǒng)信息采集
幾個(gè)比較關(guān)鍵的文件作用:
Daemon.js 守護(hù)進(jìn)程的主要邏輯實(shí)現(xiàn),包括 rpc server,以及各種守護(hù)進(jìn)程的能力 God.js 業(yè)務(wù)進(jìn)程的包裹層,負(fù)責(zé)與守護(hù)進(jìn)程建立連接,以及注入一些操作,我們編寫的代碼最終是由這里執(zhí)行的 Client.js 執(zhí)行 PM2 命令的主要邏輯實(shí)現(xiàn),包括與守護(hù)進(jìn)程建立 rpc 連接,以及各種請(qǐng)求守護(hù)進(jìn)程的操作 API.js 各種功能性的實(shí)現(xiàn),包括啟動(dòng)、關(guān)閉項(xiàng)目、展示列表、展示系統(tǒng)信息等操作,會(huì)調(diào)用 Client 的各種函數(shù) binaries/CLI.js 執(zhí)行 pm2 命令時(shí)候觸發(fā)的入口文件
守護(hù)進(jìn)程與 Client 進(jìn)程通訊方式
看源碼后會(huì)知道,PM2 與 Client 進(jìn)程(也就是我們 pm2 start XXX 時(shí)對(duì)應(yīng)的進(jìn)程),是通過 RPC 進(jìn)行通訊的,這樣就能保證所有的 Client 進(jìn)程可以與守護(hù)進(jìn)程進(jìn)行通訊,上報(bào)一些信息,以及從守護(hù)進(jìn)程層面執(zhí)行一些操作。
PM2 啟動(dòng)程序的方式
PM2 并不是簡(jiǎn)單的使用 node XXX 來啟動(dòng)我們的程序,就像前邊所提到了守護(hù)進(jìn)程與 Client 進(jìn)程的通訊方式,Client 進(jìn)程會(huì)將啟動(dòng)業(yè)務(wù)進(jìn)程所需要的配置,通過 rpc 傳遞給守護(hù)進(jìn)程,由守護(hù)進(jìn)程去啟動(dòng)程序。
這樣,在 PM2 start 命令執(zhí)行完成以后業(yè)務(wù)進(jìn)程也在后臺(tái)運(yùn)行起來了,然后等到我們后續(xù)想再針對(duì)業(yè)務(wù)進(jìn)程進(jìn)行一些操作的時(shí)候,就可以通過列表查看對(duì)應(yīng)的 pid、name 來進(jìn)行對(duì)應(yīng)的操作,同樣是通過 Client 觸發(fā) rpc 請(qǐng)求到守護(hù)進(jìn)程,實(shí)現(xiàn)邏輯。
當(dāng)然,我們其實(shí)很少會(huì)有單獨(dú)啟動(dòng)守護(hù)進(jìn)程的操作,守護(hù)進(jìn)程的啟動(dòng)其實(shí)被寫在了 Client 啟動(dòng)的邏輯中,在 Client 啟動(dòng)的時(shí)候會(huì)檢查是否有存活的守護(hù)進(jìn)程,如果沒有的話,會(huì)嘗試啟動(dòng)一個(gè)新的守護(hù)進(jìn)程用于后續(xù)的使用。
具體方式就是通過 spawn + detached: true 來實(shí)現(xiàn)的,創(chuàng)建一個(gè)單獨(dú)的進(jìn)程,這樣即便是我們的 Client 作為父進(jìn)程退出了,守護(hù)進(jìn)程依然是可以獨(dú)立運(yùn)行在后臺(tái)的。
P.S. 在使用 PM2 的時(shí)候應(yīng)該有時(shí)也會(huì)看到有些這樣的輸出,這個(gè)其實(shí)就是 Client 運(yùn)行時(shí)監(jiān)測(cè)到守護(hù)進(jìn)程還沒有啟動(dòng),主動(dòng)啟動(dòng)了守護(hù)進(jìn)程:
>?[PM2]?Spawning?PM2?daemon?with?pm2_home=/Users/jiashunming/.pm2
>?[PM2]?PM2?Successfully?daemonized

多進(jìn)程管理
一般使用 PM2 實(shí)現(xiàn)多進(jìn)程管理主要的目的是為了能夠讓我們的 node 程序可以運(yùn)行在多核 CPU 上,比如四核機(jī)器,我們就希望能夠存在四個(gè)進(jìn)程在運(yùn)行,以便更高效的支持服務(wù)。
在進(jìn)程管理上,PM2 提供了一個(gè)大家經(jīng)常會(huì)用到的參數(shù):exec_mode,它的取值只有兩個(gè),cluster和fork,fork 是一個(gè)比較常規(guī)的模式,相當(dāng)于就是執(zhí)行了多次的 node XXX.js。
但是這樣去運(yùn)行 node 程序就會(huì)有一個(gè)問題,如果是一個(gè) HTTP 服務(wù)的話,很容易就會(huì)出現(xiàn)端口沖突的問題:
const?http?=?require('http')
http.createServer(()?=>?{}).listen(8000)
比如我們有這樣的一個(gè) PM2 配置文件,那么執(zhí)行的時(shí)候你就會(huì)發(fā)現(xiàn),報(bào)錯(cuò)了,提示端口沖突:
module.exports?=?{
??apps:?[
????{
??????//?設(shè)置啟動(dòng)實(shí)例個(gè)數(shù)
??????"instances":?2,
??????//?設(shè)置運(yùn)行模式
??????"exec_mode":?"fork",
??????//?入口文件
??????"script":?"./test-create-server.js"
????}
??]
}
這是因?yàn)樵?PM2 的實(shí)現(xiàn)中, fork 模式下就是簡(jiǎn)單的通過 spawn 執(zhí)行入口文件罷了。
實(shí)現(xiàn)位置:lib/God/ForkMode.js
而當(dāng)我們把 exec_mode 改為 cluster 之后,你會(huì)發(fā)現(xiàn)程序可以正常運(yùn)行了,并不會(huì)出現(xiàn)端口占用的錯(cuò)誤。
這是因?yàn)?PM2 使用了 node 官方提供的 cluster 模塊來運(yùn)行程序。
cluster 是一個(gè) master-slave 模型的運(yùn)行方式(最近 ms 這個(gè)說法貌似變得不政治正確了。。),首先需要有一個(gè) master 進(jìn)程來負(fù)責(zé)創(chuàng)建一些工作進(jìn)程,或者叫做 worker 吧。
然后在 worker 進(jìn)程中執(zhí)行 createServer 監(jiān)聽對(duì)應(yīng)的端口號(hào)即可。
const?http?=?require('http')
const?cluster?=?require('cluster')
if?(cluster.isMaster)?{
??let?limit?=?2
??while?(limit--)?{
????cluster.fork()
??}
}?else?{
??http.createServer((req,?res)?=>?{
????res.write(String(process.pid))
????res.end()
??}).listen(8000)
}
詳情可以參考 node.js 中 TCP 模塊關(guān)于 listen 的實(shí)現(xiàn):lib/net.js
在內(nèi)部實(shí)現(xiàn)邏輯大致為, master 進(jìn)程負(fù)責(zé)監(jiān)聽端口號(hào),并通過 round_robin 算法來進(jìn)行請(qǐng)求的分發(fā),master 進(jìn)程與 worker 進(jìn)程之間會(huì)通過基于 EventEmitter 的消息進(jìn)行通訊。
具體的邏輯實(shí)現(xiàn)都在這里 lib/internal/cluster 因?yàn)槭?node 的邏輯,并不是 PM2 的邏輯,所以就不太多說了。
然后回到 PM2 關(guān)于 cluster 的實(shí)現(xiàn),其實(shí)是設(shè)置了 N 多的默認(rèn)參數(shù),然后添加了一些與進(jìn)程之間的 ipc 通訊邏輯,在進(jìn)程啟動(dòng)成功、出現(xiàn)異常等特殊情況時(shí),進(jìn)行對(duì)應(yīng)的操作。
因?yàn)榍斑呉蔡岬搅耍琍M2 是由守護(hù)進(jìn)程維護(hù)管理所有的業(yè)務(wù)進(jìn)程的,所以守護(hù)進(jìn)程會(huì)維護(hù)與所有服務(wù)的連接。process 對(duì)象是繼承自 EventEmitter 的,所以我們只是監(jiān)聽了一些特定的事件,包括 uncaughtException、unhandledRejection 等。
在進(jìn)程重啟的實(shí)現(xiàn)方式中,就是由子進(jìn)程監(jiān)聽到異常事件,向守護(hù)進(jìn)程發(fā)送異常日志的信息,然后發(fā)送 disconnect 表示進(jìn)程即將退出,最后觸發(fā)自身的 exit 函數(shù)終止掉進(jìn)程。
同時(shí)守護(hù)進(jìn)程在接收到消息以后,也會(huì)重新創(chuàng)建新的進(jìn)程,從而完成了進(jìn)程自動(dòng)重啟的邏輯。
實(shí)現(xiàn)業(yè)務(wù)進(jìn)程的主要邏輯在 lib/ProcessContainer 中,它是我們實(shí)際代碼執(zhí)行的載體。
系統(tǒng)信息監(jiān)控
系統(tǒng)信息監(jiān)控這塊,在看源碼之前以為是用什么 addon 來做的,或者是某些黑科技。
但是真的循著源碼看下去,發(fā)現(xiàn)了就是用了 pidusage 這個(gè)包來做的- -
只關(guān)心 unix 系統(tǒng)的話,內(nèi)部實(shí)際上就是ps \-p XXX這么一個(gè)簡(jiǎn)單的命令。
至于在使用 pm2 monit、pm2 ls \--watch 命令時(shí),實(shí)際上就是定時(shí)器在循環(huán)調(diào)用上述的獲取系統(tǒng)信息方法了。
具體實(shí)現(xiàn)邏輯:getMonitorData dashboard
list:https://github.com/Unitech/pm2/blob/master/lib/God/ActionMethods.js#L40
后邊就是如何使用基于終端的 UI 庫(kù)展現(xiàn)數(shù)據(jù)的邏輯了。
日志管理
日志在 PM2 中的實(shí)現(xiàn)分了兩塊。
一個(gè)是業(yè)務(wù)進(jìn)程的日志、還有一個(gè)是 PM2 守護(hù)進(jìn)程自身的日志。
守護(hù)進(jìn)程的日志實(shí)現(xiàn)方式是通過 hack 了 console 相關(guān) API 實(shí)現(xiàn)的,在原有的輸出邏輯基礎(chǔ)上添加了一個(gè)基于 axon 的消息傳遞,是一個(gè) pub/sub 模型的,主要是用于 Client 獲得日志,例如 pm2 attach、pm2 dashboard 等命令。
業(yè)務(wù)進(jìn)程的日志實(shí)現(xiàn)方式則是通過覆蓋了 process.stdout、process.stderr 對(duì)象上的方法(console API 基于它實(shí)現(xiàn)),在接收到日志以后會(huì)寫入文件,同時(shí)調(diào)用 process.send 將日志進(jìn)行轉(zhuǎn)發(fā),而守護(hù)進(jìn)程監(jiān)聽對(duì)應(yīng)的數(shù)據(jù),也會(huì)使用上述守護(hù)進(jìn)程創(chuàng)建的 socket 服務(wù)將日志數(shù)據(jù)進(jìn)行轉(zhuǎn)發(fā),這樣業(yè)務(wù)進(jìn)程與守護(hù)進(jìn)程就有了統(tǒng)一的可以獲取的位置,通過 Client 就可以建立 socket 連接來實(shí)現(xiàn)日志的輸出了。
hack console 的位置:lib/Utility.js hack stdout/stderr write 的位置:lib/Utility.js 創(chuàng)建文件可寫流用于子進(jìn)程寫入文件:lib/Utility.js 子進(jìn)程接收到輸出后寫入文件并發(fā)送消息到守護(hù)進(jìn)程:lib/ProcessContainer.js 守護(hù)進(jìn)程監(jiān)聽子進(jìn)程消息并轉(zhuǎn)發(fā):lib/God/ClusterMode.js 守護(hù)進(jìn)程將事件通過 socket 廣播:lib/Daemon.js Client 讀取并展示日志:lib/API/Extra.js

查看日志的流程中有一個(gè)小細(xì)節(jié),就是業(yè)務(wù)日志, PM2 會(huì)先去讀取文件最后的幾行進(jìn)行展示,然后才是依據(jù) socket 服務(wù)返回的數(shù)據(jù)進(jìn)行刷新終端展示數(shù)據(jù)。
后記
PM2 比較核心的也就是這幾塊了,因?yàn)橥ㄟ^ Client 可以與守護(hù)進(jìn)程進(jìn)行交互,而守護(hù)進(jìn)程與業(yè)務(wù)進(jìn)程之間也存在著聯(lián)系,可以執(zhí)行一些操作。
所以我們就可以很方便的對(duì)業(yè)務(wù)進(jìn)程進(jìn)行管理,剩下的邏輯基本就是基于這之上的一些輔助功能,以及還有就是 UI 展示上的邏輯處理了。
PM2 是一個(gè)純 JavaScript 編寫的工具,在第一次看的時(shí)候還是會(huì)覺得略顯復(fù)雜,到處繞來繞去的比較暈,我推薦的一個(gè)閱讀源碼的方式是,通過找一些入口文件來下手,可以采用 調(diào)試 or 加日志的方式,一步步的來看代碼的執(zhí)行順序。
最終就會(huì)有一個(gè)較為清晰的概念。
