PM2源碼分析

近期有需求須要瞭解 PM2 一些功能的實現方式,因此趁勢看了一下 PM2 的源碼,也算是用了這麼多年的 PM2,第一次進入內部進行一些探索。
PM2 是一個 基於 node.js 的進程管理工具,自己 node.js 是一個單進程的語言,可是 PM2 能夠實現多進程的運行及管理(固然仍是基於 node 的 API),還提供程序系統信息的展現,包括 內存、CPU 等數據。javascript

PM2 的核心功能概覽

源碼位置
官方網站html

PM2 的功能、插件很是的豐富,但比較核心的功能其實很少:java

  1. 多進程管理
  2. 系統信息監控
  3. 日誌管理

其餘的一些功能就都是基於 PM2 之上的輔助功能了。node

項目結構

PM2 的項目結構算是比較簡潔的了,主要的源碼都在 lib 目錄下, God 目錄爲核心功能多進程管理的實現,以及 API 目錄則是提供了各類能力,包括 日誌管理、面板查看系統信息以及各類輔助功能,最後就是 Sysinfo 目錄下關於如何採集系統信息的實現了。git

# 刪除了多個不相干的文件、文件夾
lib
├── API     # 日誌管理、GUI 等輔助功能
├── God     # 多進程管理邏輯實現位置
└── Sysinfo # 系統信息採集
複製代碼

幾個比較關鍵的文件做用:github

  • Daemon.js
    • 守護進程的主要邏輯實現,包括 rpc server,以及各類守護進程的能力
  • God.js
    • 業務進程的包裹層,負責與守護進程創建鏈接,以及注入一些操做,咱們編寫的代碼最終是由這裏執行的
  • Client.js
    • 執行 PM2 命令的主要邏輯實現,包括與守護進程創建 rpc 鏈接,以及各類請求守護進程的操做
  • API.js
    • 各類功能性的實現,包括啓動、關閉項目、展現列表、展現系統信息等操做,會調用 Client 的各類函數
  • binaries/CLI.js
    • 執行 pm2 命令時候觸發的入口文件

守護進程與 Client 進程通信方式

看源碼後會知道,PM2 與 Client 進程(也就是咱們 pm2 start XXX 時對應的進程),是經過 RPC 進行通信的,這樣就能保證全部的 Client 進程能夠與守護進程進行通信,上報一些信息,以及從守護進程層面執行一些操做。算法

PM2 啓動程序的方式

PM2 並非簡單的使用 node XXX 來啓動咱們的程序,就像前邊所提到了守護進程與 Client 進程的通信方式,Client 進程會將啓動業務進程所須要的配置,經過 rpc 傳遞給守護進程,由守護進程去啓動程序。
這樣,在 PM2 start 命令執行完成之後業務進程也在後臺運行起來了,而後等到咱們後續想再針對業務進程進行一些操做的時候,就能夠經過列表查看對應的 pid、name 來進行對應的操做,一樣是經過 Client 觸發 rpc 請求到守護進程,實現邏輯。npm

固然,咱們其實不多會有單獨啓動守護進程的操做,守護進程的啓動其實被寫在了 Client 啓動的邏輯中,在 Client 啓動的時候會檢查是否有存活的守護進程,若是沒有的話,會嘗試啓動一個新的守護進程用於後續的使用。
具體方式就是經過 spawn + detached: true 來實現的,建立一個單獨的進程,這樣即使是咱們的 Client 做爲父進程退出了,守護進程依然是能夠獨立運行在後臺的。api

P.S. 在使用 PM2 的時候應該有時也會看到有些這樣的輸出,這個其實就是 Client 運行時監測到守護進程尚未啓動,主動啓動了守護進程:bash

> [PM2] Spawning PM2 daemon with pm2_home=/Users/jiashunming/.pm2
> [PM2] PM2 Successfully daemonized
複製代碼

多進程管理

通常使用 PM2 實現多進程管理主要的目的是爲了可以讓咱們的 node 程序能夠運行在多核 CPU 上,好比四核機器,咱們就但願可以存在四個進程在運行,以便更高效的支持服務。
在進程管理上,PM2 提供了一個你們常常會用到的參數: exec_mode,它的取值只有兩個,clusterforkfork 是一個比較常規的模式,至關於就是執行了屢次的 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 的,因此咱們只是監聽了一些特定的事件,包括 uncaughtExceptionunhandledRejection 等。
在進程重啓的實現方式中,就是由子進程監聽到異常事件,向守護進程發送異常日誌的信息,而後發送 disconnect 表示進程即將退出,最後觸發自身的 exit 函數終止掉進程。
同時守護進程在接收到消息之後,也會從新建立新的進程,從而完成了進程自動重啓的邏輯。

實現業務進程的主要邏輯在 lib/ProcessContainer 中,它是咱們實際代碼執行的載體。

系統信息監控

系統信息監控這塊,在看源碼以前覺得是用什麼 addon 來作的,或者是某些黑科技。
可是真的循着源碼看下去,發現了就是用了 pidusage 這個包來作的- -
只關心 unix 系統的話,內部實際上就是ps -p XXX這麼一個簡單的命令。

至於在使用 pm2 monitpm2 ls --watch 命令時,實際上就是定時器在循環調用上述的獲取系統信息方法了。

具體實現邏輯: getMonitorData dashboard
list

後邊就是如何使用基於終端的 UI 庫展示數據的邏輯了。

日誌管理

日誌在 PM2 中的實現分了兩塊。
一個是業務進程的日誌、還有一個是 PM2 守護進程自身的日誌。

守護進程的日誌實現方式是經過 hack 了 console 相關 API 實現的,在原有的輸出邏輯基礎上添加了一個基於 axon 的消息傳遞,是一個 pub/sub 模型的,主要是用於 Client 得到日誌,例如 pm2 attachpm2 dashboard 等命令。
業務進程的日誌實現方式則是經過覆蓋了 process.stdoutprocess.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 加日誌的方式,一步步的來看代碼的執行順序。 最終就會有一個較爲清晰的概念。

相關文章
相關標籤/搜索