一、推送基础概念简述
在即时通讯(IM)系统中,最基础的一件事就是"如何把消息推送给用户"。为了实现这个过程,我们要先了解两种常见的网络通信方式:HTTP 和 WebSocket。
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 的用户发一条消息,流程是这样的:
-
路由服务查 Redis,发现 UID 1001 在 Server2;
-
路由服务就往"Server2 的频道"发一条消息;
-
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. 可控性和维护性
自己定制中间件,可以更方便地监控、调试和升级,减少对第三方依赖。