顶层架构 - 消息集群推送方案

一、推送基础概念简述

在即时通讯(IM)系统中,最基础的一件事就是"如何把消息推送给用户"。为了实现这个过程,我们要先了解两种常见的网络通信方式:HTTPWebSocket

1. HTTP 是什么?

HTTP 就像一次性对话:

  • 客户端发起请求,服务端回复一次后,这条连接就断了。

  • 它是"无状态"的,也就是说服务器不会记得你上一次说了什么。

它的好处是使用简单,适合访问网页、发评论这种"说一次就好"的请求。

2. WebSocket 是什么?

WebSocket 则像"打电话":

  • 一旦连接建立,客户端和服务端可以一直保持联系,随时互相发消息。

  • 它同样是基于 TCP 的,但是比 HTTP 更适合 IM 这种"随时聊"的场景。

这种方式的好处是响应快,不用每次都重新连接。

3. 单机情况:UID 和 Channel 的绑定

在只有一台服务器的情况下,推送消息很简单:

  • 用户上线后会建立一个 WebSocket 连接,系统会记录下他的用户 ID(UID)和对应的连接通道(Channel)。

  • 当有新消息时,直接通过这个通道发出去就行。

就像是你进了聊天室,系统知道你在哪个窗口,喊你一声就能听到。

二、集群推送的基本挑战

在上节中我们提到:在只有一台服务器时,系统只要记住用户的 UID 和对应的连接通道(Channel),推送消息非常简单。

但如果用户数量多到需要部署多台 WebSocket 服务器怎么办?这时候系统进入"集群模式",问题也就随之来了。

1. WebSocket 的"局部性"问题

和 HTTP 不同,WebSocket 是"长连接",一旦用户连上了某台服务器,他的这条连接就只属于这一台服务器了。

也就是说:

  • 用户 A 连的是 Server1;

  • 如果你在 Server2 上发消息,是没办法直接推给他的。

这就像你给朋友打电话,他接电话的手机只在一台服务器上,你换台服务器就打不通了。

2. UID 和 Channel 不能中心化存储

很多人第一反应是:"我用 Redis 把 UID 和 Channel 的关系存起来就好了!"

其实不行。为什么?

  • Channel 是服务器本地的一个连接对象,不能序列化,也不能跨服务器使用

  • 换句话说,哪怕你从 Redis 拿到了 UID 对应的 Channel 标识,你也没办法在别的机器上用。

3. 如何找到用户在哪台服务器?

那怎么办?

这时候我们用 Redis 存一张 "UID ↔ 所在服务器 IP " 的表。
比如:

  • UID 1001 在 Server1;

  • UID 1002 在 Server2。

我们叫这个模块"Router 路由服务"。当需要推送时,先查 Redis 知道用户在哪台服务器,然后转发过去。

4. 路由连接数爆炸问题

但是,问题又来了。

如果有 1000 台 WebSocket 服务器,为了支持转发消息,每台 Router 服务都得跟这 1000 台服务器建立连接。

再假设我们有很多台 Router,那连接数就会变得非常庞大,连接被系统自己占满了,真正给用户的反而不够了

这种情况叫"连接数爆炸",是一种严重的性能隐患。

总结一下,在进入集群推送后,我们面临几个核心挑战:

  • WebSocket 连接是"黏在服务器"上的,不能随便切换;

  • Channel 是局部对象,无法跨服务器传递;

  • 路由虽然能找到人在哪,但会带来连接爆炸的问题。

三、路由层优化方案

上节我们说到,虽然可以通过 Redis 知道用户在哪台服务器,但如果每个路由服务都要和所有 WebSocket 服务器建连接,会导致连接数爆炸,影响性能。

那怎么优化呢?答案是:加一层中转站,让大家不用"全连接"了。

1. 加一层"中间人":消息中间件

我们可以在路由和 WebSocket 服务器之间,加一层叫做 消息中间件 的东西,常用的有:

  • Kafka

  • Redis 的发布订阅(Pub/Sub)

  • RabbitMQ 等等

这层中间件就像一个"公共广播站",大家只要:

  • WebSocket 服务器:订阅广播频道(比如订阅"用户1001的推送")

  • 路由服务:往广播频道发消息

就能做到"不需要直接连接,也能把消息推过去"。

2. 怎么发消息?

假设你要给 UID 为 1001 的用户发一条消息,流程是这样的:

  1. 路由服务查 Redis,发现 UID 1001 在 Server2;

  2. 路由服务就往"Server2 的频道"发一条消息;

  3. Server2 已经订阅了这个频道,收到后立刻推送给用户。

注意:

  • 这时候,路由服务 不需要 和 Server2 保持 TCP 连接;

  • 所有服务器只要订阅自己的频道即可,不会引发连接爆炸。

