☰
PM2 集群模式启动 TCP 服务:从 ecosystem 配置到 Node 负载均衡原理
2026/9/30 1:48:20 网站建设 项目流程
  • 运维
  • CLI
  • 可观测性

【免费下载链接】pm2

Node.js/Typescript/Bun Production Process Manager with a built-in Load Balancer.

项目地址:https://gitcode.com/gh_mirrors/pm/pm2
点击查看免费下载

导读

本篇文章围绕 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 集群化量身设计的关键点:

  1. 端口取自环境变量:process.env.PORT || 8888。应用本身不硬编码端口,而是由 PM2 在启动时注入PORT环境变量(或回退到默认端口 8888)。这使同一个脚本可以被多个实例以不同端口运行,是实现“多实例不冲突”的前提。
  2. 实例编号标识:process.env.NODE_APP_INSTANCE是 PM2 为每个集群实例自动注入的编号(从 0 开始)。示例用它把欢迎语输出为 "process0"、"process1" 等,方便在客户端直接辨认当前连接的是哪个实例。
  3. 连接即写回:每当有客户端建立连接,服务器立即向 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_tcpforked_tcp说明
nameclustered_tcpforked_tcp进程在pm2 ls中显示的名字,也是日志文件前缀
script./tcp.js./tcp.js同一个脚本,两种模式对比
instances'max'未设置(默认 1)集群实例数量,max表示铺满全部 CPU 核心
exec_mode'cluster'未设置(默认 fork)运行模式,决定底层走 Nodecluster还是独立进程
env.PORT80028001为每个应用注入独立端口,避免多实例端口冲突

这个文件展示了一个非常实用的调试技巧:同一个 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.jslib/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。因此:

  1. 无状态协议(如文本协议、命令响应式服务)可直接套用本示例的集群模式;
  2. 有状态协议(如需要会话保持的协议)需自行在应用层设计粘性会话或共享存储,Node 集群机制本身不提供会话保持;
  3. 跨机器水平扩展时,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 调用 Nodecluster.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.

项目地址:https://gitcode.com/gh_mirrors/pm/pm2
点击查看免费下载

相关推荐

上一篇:Laravel Boost终极指南:15+ MCP工具加速AI辅助开发
下一篇:DeepSeek Harness 模型提供方配置指南:从官方路由、目录提供方到自定义网关与兼容性调优

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询