electron + pm2 - 从一个问题展开:解读pm2进程管理机制

1,818 阅读4分钟

最近在用electron + pm2 来构建我司自己的产品,发现开启asar后pm2在windows系统里无法正常运行,于是尝试通过hack的方法解决这一问题,由此看了pm2 0.4.10到目前4.x.x的源码,得到不少启发。

注:本文用的是pm2 4.5.2代码,以 pm2 api方式执行

直接上问题:在asar无法使用pm2

electron有个asar压缩的开关,虽然我没有看出来打开此开关有什么必要的好处,但设置asar为true确实会导致pm2无法运行。传统技能google出来的结果都是建议设置asar为false 或者设置pm2运行为noDaemon模式,但这样操作实在失去了一个程序员的尊严,对自己使用的第三方库失去了掌控。

于是我们继续寻找,stackoverflow上有人给出了启发:

stackoverflow.com/questions/4…

You don't need to bundle node with electron. Just make sure your 'node_modules/my-module/server.js' is packaged in asar and use

cp.fork(path.resolve(__dirname, 'node_modules/my-module/server.js'))

or

cp.fork(require.resolve('my-module/server.js'))

it should work just fine.

This way electron will use bundled node, add transparent asar support to it and run script from asar archive.

If you cp.execFile('node'... you will use outside node, that doesn't support asar.

看起来是由于pm2启动Daemon进程时用的不是fork导致这一结果

Daemon - pm2的灵魂

接下来就来到本文的重点,来讲讲pm2的灵魂 ———— 守护进程。

注:noDaemon模式就是把Daemon直接关掉,这样带来的问题就是electron启动多个渲染进程的时候互相之间不知道对方用pm2启动了什么

设想一下,如果我们自己实现一个进程管理工具改如何实现。最快速:cluster+child_process+一堆事件回调。但这一做有两个致命问题

  • 如果当前主进程挂掉,通过主进程启动的子进程则失控

  • electron可以启动多个渲染进程,在其中一个渲染进程中,想获取其他渲染进程启动的子进程,实现起来不光繁琐,而且这种多个进程间毫无管理的编程方式会使业务管理变成地狱

pm2简单来说,就是通过启动一个守护进程(Daemon)的方式来统一管理子进程。每次 pm2 start/list/stop 之后都会试图通过rpc通信连接Daemon,如果发现没有Daemon,则先建立一个,之前启动过进程的pid也都会持久化到 ~/.pm2 里。下面一张图良好的展示了pm2各个核心模块间的关系

所谓Satan,在pm2 2.0.0后被拆成 Client + Daemon
God 其实就是一些指令集合,Daemon启动后,Daemon收到Client发来的指令,执行God中的方法。

1. pm2 start script 被调用
2. pingDaemon 
3. launchDaemon 启动 Daemon
4. 发送指令 prepare 至 Daemon5. Daemon 收到 prepare 指令
6. Daemon 执行 God.prepare 方法,通过spawn执行script

下面我们直接看源码

pm2/lib/Client.js 的 launchDaemon方法

...  
...
...

var child = require('child_process').spawn(interpreter, node_args, {    
    detached   : true,    
    cwd        : that.conf.cwd || process.cwd(),    
    env        : util._extend({      
        'SILENT'      : that.conf.DEBUG ? !that.conf.DEBUG : true,      
        'PM2_HOME'   : that.pm2_home    }, process.env),    
    stdio      : ['ipc', out, err]  });

...
...
...

看起来,pm2通过spawn的方式启动了Daemon,而这就是在asar压缩时无法使用pm2的原因,如果用fork则能解决

P.S. 看源码的时候,茫茫多的文件,最核心的就三个

解决问题

js这种动态语言给我们解决这类问题提供了不少便利,我们可以利用替换对象方法的方式来hack pm2.

直接上代码

const pm2 = require('pm2');pm2.Client.__proto__.launchDaemon = hackLaunchDaemon

function hackLaunchDaemon (opts: any, cb: Function) {
    ...    
    // webpack会替换掉原生的require.resolve        
    const ClientJS = path.resolve(__non_webpack_require__.resolve('pm2/lib/Daemon.js'))    
    var child = require('child_process').fork(ClientJS, node_args, {        
        detached   : true,        
        cwd        : that.conf.cwd || process.cwd(),        
        env        : util._extend({          
            'SILENT'      : that.conf.DEBUG ? !that.conf.DEBUG : true,          
            'PM2_HOME'   : that.pm2_home        }, process.env),        
        stdio      : ['ipc', out, err]    })    
    ...
}

到此成功解决

补充背景:为什么要用pm2?

想必这是很多人的疑问,pm2不是主要做负载均衡的么,为啥要跟electron结合?具体场景是什么?

electron想必接触过的同学都比较熟悉,我司主要目标就是通过electron来实现一个多进程交易系统(不做展开)。既然涉及多进程管理,肯定是需要一个管理工具,如果自己实现,

淹没在各种回调跟connercase的海洋里

.once('error', () => {})
.once('message', () => {})
.once('connect', () => {})
.once('close', () => {})
.once('SIGOUIT', () => {})
.once('disconnect', () => {})
.once('exit', () => {})
.once('listening', () => {})
.on('SIGTERM', () => {})
.on('SIGINT', () => {})
.on('SIGUP', () => {})
.on('SIGUSR2', () => {})

...

类似这样,如此多的事件connercase纷繁交织在一起,自己实现是个纯粹的成本问题。之前在linux版本阶段用的是Supervisor,到了electron,幸运的是pm2一直都在。

对于负载均衡,pm2利用cluster根据cpu数量启动多个相同的进程来实现(此处没有细看,此处吐槽一下pm2的代码在不少地方可读性是在不敢恭维,经常一个100行的大function)

而对于进程管理,最重要就那么几个业务:start,stop,  list,describe(单个),这简直就是pm2的最基础的功能。当然还有跨平台,而编写pm2的js作为世界上第二好的语言天然具有跨平台的特性,于是pm2当仁不让的成为了我们的选择