3. 多个用户怎么发?

如果你要群发给很多用户,比如 UID 列表是:[1001, 1002, 1003]

做法是:

  • 分别查每个用户所在的服务器;

  • 按服务器分组,比如 1001 和 1003 在 Server2,1002 在 Server1;

  • 给 Server1 和 Server2 各发一条消息,带上用户列表。

WebSocket 服务器拿到后,内部再去一个个推送。

小结一下:

我们通过"消息中间件"解决了连接数爆炸的问题:

  • 路由服务发消息 → 中间件;

  • WebSocket 服务器监听消息 → 收到后推送;

  • 不需要点对点连接,系统更加轻量稳定。

四、推送策略的两种选择

在给用户发消息时,我们有两种主要的推送方式,每种都有自己的适用场景:

1. 精准推送(点对点)

  • 什么是精准推送?
    就是给每个用户单独发送消息,确保消息只送到那个用户对应的服务器和连接。

  • 优点
    消息精准送达,适合私聊或者重要消息,避免不必要的数据浪费。

  • 缺点
    如果用户量很大,需要推送的次数就多,服务器压力大,可能导致延迟或宕机。

2. 集群广播(群发)

  • 什么是集群广播?
    给一个服务器(或多个服务器)广播消息,让服务器自己决定把消息发给哪些用户。

  • 优点
    推送次数少,服务器压力相对低,适合大群聊或通知类消息。

  • 缺点
    每台服务器要自己过滤哪些用户能收到消息,效率上可能稍有损耗。

什么时候用哪个?

  • 私聊和小范围消息,推荐用精准推送,保证消息不漏发。

  • 大群聊或者广播消息,推荐用集群广播,减少系统压力。

简单来说,就是:

  • 想"定点发射",用精准推送;

  • 想"全网广播",用集群广播。

五、消息过滤优化手段

在消息推送过程中,不是所有消息都要发给所有用户,过滤消息非常重要,能帮我们节省网络和服务器资源。

1. 把用户ID放在消息头部(Header)

  • 每条消息里带上目标用户的ID(UID)信息。

  • 服务器收到消息后,先看UID,只给对应的用户处理,不用把消息内容全部解析,节省时间。

2. 服务器端过滤

  • 有些消息队列系统支持在服务器端过滤消息,只有符合条件的消息才会被发送给对应的连接。

  • 这样可以减少不必要的数据传输,降低网络负担。

3. 分组或标签过滤

  • 给用户分组或打标签,比如"兴趣爱好"、"地理位置"等。

  • 发送消息时只给相关组的用户推送,避免无效发送。

4. 动态过滤的挑战

  • 动态变化的用户群体和消息目标,让过滤变得复杂。

  • 一些消息系统对动态过滤支持不好,需要通过自定义改造来实现。

六、定制化消息中间件的探索方向

消息中间件是负责帮我们传递消息的"邮递员",但有时候现成的"邮递员"不完全符合我们的需求,所以我们会考虑自己定制或改造它们。

1. 适配业务特点

不同业务对消息传递的要求不同,比如速度、可靠性、扩展性等。定制中间件能更好地满足这些需求。

2. 优化性能

定制消息中间件可以减少不必要的开销,比如精简消息格式、减少网络传输,提升整体效率。

3. 灵活过滤和路由

自定义过滤和路由规则,让消息能更精准快速地送达目标用户,减少资源浪费。

4. 解决扩展性问题

随着用户量和消息量增长,普通中间件可能瓶颈明显。定制化设计能更好地支持大规模集群和多层路由。

5. 可控性和维护性

自己定制中间件,可以更方便地监控、调试和升级,减少对第三方依赖。

相关推荐
编程星空9 分钟前
架构与UML4+1视图
架构
nlog3n21 分钟前
Go语言交替打印问题及多种实现方法
开发语言·算法·golang
kaixin_learn_qt_ing25 分钟前
Golang
开发语言·后端·golang
ddd...e_bug28 分钟前
Shell和Bash介绍
开发语言·bash
C4程序员1 小时前
Java百度身份证识别接口实现【配置即用】
java·开发语言
unityのkiven1 小时前
C++中的虚表和虚表指针的原理和示例
开发语言·c++
炒空心菜菜1 小时前
MapReduce 实现 WordCount
java·开发语言·ide·后端·spark·eclipse·mapreduce
(・Д・)ノ1 小时前
python打卡day27
开发语言·python
zy happy2 小时前
搭建运行若依微服务版本ruoyi-cloud最新教程
java·spring boot·spring cloud·微服务·ruoyi
芯眼2 小时前
STM32启动文件详解(重点)
java·开发语言·c++·stm32·单片机·mybatis