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

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

查看日志的流程中有一個小細節(jié),就是業(yè)務日志, PM2 會先去讀取文件最后的幾行進行展示,然后才是依據(jù) socket 服務返回的數(shù)據(jù)進行刷新終端展示數(shù)據(jù)。
后記
PM2 比較核心的也就是這幾塊了,因為通過 Client 可以與守護進程進行交互,而守護進程與業(yè)務進程之間也存在著聯(lián)系,可以執(zhí)行一些操作。
所以我們就可以很方便的對業(yè)務進程進行管理,剩下的邏輯基本就是基于這之上的一些輔助功能,以及還有就是 UI 展示上的邏輯處理了。
PM2 是一個純 JavaScript 編寫的工具,在第一次看的時候還是會覺得略顯復雜,到處繞來繞去的比較暈,我推薦的一個閱讀源碼的方式是,通過找一些入口文件來下手,可以采用 調(diào)試 or 加日志的方式,一步步的來看代碼的執(zhí)行順序。
最終就會有一個較為清晰的概念。
??愛心三連擊
1.看到這里了就點個在看支持下吧,你的「點贊,在看」是我創(chuàng)作的動力。
2.關(guān)注公眾號
程序員成長指北,回復「1」加入Node進階交流群!「在這里有好多 Node 開發(fā)者,會討論 Node 知識,互相學習」!3.也可添加微信【ikoala520】,一起成長。
“在看轉(zhuǎn)發(fā)”是最大的支持
