近期有需求須要瞭解 PM2 一些功能的實現方式,因此趁勢看了一下 PM2 的源碼,也算是用了這麼多年的 PM2,第一次進入內部進行一些探索。
PM2 是一個 基於 node.js 的進程管理工具,自己 node.js 是一個單進程的語言,可是 PM2 能夠實現多進程的運行及管理(固然仍是基於 node 的 API),還提供程序系統信息的展現,包括 內存、CPU 等數據。
源碼位置
官方網站
PM2 的功能、插件很是的豐富,但比較核心的功能其實很少:javascript
其餘的一些功能就都是基於 PM2 之上的輔助功能了。html
PM2 的項目結構算是比較簡潔的了,主要的源碼都在 lib
目錄下, God
目錄爲核心功能多進程管理的實現,以及 API
目錄則是提供了各類能力,包括 日誌管理、面板查看系統信息以及各類輔助功能,最後就是 Sysinfo
目錄下關於如何採集系統信息的實現了。java
# 刪除了多個不相干的文件、文件夾 lib ├── API # 日誌管理、GUI 等輔助功能 ├── God # 多進程管理邏輯實現位置 └── Sysinfo # 系統信息採集
幾個比較關鍵的文件做用:node
Daemon.jsgit
God.jsgithub
Client.js算法
API.jsnpm
binaries/CLI.jsapi
看源碼後會知道,PM2 與 Client 進程(也就是咱們 pm2 start XXX
時對應的進程),是經過 RPC 進行通信的,這樣就能保證全部的 Client 進程能夠與守護進程進行通信,上報一些信息,以及從守護進程層面執行一些操做。bash
PM2 並非簡單的使用 node XXX 來啓動咱們的程序,就像前邊所提到了守護進程與 Client 進程的通信方式,Client 進程會將啓動業務進程所須要的配置,經過 rpc 傳遞給守護進程,由守護進程去啓動程序。
這樣,在 PM2 start 命令執行完成之後業務進程也在後臺運行起來了,而後等到咱們後續想再針對業務進程進行一些操做的時候,就能夠經過列表查看對應的 pid、name 來進行對應的操做,一樣是經過 Client 觸發 rpc 請求到守護進程,實現邏輯。
固然,咱們其實不多會有單獨啓動守護進程的操做,守護進程的啓動其實被寫在了 Client 啓動的邏輯中,在 Client 啓動的時候會檢查是否有存活的守護進程,若是沒有的話,會嘗試啓動一個新的守護進程用於後續的使用。
具體方式就是經過 spawn
+ detached: true
來實現的,建立一個單獨的進程,這樣即使是咱們的 Client 做爲父進程退出了,守護進程依然是能夠獨立運行在後臺的。
P.S. 在使用 PM2 的時候應該有時也會看到有些這樣的輸出,這個其實就是 Client 運行時監測到守護進程尚未啓動,主動啓動了守護進程:
> [PM2] Spawning PM2 daemon with pm2_home=/Users/jiashunming/.pm2 > [PM2] PM2 Successfully daemonized
通常使用 PM2 實現多進程管理主要的目的是爲了可以讓咱們的 node 程序能夠運行在多核 CPU 上,好比四核機器,咱們就但願可以存在四個進程在運行,以便更高效的支持服務。
在進程管理上,PM2 提供了一個你們常常會用到的參數: exec_mode
,它的取值只有兩個,cluster
和fork
,fork
是一個比較常規的模式,至關於就是執行了屢次的 node XXX.js
。
可是這樣去運行 node 程序就會有一個問題,若是是一個 HTTP 服務的話,很容易就會出現端口衝突的問題:
const http = require('http') http.createServer(() => {}).listen(8000)
好比咱們有這樣的一個 PM2 配置文件,那麼執行的時候你就會發現,報錯了,提示端口衝突:
module.exports = { apps: [ { // 設置啓動實例個數 "instances": 2, // 設置運行模式 "exec_mode": "fork", // 入口文件 "script": "./test-create-server.js" } ] }
這是由於在 PM2 的實現中, fork 模式下就是簡單的經過 spawn 執行入口文件罷了。
實現位置: lib/God/ForkMode.js
而當咱們把 exec_mode
改成 cluster
以後,你會發現程序能夠正常運行了,並不會出現端口占用的錯誤。
這是由於 PM2 使用了 node 官方提供的 cluster 模塊來運行程序。
cluster 是一個 master-slave 模型的運行方式(_最近 ms 這個說法貌似變得不政治正確了。。_),首先須要有一個 master 進程來負責建立一些工做進程,或者叫作 worker 吧。
而後在 worker 進程中執行 createServer 監聽對應的端口號便可。
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 模塊關於 listen 的實現:lib/net.js
在內部實現邏輯大體爲, master 進程負責監聽端口號,並經過 round_robin 算法來進行請求的分發,master 進程與 worker 進程之間會經過基於 EventEmitter 的消息進行通信。
具體的邏輯實現都在這裏 lib/internal/cluster 由於是 node 的邏輯,並非 PM2 的邏輯,因此就不太多說了。
而後回到 PM2 關於 cluster 的實現,實際上是設置了 N 多的默認參數,而後添加了一些與進程之間的 ipc 通信邏輯,在進程啓動成功、出現異常等特殊狀況時,進行對應的操做。
由於前邊也提到了,PM2 是由守護進程維護管理全部的業務進程的,因此守護進程會維護與全部服務的鏈接。 process
對象是繼承自 EventEmitter
的,因此咱們只是監聽了一些特定的事件,包括 uncaughtException
、unhandledRejection
等。
在進程重啓的實現方式中,就是由子進程監聽到異常事件,向守護進程發送異常日誌的信息,而後發送 disconnect
表示進程即將退出,最後觸發自身的 exit
函數終止掉進程。
同時守護進程在接收到消息之後,也會從新建立新的進程,從而完成了進程自動重啓的邏輯。
實現業務進程的主要邏輯在 lib/ProcessContainer 中,它是咱們實際代碼執行的載體。
系統信息監控這塊,在看源碼以前覺得是用什麼 addon 來作的,或者是某些黑科技。
可是真的循着源碼看下去,發現了就是用了 pidusage 這個包來作的- -
只關心 unix 系統的話,內部實際上就是ps -p XXX
這麼一個簡單的命令。
至於在使用 pm2 monit
、pm2 ls --watch
命令時,實際上就是定時器在循環調用上述的獲取系統信息方法了。
具體實現邏輯:
getMonitorData
dashboard
list
後邊就是如何使用基於終端的 UI 庫展示數據的邏輯了。
日誌在 PM2 中的實現分了兩塊。
一個是業務進程的日誌、還有一個是 PM2 守護進程自身的日誌。
守護進程的日誌實現方式是經過 hack 了 console
相關 API 實現的,在原有的輸出邏輯基礎上添加了一個基於 axon 的消息傳遞,是一個 pub/sub 模型的,主要是用於 Client 得到日誌,例如 pm2 attach
、pm2 dashboard
等命令。
業務進程的日誌實現方式則是經過覆蓋了 process.stdout
、process.stderr
對象上的方法(console
API 基於它實現),在接收到日誌之後會寫入文件,同時調用 process.send
將日誌進行轉發,而守護進程監聽對應的數據,也會使用上述守護進程建立的 socket 服務將日誌數據進行轉發,這樣業務進程與守護進程就有了統一的能夠獲取的位置,經過 Client 就能夠創建 socket 鏈接來實現日誌的輸出了。
hack console 的位置: lib/Utility.js
hack stdout/stderr write 的位置: lib/Utility.js
建立文件可寫流用於子進程寫入文件: lib/Utility.js
子進程接收到輸出後寫入文件併發送消息到守護進程: lib/ProcessContainer.js
守護進程監聽子進程消息並轉發: lib/God/ClusterMode.js
守護進程將事件經過 socket 廣播: lib/Daemon.js
Client 讀取並展現日誌: lib/API/Extra.js
查看日誌的流程中有一個小細節,就是業務日誌, PM2 會先去讀取文件最後的幾行進行展現,而後纔是依據 socket 服務返回的數據進行刷新終端展現數據。
PM2 比較核心的也就是這幾塊了,由於經過 Client 能夠與守護進程進行交互,而守護進程與業務進程之間也存在着聯繫,能夠執行一些操做。
因此咱們就能夠很方便的對業務進程進行管理,剩下的邏輯基本就是基於這之上的一些輔助功能,以及還有就是 UI 展現上的邏輯處理了。
PM2 是一個純 JavaScript 編寫的工具,在第一次看的時候仍是會以爲略顯複雜,處處繞來繞去的比較暈,我推薦的一個閱讀源碼的方式是,經過找一些入口文件來下手,能夠採用 調試 or 加日誌的方式,一步步的來看代碼的執行順序。 最終就會有一個較爲清晰的概念。