关于node.js:PM2-源码分析

50次阅读

共计 4106 个字符,预计需要花费 11 分钟才能阅读完成。

近期有需要须要理解 PM2 一些性能的实现形式,所以趁势看了一下 PM2 的源码,也算是用了这么多年的 PM2,第一次进入外部进行一些摸索。
PM2 是一个 基于 node.js 的过程管理工具,自身 node.js 是一个单过程的语言,然而 PM2 能够实现多过程的运行及治理(当然还是基于 node 的 API),还提供程序零碎信息的展现,包含 内存、CPU 等数据。

PM2 的外围性能概览

源码地位
官方网站

PM2 的性能、插件十分的丰盛,但比拟外围的性能其实不多:

  1. 多过程治理
  2. 零碎信息监控
  3. 日志治理

其余的一些性能就都是基于 PM2 之上的辅助性能了。

我的项目构造

PM2 的我的项目构造算是比拟简洁的了,次要的源码都在 lib 目录下,God 目录为外围性能多过程治理的实现,以及 API 目录则是提供了各种能力,包含 日志治理、面板查看零碎信息以及各种辅助性能,最初就是 Sysinfo 目录下对于如何采集零碎信息的实现了。

# 删除了多个不相干的文件、文件夹
lib
├── API     # 日志治理、GUI 等辅助性能
├── God     # 多过程治理逻辑实现地位
└── Sysinfo # 零碎信息采集

几个比拟要害的文件作用:

  • 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 申请到守护过程,实现逻辑。

当然,咱们其实很少会有独自启动守护过程的操作,守护过程的启动其实被写在了 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,它的取值只有两个,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 加日志的形式,一步步的来看代码的执行程序。
最终就会有一个较为清晰的概念。

正文完
 0