Socket.io+负载均衡场景下高效存储Socket连接的方案咨询
负载均衡场景下WebSocket连接的高效存储方案
核心痛点
你当前用内存Map<number, Socket.socket>存储连接,在PM2多进程+负载均衡的架构下,每个进程的内存空间相互独立,跨进程无法访问其他进程里的连接实例,导致无法给连接到其他进程的用户推送消息,这是核心问题。
1. Redis发布/订阅(Pub/Sub)+ 本地连接映射
这是业内最常用的轻量方案,不需要集中存储所有连接,而是用Redis做跨进程消息中转:
- 每个PM2进程启动后,订阅Redis的专属频道(比如
socket_process_${process.pid}) - 维护一个Redis Hash表(比如
user_process_map),用户连接时记录用户ID -> 进程ID的映射,断开时删除对应记录 - 当需要给某用户推送消息时,先从Redis Hash中查找到用户所在的进程ID,然后向该进程的专属频道发送消息
- 目标进程收到消息后,从本地内存Map中取出对应的Socket连接推送内容
- 代码示例:
// 用户连接时更新Redis映射 await redisClient.hset('user_process_map', userId, process.pid); // 推送消息逻辑 const targetPid = await redisClient.hget('user_process_map', userId); if (targetPid) { await redisClient.publish(`socket_process_${targetPid}`, JSON.stringify({ userId, content: '推送内容' })); } // 进程订阅专属频道 redisClient.subscribe(`socket_process_${process.pid}`, (message) => { const { userId, content } = JSON.parse(message); const socket = localSocketMap.get(userId); socket?.send(content); }); - 优势:Redis Pub/Sub延迟极低,实时性好;本地Map保留连接实例,避免跨进程直接操作Socket的风险;资源占用少,性能高
- 注意:要处理进程异常退出的情况,比如给Redis映射设置过期时间,或者用PM2的退出钩子清理对应记录
2. 粘性会话(Sticky Sessions)
如果你的负载均衡器支持粘性会话(比如Nginx的ip_hash、Cloudflare的会话亲和性),可以让同一个用户的WebSocket连接始终路由到同一个PM2进程:
- 配置负载均衡器,根据客户端IP或用户ID做哈希路由,固定请求到某一进程
- 每个进程只需维护本地的
Map连接表,不需要跨进程通信 - Nginx配置示例:
upstream ws_backend { ip_hash; server 127.0.0.1:3000; server 127.0.0.1:3001; # 其他PM2进程端口 } - 优势:实现成本极低,不需要额外中间件;没有跨进程通信的性能损耗
- 缺点:负载可能不均(比如某IP段用户集中);进程重启时该进程的所有连接会断开,影响用户体验
3. Redis哈希表存储连接元数据(多设备场景)
如果你的用户存在多设备登录的情况,可以把连接的元数据(用户ID、进程ID、连接标识)存在Redis哈希表中:
- 每个连接生成唯一ID,将
连接ID -> { userId, pid, connInfo }存入Redis Hash - 推送消息时,根据用户ID查询所有关联的连接ID,再分别向对应进程的频道发送消息
- 这种方式可以支持一个用户多个连接的推送需求
4. 基于Socket.IO的Redis适配器(框架化方案)
如果你的项目用Socket.IO而非原生WebSocket,可以直接用官方的Redis适配器:
- 适配器会自动处理跨进程的连接同步和消息路由,无需自己维护映射表和Pub/Sub逻辑
- 代码示例:
const { Server } = require('socket.io'); const { createAdapter } = require('@socket.io/redis-adapter'); const { createClient } = require('redis'); const io = new Server(3000); const pubClient = createClient({ url: 'redis://localhost:6379' }); const subClient = pubClient.duplicate(); await Promise.all([pubClient.connect(), subClient.connect()]); io.adapter(createAdapter(pubClient, subClient)); - 优势:开箱即用,Socket.IO封装了所有集群逻辑,减少重复开发
- 缺点:依赖Socket.IO框架,原生WebSocket无法直接使用
选型建议
- 原生WebSocket项目:优先选Redis Pub/Sub + 本地Map,兼顾灵活性和性能
- Socket.IO项目:直接用Redis适配器,高效省心
- 小体量项目、对负载均衡均匀性要求不高:可以先用粘性会话快速解决问题
内容的提问来源于stack exchange,提问作者Rojith.P
相关产品推荐
相关产品推荐

