如何在 PM2 下配置 Socket.IO 集群:instances、cluster 模式与优雅关闭
【免费下载链接】socket.ioBidirectional and low-latency communication for every platform项目地址: https://gitcode.com/gh_mirrors/so/socket.io
本文解决一个部署场景的问题:用 PM2 的 cluster 模式让 Socket.IO 服务跑在多个 Node.js 进程中,使所有 worker 之间能互相广播,并在收到停止信号时优雅关闭连接而不是被直接杀掉。socket.io 仓库自带 examples/pm2-example 目录,完整给出了 ecosystem 配置、worker 入口脚本、前端验证页面和一套@socket.io/pm2管理命令,下文按该示例逐段说明。
准备条件与依赖
pm2-example的 package.json 声明了 ESM("type": "module")和以下依赖版本,可作为自己项目的依赖清单参照:
{ "type": "module", "dependencies": { "@socket.io/cluster-adapter": "^0.3.0", "@socket.io/sticky": "^1.0.4", "socket.io": "^4.8.3" } }其中两个关键包的作用:
@socket.io/cluster-adapter:在多个 Socket.IO server 之间转发广播包。根据其 README,它可与@socket.io/sticky配合使用,在同一个 Node.js cluster 的各 worker 之间广播。支持的成员功能包括 broadcasting 和 utility methods(socketsJoin、socketsLeave、disconnectSockets、fetchSockets、serverSideEmit)。@socket.io/sticky:实现 sticky session。socket.io 的 Readme 说明,断线检测依赖心跳定时器(pingInterval/pingTimeout),这些定时器要求客户端后续请求被导向同一台服务器,因此多节点部署存在sticky-session要求——这是 cluster 模式下必须调用setupWorker的原因。
Fastify 变体入口还会用到fastify ^5.8.5与@fastify/static ^9.1.3。
配置 ecosystem.config.js:instances 与 cluster 模式
PM2 侧的核心配置是 ecosystem.config.js,内容很短:
export const apps = [ { name: "pm2-example", script: "entrypoint.js", // script: "fastify-entrypoint.js", instances: "max", exec_mode: "cluster", }, ];各项含义:
name:PM2 进程名,后面的 stop/ps/cleanup 命令都用它定位进程,可按项目改名,但后续命令要同步改。script:PM2 fork 后执行的入口脚本。示例默认是entrypoint.js(原生 http server 变体);注释里那行是换成fastify-entrypoint.js的开关,两者二选一,不要同时启用。instances: "max":按 CPU 核数启动最大数量的进程实例。exec_mode: "cluster":以 Node.js cluster 模式运行各实例,sticky 的会话保持才在这组 worker 之间生效。
注意该文件用export const导出(ESM),与package.json的"type": "module"保持一致;如果用 CommonJS 项目,需要相应写成module.exports形式。
编写 worker 入口:adapter 与 sticky
以示例的 entrypoint.js 为主路径,它用原生 http server 同时托管页面和 Socket.IO:
import { readFileSync } from "node:fs"; import { createServer } from "node:http"; import { Server } from "socket.io"; import { createAdapter } from "@socket.io/cluster-adapter"; import { setupWorker } from "@socket.io/sticky"; const httpServer = createServer((req, res) => { if (req.method === "GET" && req.url === "/") { const content = readFileSync("./index.html"); res.writeHead(200, { "content-type": "text/html", }); res.write(content); res.end(); } else { res.writeHead(404).end(); } }); const io = new Server(httpServer, { adapter: createAdapter(), }); setupWorker(io); io.on("connection", (socket) => { console.log(`connect ${socket.id}`); socket.emit("nodeId", process.env.NODE_APP_INSTANCE); socket.conn.on("upgrade", (transport) => { console.log(`transport upgraded to ${transport.name}`); }); socket.on("disconnect", (reason) => { console.log(`disconnect ${socket.id} due to ${reason}`); }); });三个必须保留的调用:
new Server(httpServer, { adapter: createAdapter() })——装上 cluster adapter 后,本进程发出的广播才能被其他 worker 收到。setupWorker(io)——向 master 进程注册本 worker 并建立 sticky 所需的内部连接。process.env.NODE_APP_INSTANCE——cluster 模式(及 PM2)会给每个实例注入该环境变量作为实例编号;示例用它向客户端回发nodeId事件,是后面验证"连接确实落在不同实例上"的依据。
如果你的 HTTP 层用 Fastify,可参考 fastify-entrypoint.js:结构相同(createAdapter()+setupWorker(io)),差别在静态文件交给@fastify/static,且关闭逻辑走 Fastify 的生命周期钩子(见后文)。
启动与查看实例
示例的 package.json 用@socket.io/pm2包装了常用操作:
"scripts": { "start": "npx @socket.io/pm2 startOrReload ecosystem.config.js", "stop": "npx @socket.io/pm2 stop pm2-example", "ps": "npx @socket.io/pm2 ps", "cleanup": "npx @socket.io/pm2 delete pm2-example" }在目录里执行npm install装好依赖后:
npm run startstartOrReload表示如果名为pm2-example的应用已存在则先 reload 再启动。随后用下面的命令查看各实例的进程编号与状态:
npm run ps验证集群是否生效
验证页就是示例自带的 index.html,由 server 本身在/路径返回。浏览器打开http://localhost:<端口>/后,页面显示三个字段:
- Connection status:连接成功时变为
connected,断线时变为disconnected; - Node ID:服务端通过
socket.emit("nodeId", process.env.NODE_APP_INSTANCE)下发的实例编号; - Transport:当前传输方式,并通过
socket.io.engine.on("upgrade")在升级时刷新。
客户端连接代码(节选自 index.html):
const socket = io({ transports: ["polling", "websocket"], }); socket.on("connect", () => { $transport.innerText = socket.io.engine.transport.name; socket.io.engine.on("upgrade", (transport) => { $transport.innerText = socket.io.engine.transport.name; }); });服务端同时会在控制台输出(entrypoint.js中的日志语句):
connect <socket.id> transport upgraded to websocket disconnect <socket.id> due to <reason>判断方法:页面显示connected且 Node ID 为0到instances数量减一之间的某个数字,说明连接落在了对应 worker;重新加载几次页面,Node ID 出现变化,说明 sticky 会话在多实例间正常分配。若只看到一个固定编号且与预期实例数不符,先npm run ps确认实例数量是否符合instances: "max"的预期。
优雅关闭
示例在进程收到SIGINT时调用io.close(),等 Socket.IO 完成清理后再退出:
// graceful shutdown process.on("SIGINT", () => { io.close((err) => { process.exit(err ? 1 : 0); }); });Fastify 变体多一步"主动断开本实例连接"的逻辑,在preClose钩子里调用io.local.disconnectSockets(true)(关闭该 server 上所有活跃连接),关闭入口换成fastify.close():
fastify.addHook("preClose", (done) => { // close all active connections on this server io.local.disconnectSockets(true); done(); }); // graceful shutdown process.on("SIGINT", () => { fastify.close((err) => { process.exit(err ? 1 : 0); }); });两种写法对应不同取舍:http server 变体只关闭 Socket.IO 自身;Fastify 变体会在关闭前显式断开本地所有 socket,适合希望停止时不遗留挂起连接的部署。停止整个应用的命令是npm run stop,彻底删除 PM2 应用记录用npm run cleanup(后者会执行delete pm2-example,之后该应用不在 PM2 列表里,需重新start才能拉起)。
限制与边界
- cluster adapter 解决的是同机 cluster 的各 worker 之间的广播;
@socket.io/cluster-adapterREADME 也说明它是与@socket.io/sticky搭配、针对同一 Node.js cluster 的方案。跨机器部署属于另一类方案(README 中另列了 Postgres/Redis/MongoDB adapter 等外部项目),不在本文范围内。 - 心跳断线检测要求客户端后续请求被导向同一服务器,所以 cluster 模式下
setupWorker(io)不可省略;去掉它后跨实例的心跳行为没有保证。 - 示例入口以 ESM 编写(
import语法、"type": "module"),直接复制到自己的 CommonJS 项目时需要整体转为require形式,adapter/sticky 的调用方式不变。
【免费下载链接】socket.ioBidirectional and low-latency communication for every platform项目地址: https://gitcode.com/gh_mirrors/so/socket.io
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考