- 运维
- CLI
- 可观测性
【免费下载链接】pm2
Node.js/Typescript/Bun Production Process Manager with a built-in Load Balancer.
导读
本篇文章围绕 PM2 仓库中examples/cluster-tcp示例,讲解如何用 PM2 以集群模式(cluster mode)启动一个基于 Node.jsnet模块的 TCP 服务。你将掌握两种启动方式(ecosystem 配置文件与命令行-i max)、process.config.js中 cluster/fork 两种运行模式的差异、NODE_APP_INSTANCE实例编号的注入机制,以及 PM2 底层如何通过 Nodecluster模块实现负载均衡,并最终能独立完成 TCP 长连接服务的多进程部署与验证。
示例概览:cluster-tcp 目录结构
在仓库根目录下的 examples/cluster-tcp 中,包含四个文件:
- tcp.js:基于
net模块的 TCP 服务器,是本次集群化部署的核心应用; - process.config.js:ecosystem 配置文件,同时定义了 cluster 与 fork 两种运行模式的实例;
- http.js:同目录下的 HTTP 对比示例,用于直观对比 HTTP 与 TCP 两类服务的集群化差异;
- README.md:示例的启动说明,原文如下:
To start tcp application in cluster mode:
$ pm2 start ecosystem.config.js # OR $ pm2 start tcp.js -i max
下文将围绕这两条命令逐步展开:先剖析应用代码与配置文件,再追踪底层实现,最后给出完整可复现的验证流程。
第一步:阅读 TCP 应用代码 tcp.js
tcp.js 的完整实现如下:
var net = require('net'); var server = net.createServer(function (socket) { socket.write('Welcome to the Telnet server of the process' + (process.env.NODE_APP_INSTANCE || 'must be run on pm2')); }).listen(process.env.PORT || 8888, function() { console.log('Listening on port %s', server.address().port); });这段代码有三处为 PM2 集群化量身设计的关键点:
- 端口取自环境变量:
process.env.PORT || 8888。应用本身不硬编码端口,而是由 PM2 在启动时注入PORT环境变量(或回退到默认端口 8888)。这使同一个脚本可以被多个实例以不同端口运行,是实现“多实例不冲突”的前提。 - 实例编号标识:
process.env.NODE_APP_INSTANCE是 PM2 为每个集群实例自动注入的编号(从 0 开始)。示例用它把欢迎语输出为 "process0"、"process1" 等,方便在客户端直接辨认当前连接的是哪个实例。 - 连接即写回:每当有客户端建立连接,服务器立即向 socket 写入一行欢迎信息。配合
telnet或nc即可无代码验证集群分配效果。
第二步:两种启动方式
按示例 README,启动集群模式的 TCP 应用有两种等价方式:
# 方式一:通过 ecosystem 配置文件启动 $ pm2 start ecosystem.config.js # 方式二:直接指定脚本与实例数 $ pm2 start tcp.js -i max两种方式的语义相同:以集群模式启动,实例数取 CPU 核心数。区别在于:
- 方式一面向“可交付、可复用”的场景:所有应用级配置(名称、实例数、运行模式、环境变量、日志路径等)固化在配置文件中,团队成员共享同一份配置即可保证部署一致;
- 方式二面向“快速验证”的场景:适合临时起一个进程做冒烟测试,
-i(等价于--instances)参数直接指定实例数量。
关于instances参数的语义,可参考 lib/API/schema.json:
"instances": { "type": "number", "docDefault": 1, "docDescription": "Number of instances to be started in cluster mode" }即:不指定时默认 1 个实例;传入max时 PM2 会按可用 CPU 数量展开。此处exec_mode未显式指定时默认走 fork 模式,只有配置了 cluster 模式(或命令行带-i)才会走集群分支。
第三步:剖析 process.config.js:cluster 与 fork 同台对比
process.config.js 是本示例的精华所在,它同时声明了两个应用:
module.exports = { apps : [{ name : 'clustered_tcp', script : './tcp.js', instances : 'max', exec_mode : 'cluster', env : { PORT : 8002 } }, { name : 'forked_tcp', script : './tcp.js', env : { PORT : 8001 } }] }逐项解读:
| 字段 | clustered_tcp | forked_tcp | 说明 |
|---|---|---|---|
name | clustered_tcp | forked_tcp | 进程在pm2 ls中显示的名字,也是日志文件前缀 |
script | ./tcp.js | ./tcp.js | 同一个脚本,两种模式对比 |
instances | 'max' | 未设置(默认 1) | 集群实例数量,max表示铺满全部 CPU 核心 |
exec_mode | 'cluster' | 未设置(默认 fork) | 运行模式,决定底层走 Nodecluster还是独立进程 |
env.PORT | 8002 | 8001 | 为每个应用注入独立端口,避免多实例端口冲突 |
这个文件展示了一个非常实用的调试技巧:同一个 TCP 服务,用 cluster 和 fork 两种模式同时跑在不同端口上。客户端分别连接8001、8002端口即可直观对比两种模式的行为差异(例如 fork 模式每个进程各自独占监听端口,而 cluster 模式多个 worker 共享同一端口由 master 分发)。
第四步:底层原理——PM2 如何实现集群化
PM2 的 cluster 与 fork 两种模式在源码中由两个模块分别实现:
4.1 cluster 模式:God.nodeApp
lib/God/ClusterMode.js 中的God.nodeApp是集群模式的入口。核心动作只有一行——调用 Node 原生cluster.fork:
var cluster = require('cluster'); God.nodeApp = function nodeApp(env_copy, cb){ // 透传 node_args 到 cluster.settings.execArgv if (env_copy.node_args && Array.isArray(env_copy.node_args)) { cluster.settings.execArgv = env_copy.node_args; } // 环境信息以 JSON 字符串形式传给 fork 出的子进程 clu = cluster.fork({pm2_env: JSON.stringify(env_copy), windowsHide: true}); // ... };从源码结构可以推断出 PM2 集群模式的核心机制:PM2 的 Daemon(God)扮演 Node cluster 的 master 角色,由它fork出 N 个 worker;Node 内置的负载均衡器把到达 master 监听端口的连接按轮询(round-robin,非 Windows 平台默认)分发给各 worker。这正是 PM2 自述 "built-in Load Balancer" 的落地点。此外,源码特意将pm2_env序列化为 JSON 字符串再传入(注释解释了 Node cluster 在 fork 进程间传递嵌套对象时会被 toString 化,导致args/env结构损坏),这是保证 PM2 元数据在 worker 中完整还原的实现细节。
4.2 fork 模式:God.forkMode
lib/God/ForkMode.js 的God.forkMode则是另一条路径:它通过child_process.spawn以独立进程方式拉起应用。关键逻辑:
if (interpreter === 'node' || RegExp('node$').test(interpreter)) { args.push(path.resolve(..., 'ProcessContainerFork.js')); }即 fork 模式下 PM2 并不是直接执行用户的tcp.js,而是先启动一个包装脚本 lib/ProcessContainerFork.js(可选启用 source map 支持、注入自定义模块、发送 node_version 上报),再通过require('module')._load(process.env.pm_exec_path, null, true)加载真实应用。这也是 fork 模式进程标题被改写为node <pm_exec_path>的原因。
4.3 两种模式的对比要点
| 维度 | cluster 模式 | fork 模式 |
|---|---|---|
| 底层机制 | Nodecluster.fork,master 统一分发连接 | child_process.spawn独立进程 |
| 负载均衡 | 由 Node 内置 Load Balancer 提供 | 无(需外部负载均衡器) |
| 端口占用 | 多 worker 共享同一监听端口 | 各进程需独立端口(本例用 PORT 区分) |
| 适用场景 | HTTP / 无状态 TCP 服务 | 有状态服务、非常规解释器、需要隔离的场景 |
| 源码入口 | lib/God/ClusterMode.js | lib/God/ForkMode.js |
第五步:NODE_APP_INSTANCE 是怎么注入的
tcp.js 中读取的NODE_APP_INSTANCE,由 PM2 在启动每个实例前统一注入,实现在 lib/God.js 的God.injectVariables:
// 允许通过 instance_var 或环境变量覆盖实例编号的键名 var instanceKey = process.env.PM2_PROCESS_INSTANCE_VAR || env.instance_var; // 收集同名应用中已使用的实例编号 var instances = Object.keys(God.clusters_db) .map(...) .filter(function (proc) { return proc.pm2_env.name === env.name && ...; }).map(function (proc) { return proc.pm2_env[instanceKey]; }).sort(function (a, b) { return b - a; }); // 默认取 "最大编号 + 1",并优先复用空缺编号 var instanceNumber = typeof instances[0] === 'undefined' ? 0 : instances[0] + 1; for (var i = 0; i < instances.length; i++) { if (instances.indexOf(i) === -1) { instanceNumber = i; break; } } env[instanceKey] = instanceNumber;要点:
- 编号从 0 开始,按同名字应用内的“已用编号 + 1”递增,并会复用因重启、删除而空出来的编号;
- 键名默认为
NODE_APP_INSTANCE,可以通过配置instance_var改名(见 lib/API/schema.json 中的instance_var字段,默认值为NODE_APP_INSTANCE); - 若同时配置了
increment_var,PM2 还会注入一个按实例递增的自定义变量(schema 中描述为 “inject which increments for each cluster”),适合需要每个实例拿到不同编号做分片等场景。
第六步:启动、验证与运维
启动
$ cd examples/cluster-tcp $ pm2 start ecosystem.config.js # 或快速启动单个应用(铺满 CPU) $ pm2 start tcp.js -i max验证集群分配
pm2 ls应能看到clustered_tcp展开为多个实例(编号 0、1、2…),forked_tcp为单实例。使用终端客户端连接验证:
# 连接集群模式实例(端口 8002) $ telnet localhost 8002 # 或 $ nc localhost 8002每次新建立连接时,欢迎语中的实例编号(process0 / process1 / …)会轮换,说明连接被 Node 内置负载均衡器分发到了不同 worker。查看各实例日志可进一步确认:
$ pm2 logs clustered_tcp日志中的 "Listening on port 8002" 会来自多个不同 PID 的实例,但所有 worker 共享同一个监听端口,这正是 cluster 模式与 fork 模式的直观差异。
运维提示
pm2 reload/pm2 restart均可作用于集群应用,滚动重启时 Node 的 cluster 机制会逐个替换 worker,最大限度降低连接中断;- 集群模式下日志默认按实例拆分,可通过
merge_logs: true合并(见 lib/API/schema.json 中merge_logs字段说明); - 若应用需要固定的外部端口(如防火墙白名单),可像本示例一样为每个实例注入独立
PORT,或仅在 cluster 模式中使用单个共享端口。
第七步:TCP 集群化的注意事项
HTTP 与 TCP 在集群化上的一个重要差异值得说明:Node 内置负载均衡器对 HTTP 这类短连接、无状态的请求分发非常理想;而对于 TCP 长连接,连接一旦建立就会固定绑定到某个 worker。因此:
- 无状态协议(如文本协议、命令响应式服务)可直接套用本示例的集群模式;
- 有状态协议(如需要会话保持的协议)需自行在应用层设计粘性会话或共享存储,Node 集群机制本身不提供会话保持;
- 跨机器水平扩展时,Node 内置集群仅覆盖单机多核,多机场景仍需外置负载均衡(如 LVS、HAProxy 等)配合。
从当前仓库源码看,PM2 集群模式的职责边界正是“单机多核 + Node 原生 cluster”,这一定位在 lib/God/ClusterMode.js 的注释中也有体现("It will wrap the code and enable load-balancing mode")。
小结
通过examples/cluster-tcp示例,你可以得到一条完整的 TCP 服务集群化链路:
- 配置层:用 process.config.js 声明
exec_mode: 'cluster'与instances: 'max',用env.PORT区分实例; - 代码层:应用通过
process.env.PORT与process.env.NODE_APP_INSTANCE感知环境,无需任何 PM2 相关依赖; - 原理层:lib/God/ClusterMode.js 调用 Node
cluster.fork实现 worker 展开与内置负载均衡,lib/God.js 负责实例编号注入; - 验证层:
pm2 ls、pm2 logs加telnet/nc客户端即可确认多实例轮询分发。
如需进一步对比,同目录的 http.js 提供了 HTTP 版集群示例(监听process.env.PORT || 8000),而仓库中更完整的 HTTP 集群演示可参考 examples/cluster-http,两相对照即可全面掌握 PM2 的进程管理与负载均衡能力。
- 运维
- CLI
- 可观测性
【免费下载链接】pm2
Node.js/Typescript/Bun Production Process Manager with a built-in Load Balancer.
相关推荐
Conan服务器水平扩展:负载均衡与集群配置
Conan服务器水平扩展:负载均衡与集群配置 为什么需要Conan服务器集群? 随着团队规模扩大和项目复杂度提升,单一Conan服务器面临三大挑战:并发请求瓶颈
开发工具包管理器构建工具CLILearn GDScript From Zero社区贡献指南:如何参与翻译和代码开发
Learn GDScript From Zero社区贡献指南:如何参与翻译和代码开发 Learn GDScript From Zero是一个帮助新手从零开始学习
人工智能AI 应用桌面应用代码智能体Meshery跨集群服务发现:DNS配置与负载均衡策略
Meshery跨集群服务发现:DNS配置与负载均衡策略 在云原生环境中,随着Kubernetes集群数量的增长,跨集群服务发现成为保障微服务通信的关键挑战。传统
云原生微服务运维DevOps
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考