PM2发布于2013年,是一款使用JavaScript开发的进程管理器,主要用于Node.js业务持久化。相较于Systemd、Supervisord等通用进程管理器,PM2对JavaScript业务更加友好、易用、扩展性丰富,对非JavaScript业务的管理也非常出色。可惜很多PM2用户对PM2了解不多。大多数用户只掌握基本的流程管理。其实PM2的能力远不止于此。充分利用PM2可以大大提高业务开发和维护的效率。本文将列举出PM2中十个这样冷门但实用的功能,希望能帮助读者对PM2有一个全新的认识。本文原发于某匿名网站,作者本人同步至SegmentFault。转载请注明原作者博客地址或此链接,谢谢!提前说明:本文使用PM2的命令行方式进行讲解,但以下内容对API调用同样有效。命令行与API的转换规则,可以阅读官方文档。0x01自动保存通常我们希望PM2在启动时自动启动。我们需要执行pm2startup,将其注册到操作系统的服务管理工具中。添加或删除进程后执行pm2save,但是如果你和笔者一样记性不好,可能会忘记执行这一步,导致pm2重启后没有业务启动。那么有没有办法自动执行这个“冗余”步骤呢?答案是肯定的:pm2setpm2:autodumptrue在Shell中输入这行命令,我们会开启pm2的自动保存功能,这样我们对进程的修改会立即保存到~/.pm2/dump.pm2,不需要手动执行pm2save。这里我们使用命令pm2set。其实这条命令执行的是对~/.pm2/module_conf.json的修改。该文件是PM2下各个模块的通用配置文件,安装其他PM2模块(如反向代理、负载均衡服务器等)时也可以访问该文件。但是对于PM2本身,目前可供我们使用的配置项只有autodump、registry、docker这三个,并且没有集中的文档来描述。有兴趣的读者可以阅读这三个配置项的源码实现,这里不再赘述:pm2:autodumplib/API/Startup.js#L401pm2:registrylib/API/Modules/TAR.js#L319pm2:dockerlib/API。js#L15510x02Outputlogstofiles某些服务可能是为了直接把日志输出到stdout和stderr很方便。直接在shell中运行时,我们可以使用重定向字符>比如Linux和MacOS将stdout输出到一个文件中,然后使用2>&1将stderr输出到stdout。但是假设这样一个“省事”的业务在生产环境中,我们需要使用pm2来运行,看到日志怎么办呢?pm2也为我们提供了日志重定向的功能:pm2start--log[fille]...是的,只需要在启动进程时指定--log参数,并提供日志文件的路径(是否为这个文件存在与否无所谓),就可以将stdout和stderr输出到我们指定的日志文件中。接下来我们可以使用tail等工具来跟踪日志,也可以使用pm2自带的日志显示功能:pm2logs[id]这里的id就是执行pm2psprocess-id看到的。0x03设置内存限制也许你需要在PM2中运行一个内存管理不善的程序,但又不希望这个程序在发生内存泄漏后耗尽所有资源,影响其他进程。这时候pm2的内存限制功能就可以派上用场了:pm2start--max-memory-restart=1024M...这里的单位可以是K(iB)、M(iB)和G(iB)。使用该参数启动进程后,PM2会在进程内存使用率超过限制时强制重启进程,对于一些存在内存泄漏但又不好解决(或者不需要解决)的业务来说非常实用。0x04查看一个进程的信息通常我们可以使用pm2ps查看所有当前正在运行的进程,但是这个命令只显示最基本的信息,比如环境变量、运行入口、运行参数等信息都没有在列表显示.那么我们应该如何看待这些信息呢?pm2提供了这样一个方法:pm2show[id]pm2会输出这个进程的所有信息,如下图:0x05使用概览面板监控所有进程上一节我们提到使用pm2show查看详情一个进程的信息,但是在生产环境中我们更需要监控所有的进程,包括CPU、内存使用、日志输出等信息,PM2还提供了如下命令来帮助我们监控所有进程:pm2monitPM2会启动一个面板,如下图所示:面板可以分为四个部分:进程列表(左上角)、当前进程日志(右上角)、当前进程性能信息(左下角)、当前进程基本信息(右下角)角落)。我们可以使用键盘上的左右方向键切换面板,使用上下方向键滚动面板,监控所有进程。事实上,PM2Plus还提供了资源使用历史、内存/CPU详细分析(Profiling)等额外的高级功能,但由于此功能需要付费,在此不再赘述。愿意付费的读者可以参考官方文档:PM2Plus文档。0x06使用SourceMap获取错误位置刚才我们讲了那么多“非标准”的业务(比如日志输出到stdout,内存泄漏等),我们举一个“标准”的例子,就是使用Webpack(或其他构建工具)将JavaScript代码压缩后上线业务。如果线上这些业务有错误,但是因为代码被压缩,错误只能显示在第一行(只有一行),如何在日志中看到更详细的信息呢?pm2考虑到这一点,提供了自动加载SourceMap的功能:pm2start--source-map-support...假设你加载的js文件是index.js,开启SourceMap支持后,PM2会自动搜索相同的下面的目录index.js.map,并在出错时加载,在日志中输出更易读的错误日志。下面是一个示例://error.jssetInterval(function(){triggerError();},1000);functiontriggerError(){thrownewError("Someerror...");}这里笔者使用Webpack生成对应的error.min.js和error.min.js.map文件:webpack./error.js-oerror.min.js--devtoolsource-map--output-source-map-filenameerror.min.js。map然后用pm2加载error.min.js(不启用SourceMap):官网文档有错漏。关闭SourceMap支持应该是--disable-source-map-support而不是--disable-source-map。作者提交了相关的Revision:pull/185。pm2start./error.min.js--disable-source-map-support可以看到报错信息只出现在第一行(但显然问题不在第一行)。接下来,我们开启SourceMap支持,再次运行:pm2start./error.min.js--source-map-support此时,我们可以从日志中看到正常的调用栈信息,帮助我们更高效的跟踪问题的起源。0x07业务更新时自动重启进程在业务开发和测试过程中,我们经常会遇到文件更新后需要重启业务的情况。对于本地环境,我们可以使用WebpackDevServer等工具来监控文件变化,然后在文件变化后重新运行服务器。而pm2也提供了一个类似的功能来帮助我们实现这个需求:pm2start--watch这样只要当前目录下的任何文件发生变化,pm2都会尝试重启进程。使用该参数时,有几点需要注意:请在程序所在目录下执行启动命令,否则监控的不是程序所在目录,而是你所在的目录。当前所在的执行目录。启用--watch参数后,即使手动停止进程(不删除),文件变化后进程也会自动启动。解决这个问题的方法是在停止进程的时候加入如下参数:pm2stop[id]--watch如果我们需要忽略某些目录(比如临时文件)的变化或者只监听某些目录的变化,我们需要使用PM2的API:PM2Ecosystem,然后将如下配置文件写入ecosystem.config.js(摘自官方文档):module.exports={apps:[{script:"app.js",watch:["server","client"],//重启之间的延迟watch_delay:1000,ignore_watch:["node_modules","client/img"],watch_options:{"followSymlinks":false}}]}0x08更智能的故障重启策略相信很多使用过PM2或者Docker的读者都遇到过业务运行出错后不断自动重启的问题。但是很多时候运行时的错误并不是来自于业务本身,比如数据库服务器中断,连接数过多,甚至上面提到的--watch参数过于敏感(很多IDE或者编辑器都支持自动保存,保存的版本不一定尚未开发完成,但存在语法错误)。那么有没有延时重启、无缝重启等功能呢?pm2提供了大量的相关选项:1.fixeddelaypm2start--restart-delay=2000...这里2000的单位是毫秒,即需要重启时等待两秒。2、灵活的延迟很多时候,我们需要的是一个不断延长的重启时间。例如Filezilla连接FTP客户端失败后的重试时间会随着重试次数的增加而不断延长。pm2也提供了这样一个功能:pm2start--exp-backoff-restart-delay=1000这里的1000单位也是毫秒,pm2会在多次重启失败后以设定的时间作为初始值,使用指数移动平均算法不断延长重试时间至15000毫秒(15秒),并在进程启动成功30秒后将重试时间重置为初始值。该算法的具体实现可以参考PM2的相关源码:lib/God.js#L456lib/Worker.js#L1623。零延迟高可用重启总是需要时间的。如果我们想让业务不间断重启,就像Kubernetes的滚动更新一样,应该怎么做呢?PM2的集群模式可以帮助我们实现这个需求。需要注意的是,我们这里所说的“集群”并不是像Kubernetes那样逻辑上独立的服务器集群,而是Node.js原生支持的Cluster组件。对Cluster组件不熟悉的读者可以阅读这篇Node.js官方文档:Cluster|Node.jsv14.2.0文档。简而言之,Cluster组件类似于PHP中的FPM或者Nginx中的Worker。它为单线程JavaScript运行时添加了在多个CPU上并行接收请求的能力,即将多个实例作为子进程运行。一个父进程负责请求调度。但是即使你不了解Cluster组件或者不想在现有业务中加入Cluster支持,也没关系,因为PM2帮你实现了,这样我们就可以充分利用Cluster的功能组件无需对现有源代码进行任何修改。实现高可用,任何一个进程停止都不会影响整个业务。这就是作者在本节中提到的“零延迟高可用性”。这里我以一个非常简单的网络服务器为例:varhttp=require("http");http.createServer(function(request,response){response.writeHead(200,{'Content-Type':'text/plain'});response.end('HelloWorld\n');}).listen(8081);然后我们使用如下命令启动一个有四个进程的Cluster(因为我的电脑刚好是四核):pm2start./server.js-i4这里的4是进程数,也可以设置为max为匹配当前环境中的最大核心数。接下来在pm2ps的结果中可以看到如下四个子进程:需要注意的是,Cluster模式下重启业务需要reload,不能使用进程ID(因为我们需要重启一组processes而不是一个),如下图:pm2reloadserver这里的server是上面pm2ps结果中进程组共享的名字。这样就可以充分利用服务器的性能,实现业务的高可用,而我们需要花的只是多加一个-i参数。4、关闭失败重启功能有时候我们也会用pm2做一些不需要一直在后台运行的耗时业务,比如爬虫。pm2默认进程退出后会重启,但是它也提供了参数帮我们关闭这个功能:pm2start--no-autorestart...使用这个参数后,当业务退出时,状态会直接变成stop而不是将自动重新启动。0x09一条命令操作一组业务上一节我们提到Cluster可以批量生成进程并进行管理,但是Cluster只会生成一批相同的进程。常规业务(尤其是现在微服务当权)可能由多个流程组成。这里笔者假设有一个业务叫赤狐,它包括四个组件:API(api.js)、服务端渲染(ssr.js)、数据库(db.js)、监控(monitor.js)。如何批量管理它们(比如重启)?细心的读者可能已经看到上面pm2ps输出中的namespace字段,默认为default。其实这就是本节的重点内容:命名空间。我们可以使用命名空间对同一类业务进行分类,然后按照命名空间对业务进行批量管理:pm2startapi.js--namespacechihupm2startssr.js--namespacechihupm2startdb.js--namespacechihupm2startmonitor.js--namespacechihu这时候我们可以看到这四个进程的命名空间都是chihu。如果我们需要停止这四个业务,我们不需要一个一个停止,只需要执行一条命令:pm2stopchihu就可以同时停止四个进程。其他操作也类似,只要在对应命令的--help面板中看到namespace参数即可,如下图:这里的美食只是作者的废话。写完后,我搜索了一下。没想到真的有一个叫吃湖的网站:吃湖——发现身边的美食。如有雷同,纯属巧合。0x10PM2内置的HTTP服务器最后,我想介绍一个非常有用但很少被提及的功能:HTTP服务器。在和很多同学讨论大型前端项目的前后端分离时,发现他们大多使用Nginx、Apache甚至Tomcat来托管前端静态页面,然后再使用PM2来托管后端API。但是为一个简单的前端页面写一堆配置文件,实在是浪费时间。PM2可能已经发现了这一点,所以它内置了一个HTTP服务器:pm2serve[path][port]是的,就是这样很简单,一条命令就可以启动一个HTTP服务器。由于这个HTTP服务器是使用Node.js实现的,所以它的性能也非常好,在大多数情况下已经足够了。如果是负载非常大的业务,一般不会考虑PM2,而是会使用扩展性更强的Kubernetes。以上就是笔者分享的PM2十大冷门但实用的功能。其实这十个功能在阅读官方文档和源码的过程中是可以理解的,在学习过程中总结和掌握这些小技巧,在实际使用过程中对提高效率会有很大的帮助。限于篇幅,一些更高级的功能(如Load&Dump、PM2Plus)本文不再赘述。如果读者想了解更多,请不要犹豫,马上阅读文档和相关源码吧!相信看完这篇文章,你的收获一定会更多。静心阅读永远是最高效、最深刻、最细致的学习方式。
