当前位置: 首页 > 后端技术 > Node.js

PM2源码分析

时间:2023-04-03 11:05:25 Node.js

最近需要了解PM2的一些功能实现,于是看了一下PM2的源码。这么多年第一次进入PM2内部探索。PM2是一个基于node.js的进程管理工具。Node.js本身是单进程语言,但是PM2可以实现多进程的运行管理(当然还是基于nodeAPI),同时也提供了程序系统信息的展示。包括内存、CPU等数据。PM2核心功能一览源码位置PM2官网功能和插件非常丰富,但核心功能不多:多进程管理系统信息监控日志管理其他功能以辅助功能为主在PM2.5上。项目结构PM2的项目结构比较简单,主要源码在lib目录,god目录是核心功能多进程管理的实现,API目录提供各种能力,包括日志管理,面板查看系统信息和各种辅助功能,最后在Sysinfo目录下实现如何收集系统信息。#删除了多个不相关的文件和文件夹lib├──API#日志管理、GUI等辅助功能├──God#多进程管理逻辑实现位置└──Sysinfo#几个关键文件的系统信息收集作用:主要逻辑Daemon.js守护进程的实现,包括rpc服务器,以及God.js业务进程的包装层,负责与守护进程建立连接,注入一些操作。我们写的代码最终是这里执行的client.js执行了PM2命令的主要逻辑实现,包括与守护进程建立rpc连接,以及API.js对守护进程的各种请求的各种功能的实现,包括启动、关闭项目、显示列表、显示系统信息等操作,会在binaries/CLI.js中调用Client的各种函数来执行pm2命令。入口文件daemon进程和Client进程的通信方式,读完源码就知道了。PM2和Client进程(也就是我们的pm2启动XXX时间对应的进程)通过RPC进行通信,这样所有的Client进程就可以和守护进程进行通信,从守护进程层面上报一些信息,进行一些操作。PM2启动程序的方式PM2并不是简单的使用节点XXX来启动我们的程序,就像上面提到的守护进程和客户端进程的通信方式一样,客户端进程会把启动业务进程所需的配置传递给守护进程由守护进程启动程序。这样当pm2启动命令执行完成后,业务进程也在后台运行,然后当我们要对业务进程进行一些操作时,可以通过list查看对应的pid和name来执行相应的操作。其逻辑是通过客户端向守护进程触发rpc请求来实现的。当然,我们很少有单独启动守护进程的操作。daemon进程的启动其实是写在client启动的逻辑中的。客户端启动时会检查是否有存活的守护进程。如果没有,它将尝试启动一个新的守护进程以供进一步使用。具体的做法是通过spawn+detached:true创建一个单独的进程,这样即使我们的Client作为父进程退出了,daemon进程依然可以在后台独立运行。附言使用PM2时,您有时会看到这样的输出。这个其实是客户端检测到daemon进程没有启动,主动启动daemon进程:>[PM2]SpawningPM2daemonwithpm2_home=/Users/jiashunming/.pm2>[PM2]PM2Successfullydaemonizedmulti-processmanagement一般使用pm2来实现多进程管理。主要目的是让我们的node程序能够运行在多核CPU上,比如四核机器。流程正在运行以获得更有效的支持服务。在进程管理方面,PM2提供了一个大家经常使用的参数:exec_mode,它只有两个值,cluster和fork。fork是一种比较常规的模式,相当于多次执行节点XXX。js。但是这样运行node程序会有问题。如果是HTTP服务,很容易出现端口冲突:consthttp=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是master-slave的运行模式(_最近ms这个词好像不太政治正确..._),首先需要有一个master进程负责创建一些工作进程,或者叫做workers.然后在worker进程中执行createServer,监听对应的端口号。consthttp=require('http')constcluster=require('cluster')if(cluster.isMaster){letlimit=2while(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算法分发请求。主进程和工作进程将通过基于EventEmitter的消息进行通信。具体逻辑实现在这里lib/internal/cluster因为是node的逻辑,不是pm2的逻辑,就不多说了。然后再回到PM2对集群的实现。其实就是设置了N个以上的默认参数,然后加上一些与进程的ipc通信逻辑,在进程启动成功、异常等特殊情况下进行相应的操作。前面说过,PM2对于所有的业务流程都是由daemon进程来维护和管理的,所以daemon进程会维护与所有服务的连接。进程对象继承自EventEmitter,所以我们只是监听一些具体的事件,包括uncaughtException、unhandledRejection等。在进程重启的实现中,子进程监听异常事件,将异常日志信息发送给daemon进程,然后发送disconnect表示进程即将退出,最后触发自己的exit函数终止进程。同时daemon进程收到消息后,也会重新创建一个新的进程,从而完成进程自动重启的逻辑。实现业务流程的主要逻辑在lib/ProcessContainer中,它是我们实际代码执行的载体。系统信息监控系统信息监控,之前看源码还以为是什么addon做的,或者什么黑科技。但是真正看了源码后,才发现是用pidusage这个包来做的--如果只关心Unix系统,内部其实就是ps-pXXX这样简单的命令。至于pm2monit和pm2ls--watch命令的使用,定时器其实就是在循环调用上面的获取系统信息的方法。具体实现逻辑:getMonitorDatadashboard列表背后是如何使用终端UI库展示数据的逻辑。PM2中日志管理日志的实现分为两部分。一个是业务流程的日志,一个是PM2守护进程本身的日志。daemon的日志实现是通过hackconsole相关的API来实现的。在原有输出逻辑的基础上,增加了基于轴突的消息传递。是pub/sub模型,主要用于客户端获取日志,比如pm2attach、pm2dashboard等命令。业务流程的日志实现方法是重写process.stdout和process.stderr对象上的方法(consoleAPI是基于它的),收到日志后写入文件,调用process.send将日志转发到同一时间。daemon进程监听相应的数据,同时也使用上面daemon进程创建的socket服务转发日志数据,使得业务进程和daemon进程有一个统一的可获取位置,一个socket连接就可以通过Client建立日志数据来实现。的输出。hack控制台位置:lib/Utility.jshackstdout/stderr写入位置:lib/Utility.js创建一个文件可写流,让子进程写入文件:lib/Utility.js子进程接收到输出并写入文件并发送消息给守护进程:lib/ProcessContainer.js守护进程监听子进程消息并转发:lib/God/ClusterMode.js守护进程通过套接字广播事件:lib/Daemon.jsClient读取和显示日志:lib/API/Extra.js中查看日志的过程中有一个小细节,就是业务日志。PM2会先读取文件的最后几行进行显示,然后根据socket服务返回的数据刷新终端显示数据。后记这些是PM2的核心部分,因为客户端可以和守护进程进行交互,守护进程和业务进程之间也有联系,可以进行一些操作。这样我们就可以很方便的管理业务流程,剩下的逻辑基本都是基于这之上的一些辅助功能,以及UI展示上的逻辑处理。PM2是一个用纯JavaScript编写的工具。第一次看的时候,还是觉得有点复杂,绕来绕去也是一头雾水。我推荐的一种阅读源代码的方法是从寻找一些入口文件开始。可以通过调试或者添加日志的方式一步步查看代码的执行顺序。最终会有一个更清晰的概念。