分布式环境:告警事件产生、WebSocket 连接不在同一台机器问题

发布时间:2026/8/29 18:58:31
分布式环境:告警事件产生、WebSocket 连接不在同一台机器问题 目录问题根源常见 4 种落地方案对比方案 1消息中间件广播RocketMQ / RabbitMQ / Kafka【生产最常用】方案 2Redis Pub/Sub 发布订阅轻量实时推送方案 3外部会话中心Redis 存储 WS 会话映射方案 4网关层 WebSocket 粘滞会话 共享会话存储不推荐方案 5独立 WebSocket 服务集群专门做推送服务生产环境踩坑点重点最简伪代码示例SpringBoot WebSocket RocketMQ 广播模式选型建议智慧充电业务适配贴合你之前项目场景说明 微服务多实例部署告警事件可能在实例 A 产生但用户 WebSocket 长连接挂载在实例 B本机内存无法直接推送消息导致客户端收不到告警推送。问题根源WebSocket 连接是本机内存维护每个服务实例只持有连到自己这台机器的客户端 session告警业务逻辑在另外实例产生事件本地拿不到目标用户的 WebSocket Session直接session.sendMessage()只能发给当前机器上的连接跨实例无效。常见 4 种落地方案对比方案 1消息中间件广播RocketMQ / RabbitMQ / Kafka【生产最常用】架构流程业务实例 A 产生告警事件发送消息到 MQ Topic携带userId、告警内容所有微服务 WebSocket 实例都消费该 Topic每个实例收到消息后检查本机内存是否存在该userId对应的 WebSocket Session如果本机存在该用户连接 → 执行推送不存在直接丢弃消息。优点改动小兼容现有微服务不需要额外中间件水平扩容友好。 缺点消息会广播给全部实例机器越多无效消费越多消息丢失风险WebSocket 实例重启消费过程中断会丢失告警需要消息持久化 重试。适合告警推送、实时通知允许极少量消息丢失智慧充电告警、业务事件告警非常常用。方案 2Redis Pub/Sub 发布订阅轻量实时推送流程实例 A 产生告警redis.publish(ws:alarm:{userId}, 告警JSON)每一个 WebSocket 服务实例启动时订阅对应频道实例收到订阅消息判断本机是否持有该用户 session有则推送客户端。优点延迟极低比 MQ 轻量不需要搭建 MQ 集群 缺点Pub/Sub 没有持久化WebSocket 实例掉线期间消息直接丢失实例重启丢失订阅适用对可靠性要求不高的实时通知不适合重要告警。方案 3外部会话中心Redis 存储 WS 会话映射把userId - 当前连接所在服务实例ID存 Redis用户建立 WebSocket 连接时写入 Redisws:user:{userId}instanceId断开连接删除 key实例 A 生成告警查 Redis 拿到目标用户的instanceId通过服务内部 RPC 调用定向调用对应实例 B 接口实例 B 本地取出 session完成 websocket 推送。优点精准定向消息只发给目标实例无广播浪费 缺点需要维护会话状态连接上下线要维护 Redis网络抖动会造成 Redis 数据脏数据需要 TTL 过期兜底增加 RPC 调用链路适合用户量中等需要减少无效消息的业务。方案 4网关层 WebSocket 粘滞会话 共享会话存储不推荐Nginx sticky 会话保证用户始终连同一台机器 弊端扩容、实例重启用户连接全部断开分布式环境下粘滞会话是反弹性设计生产尽量避免。方案 5独立 WebSocket 服务集群专门做推送服务单独部署一套推送服务所有 WS 连接全部连到推送集群业务服务不处理 websocket 业务服务通过 MQ/RPC 把告警全部交给推送服务处理。大型系统标准做法业务服务和长连接服务职责拆分。生产环境踩坑点重点用户断线场景用户 websocket 断开告警不能丢需要做告警持久化客户端重连后主动拉取未读告警。最佳实践推送 兜底拉取双机制。websocket 做实时推送客户端上线主动请求接口拉取离线期间告警。Session 序列化问题WebSocket Session 不能序列化绝对不能放到 Redis/MQ 传输只能传递用户 ID、业务数据在目标实例本地拿 session。消息重复MQ 重复消费前端做好幂等告警携带唯一事件 ID。连接泄露websocket 心跳检测超时主动关闭 session清理本地 Map 和 Redis 缓存。最简伪代码示例SpringBoot WebSocket RocketMQ 广播模式java// 1. WebSocket服务本地维护 userId - Session ConcurrentHashMapString, Session userSessionMap new ConcurrentHashMap(); // 2. MQ消费端所有实例都消费 public void consumeAlarmEvent(AlarmEvent event){ String userId event.getUserId(); Session session userSessionMap.get(userId); if(session ! null session.isOpen()){ session.getBasicRemote().sendText(JSON.toJSONString(event)); } // 当前机器没有该用户连接什么都不做其他实例会尝试推送 } // 3. 客户端重连兜底提供HTTP接口查询未读告警列表选型建议业务场景推荐方案告警允许少量丢失追求简单Redis Pub/Sub告警重要不能丢微服务已有 MQMQ 广播模式首选用户量大减少无效消费Redis 存会话 RPC 定向调用大型系统大量长连接独立推送服务集群智慧充电业务适配贴合你之前项目充电设备上报产生告警事件告警产生在设备服务实例 AAPP/Web 前端 websocket 连接网关后落在实例 B。采用RocketMQ 广播消费 客户端重连拉取离线告警告警入库websocket 做实时推送APP 打开主动拉取未读告警做兜底解决实例错位、用户离线问题。