☰
实时消息推送系统设计:WebSocket、长连接与可靠性架构实战
2026/10/22 8:34:35 网站建设 项目流程

这几年做后端,接到过不少跟“实时消息推送”相关的需求。不管是给App加一个站内信实时提醒,还是给Web页面做订单状态滚动更新,又或者是IoT设备上报数据的实时展示,最后都会落到同一个技术上:服务端得主动把消息送到客户端手里,而不是等客户端一遍遍跑来问。

把话说得更直接一点:一个“实时消息推送系统”,就是服务端和客户端之间维持一条长期可用的双向或单向数据通道,当业务系统产生一个新事件时,这个事件能在毫秒或秒级范围内被推到目标用户/设备的终端页面上。它能解决的核心痛点是“事件发生后,用户感知的滞后性”,它能覆盖的场景包括工单状态变更、告警通知、行情刷新、协同编辑同步等等。这篇文章适合谁看?负责做后端服务、准备给现有系统加实时能力的开发者,以及正在设计Push基础设施、被连接稳定性折磨过的运维和全栈工程师。

我会把自己踩过的坑、最终落地的方案、调试过程的排查思路,都按一个项目的推进顺序整理出来,希望你看完能直接照着抄作业。

1. 先把需求聊明白:实时推送到底要解决什么问题

1.1 从一次具体业务需求说起

去年我们接到一个后台运营系统的改造需求,场景是这样的:运营人员在管理后台提交一个批量任务,这个任务会被拆成多个子任务分发给不同处理节点,每个子任务的状态会经历排队、执行中、成功、失败等多个阶段。原来实现方式很原始,前端每隔5秒发起一次查询接口,把任务列表整个拉一遍,表格重新渲染。结果任务量一上来,列表接口的QPS翻了四五倍,数据库压力飙升,而且前端轮询的间隔永远跟不上状态变化的速度——用户明明在手机上看到任务还在排队,实际上后台3秒前就完成了。整个体验就是“卡”和“慢”。

这时候就体现出实时推送的必要性了。但我们没有一上来就选型,而是先拉了产品一起把需求写了几个硬指标:第一,任务状态变化从发生到被用户看到,目标延迟不超过2秒;第二,系统要能同时维持至少1万个在线连接不会崩;第三,断网恢复后,客户端要在15秒内自动回到“在线”状态,中间漏掉的消息通过补偿接口补回来。这三个指标直接决定了方案选择和后续设计。

1.2 不要被“实时”两个字绑架

做技术的人容易一听到“实时”就兴奋,想直接上最重的方案。但实时是分档位的,不同业务场景的容忍度完全不一样。比如协同编辑的光标位置,理论上最好能做到100毫秒以内;订单状态推送这种,2秒以内用户就完全感知不到延迟了;而IM聊天消息,1秒以内的体验和100毫秒的体验差距很大,但在大部分企业内部工具里,1秒已经算“实时”了。所以第一步永远是跟业务方对齐“多快算快”,千万别自己拍脑袋定一个过度苛刻的目标,后面所有架构复杂度都跟这个数字直接相关。

我们的做法是把需求拆成三类:第一类是“变化必须立刻感知”的,比如任务完成、支付成功;第二类是“允许有一两秒缓冲”的,比如数据看板数字刷新、公告通知;第三类是“用户下次打开时能看到就行”的,比如系统升级日志。这三类里只有第一类需要走实时通道,第二类也可以用轻量级轮询兜底,第三类纯靠拉取就够了。把需求分类之后,实时推送要承接的消息量其实比想象中少得多。

1.3 推拉结合才是常态,别指望一推到底

很多第一次做推送的人有个误区:以为上了WebSocket之后,所有数据都要从服务端主动推给客户端,原来的查询接口全部废掉。这是把一个好技术用成了负担。真正的做法应该是“推送通知 + 拉取数据”:服务端推的是一条轻量级的“信号”,比如“任务ID为12345的任务状态更新为success”,客户端收到信号后,如果本地已经有最新数据,就直接更新界面;如果本地数据不全,或者怀疑消息因为断网丢失了,再主动调一次查询接口把完整数据拉到本地。

这样做的好处非常明显:推送通道上永远只跑小报文,通道压力小,延迟低;业务接口保持幂等和可重入,就算推送丢了,拉取也能兜底;客户端的UI更新逻辑统一收敛到“收到信号拉一次数据”这个动作上,不会被各种推送数据的格式差异搞晕。我们最终落地时,推送系统本质上就是一个非常可靠的“信号通知器”,具体业务数据的搬运还是走HTTP接口,这个架构决策让后面很多问题都变得简单了。

2. 协议与方案选型:WebSocket、SSE还是自己写TCP

2.1 先说说为什么放弃轮询和长轮询

轮询是最笨但最不容易出错的方案,前端用一个setInterval定时调接口。它的缺点是明显的:做不到“事件发生就通知”,只有“下一次轮询时才发现”;服务器的无效请求量跟用户数成正比,用户量涨一倍,数据库压力跟着涨一倍,这不是可扩展的方案。

长轮询是对轮询的改良:客户端发起请求后,服务端如果没有新消息就不立刻返回,而是把这个请求挂住一段时间,比如30秒,期间有消息就马上返回,没消息就等到超时返回,客户端收到响应后再重新发起下一个请求。长轮询的问题在于连接不干净、服务端需要维护大量挂起的HTTP请求,一旦某个节点重启,所有挂在它上面的长轮询请求全部断掉,客户端同时发起重连会造成“惊群效应”。所以我们在正式方案里直接否掉了这两种方案,把目光放到真正的长连接协议上。

2.2 WebSocket是Web场景下的主力

WebSocket是目前Web端实时通信的事实标准,它通过一次HTTP握手完成协议升级,之后服务端和客户端在同一个TCP连接上双向传输消息,不再有HTTP请求/响应配对的限制。它的优势有几个:浏览器原生支持,不用额外装第三方插件;报文开销小,握手完成后帧头只有几个字节,比HTTP头动不动几百字节好得多;服务端能主动往客户端发消息,这是“推送”的本质需求。

我们在浏览器端的站内信提醒、后台任务状态滚动展示,用的就是WebSocket。但要注意,WebSocket只是一个传输管道,它在公网上跑经常会遇到中间代理设备把空闲连接掐掉的场景,所以心跳、重连、消息确认这些能力必须自己在业务层做,这在后面我会详细展开说。很多人以为后端引入一个WebSocket库就完事了,真实项目里协议之上的工程问题才是大头。

2.3 SSE:被低估的简易方案

Server-Sent Events也是浏览器提供的能力,它是基于HTTP的纯单向推送,客户端通过EventSource接口订阅一个HTTP接口,服务端就能持续不断往这条响应里写数据。相比WebSocket,SSE有三大优势:第一,它在浏览器端做断线重连是自动的,不用自己写重连逻辑;第二,它走的是普通HTTP协议,对代理设备、鉴权方式都非常友好,不需要单独处理协议升级;第三,服务端实现极其简单,任何一个能输出流式响应的Web框架都能做。

它的短板也很明显:单向,客户端没法通过同一个通道给服务端发消息;浏览器对同时打开的SSE连接数量有限制(每个域差不多6个);在移动端原生应用上支持不好。所以我们的策略是:在后台Web页面里,凡是不需要客户端发消息的纯通知类场景,优先用SSE;需要双向交互的,比如工单页面里“用户提交评论并实时看到别人评论进来”,才用WebSocket。两个方案并存没有多复杂,反而让系统更轻量。

2.4 MQTT:为物联网和弱网环境准备

如果只是做Web和App内的推送,WebSocket基本够用。但我们当时的业务有一部分是移动端,而且网络环境相对复杂,有大量弱网和切网场景。这时候我想到了MQTT这个协议。MQTT是专门为物联网场景设计的轻量级消息协议,它最大的特点是基于“主题订阅-发布”模型,一个连接可以同时订阅多个主题,服务端按主题把消息路由给订阅者;它还内置了QoS等级、遗嘱消息、会话保持这些能力,断线重连后的消息续传是协议层面的能力,不用自己造轮子。

不过MQTT在原生App里需要引入SDK,而且如果需要走WebSocket协议来兼容Web端,做协议桥接又多一层复杂度。我们最终没有把它作为统一通道,而是只把它用在了硬件设备状态上报的场景里,和WebSocket通道并存。选型时不要想着“一个协议搞定所有”,不同端用不同通道、通过后端做一个统一的接入层,在真实项目里是完全正常的。

2.5 我们最终选的组合

经过对协议能力、开发成本、浏览器/App兼容性三个维度的权衡,我们最终确定了一个组合:

场景通道说明
Web端后台页面WebSocket为主,SSE为辅双向交互用WebSocket,纯通知用SSE
移动App端WebSocket(自研封装)不走MQTT,因为消息量不大,整体复杂度低
IoT设备状态MQTT主题订阅天然匹配设备分组,QoS兜底弱网
所有端兜底HTTP查询接口推送信号丢失后用拉取弥补

这个组合不是一步到位的,而是分了两个阶段演进。第一阶段只做了Web端WebSocket,验证了通道稳定性和业务闭环;第二阶段才补充了App端和设备端通道。做基础组件最忌讳一上来铺很大,你会发现调试阶段根本分不清是哪个环节出了问题。

3. 系统设计与核心细节:连接、消息、状态、可靠性

3.1 模块划分:对外接入层和内部业务层解耦

实时推送系统如果直接从业务服务里各写一套,后面一定乱成一团。我们的架构把它拆成了三个相对独立的部分:接入层、消息层、业务接入层。

接入层管理所有客户端长连接,每个连接在接入层有一个唯一标识对应一个用户ID;它负责接收客户端的订阅请求,维护连接心跳,转发上行消息。消息层是一个基于消息队列的中间分发模块,业务系统产生事件后,把事件写入队列,消息层消费队列,根据订阅关系找到应该在线的连接,把推送给下发到对应接入节点。业务接入层是业务系统和推送系统的边界,它做的事情是把业务事件翻译成一套统一的推送协议数据格式,并且决定这条消息的目标用户集合。

这样拆分之后,业务系统完全不需要关心客户端在哪台机器上、连接走的是WebSocket还是MQTT,它只需要往消息层投递一个“事件”就行。接入层和业务层中间隔着消息队列,也自然解决了“业务高峰时推送量突增”的问题——队列本身就是一个缓冲池,消息堆在队列里比堆在业务系统进程里安全得多。

3.2 在线状态管理:一个容易踩坑的环节

推送系统绕不开“用户在线不在线”这个问题。最简单的办法是拿内存存一份映射表,用户ID到连接对象的哈希表,每次用户建连往里塞、断开时删掉。单机流量小时完全没问题,但我们当时遇到一个情况:服务部署了多个实例,用户连接的WebSocket分配给哪个实例完全是负载均衡决定的,如果只在单机内存存连接映射,那用户连接落在A机器,业务系统投递过来的消息分到了B机器,B机器完全不知道这个用户在哪,推送就发不出去。

解决办法是加一个“连接注册中心”。所有连接建立起来后,接入层把“用户ID、接入节点ID、连接ID”这一组关系写入注册中心,注册中心用内存数据库存储(选型时我们用了一款成熟的支持分布式部署的内存数据库,具体产品不展开,重点是它的读写性能足够,又能跨节点共享)。当消息层要推送一条消息给某用户时,先查注册中心,找到该用户所在节点ID,再通过内部HTTP调用或消息队列将推送指令转发到那个节点,节点在本地连接表里找到真正的连接对象再下发。这里有个细节:注册中心的记录必须带最后心跳时间,心跳超时后要能自动摘除,否则僵尸记录会导致推送指令不断打到错误节点。

3.3 订阅关系:用户、主题和连接的三层映射

推送不能只按用户找人,还要解决“用户订阅了什么”的问题。我们设计里引入了一个“主题”概念:把同类型消息抽象成一个主题,例如“任务状态变更”是一个主题,“系统公告”是另一个主题。客户端建立连接后,需要显式发送一个订阅消息,把“这个连接要接收哪些主题”告诉接入层,接入层再把这个订阅关系写入状态存储。

为什么不能像普通消息队列那样让服务端直接按业务逻辑推?因为一个用户可能在多个页面/设备上登录,每个连接关心的话题不完全一样;而且支持主题订阅后,将来新加一种业务通知,客户端只需要新增一个订阅主题,服务端不需要为每个业务单独定制推送逻辑。主题设计上我们建议用两层结构,比如“task.status.user.{userId}”表示某个用户的任务状态变化,用“user.notice.{userId}”表示针对用户的通用通知。这样一条消息进来时能够同时命中多个订阅规则,消息层做一次广播拆分,命中不同用户的下发列表。

3.4 心跳、超时与断线重连:长连接的保命符

长连接最怕什么?怕一端已经断了,另一端还以为连着。TCP断链的检测非常慢,如果客户端直接拔网线或者程序崩溃,服务端可能要等好几分钟才能发现TCP超时。所以我们必须在业务层做心跳:客户端每隔30秒发一个ping帧,服务端收到后回一个pong帧;服务端同时记录每个连接的最后心跳时间,一旦超过90秒没收到任何数据,就判定这个连接死亡,主动关闭并清理注册中心里的记录。

客户端的心跳不能做得太死板,要跟网络切换场景配合。我们客户端SDK的做法是:切后台时不发心跳,恢复前台后立刻先发一个空消息探测连接是否还活着;如果探测无响应,立即走重连流程。重连必须加退避,不能断线后所有客户端在同一个时间点重连——那会让服务端瞬间被打爆。我们的退避算法是:第一次失败等500毫秒,第二次1秒,第三次2秒,最大退避到30秒封顶,同时加入一个随机抖动因子,让不同客户端的重连时间分散开。

下面是客户端SDK里退避算法的核心代码逻辑(以TypeScript风格示例):

function getReconnectDelay(retryCount: number): number { if (retryCount <= 3) { return Math.pow(2, retryCount) * 500; // 0.5s, 1s, 2s } // 超过3次后封顶到30秒,并引入随机抖动 return Math.min(30 * 1000, Math.pow(2, retryCount) * 500) + Math.floor(Math.random() * 1000); }

3.5 消息可靠性与幂等:丢消息和重复消息怎么防

一个很多人容易忽略的事实:WebSocket本身不保证消息一定送达。连接可能刚好在推送瞬间断开,消息可能发到半路网络闪断。为了保证可靠性,我们把推送通道设计成了“发送-确认”模式:服务端下发消息时带一个自增序列号,客户端收到后回一个ack;服务端如果在一定时间内没收到ack,就把这条消息标记为待补发,同时把消息持久化到Redis列表里,等客户端重连时,通过一个“补齐接口”把断线期间漏掉的消息一次性拉走。

这里要重点关注“重复消息”问题。客户端断线重连后,补齐接口可能会把之前已经收到但没来得及回ack的消息再拉一次。所以客户端必须对消息做去重:每个消息带全局唯一ID,客户端本地维护一个最近处理过的消息ID集合,收到重复ID直接丢弃。业务侧也要接受“推送消息可能重复”的现实,凡是消息触发的重要操作(例如弹窗提示、余额变更)一律做幂等校验,否则用户可能在页面同一瞬间看到两条一样的通知。

3.6 横向扩展:从单机到多节点的路由策略

最开始我们以为做推送系统只要开一台机器就够了,后来在线人数到了几千,单机的连接数、文件描述符、CPU都开始吃紧,才意识到必须做水平扩展。多节点部署后,最核心的问题就是前面提到的路由:一条推送消息进来,怎么知道目标用户的连接在哪个节点上。

我们的方案是两层路由。消息层收到业务事件后,先不直接碰连接,而是根据订阅关系算出一批目标用户ID,然后逐个查询注册中心,拿到每个用户当前所在的接入节点;如果用户在多个节点上有连接(比如PC和手机同时在线),比如,需要往每个节点都发一份;消息层把“目标节点、用户ID、消息内容”打包后投递到一台内部的消息分发交换机,每个接入节点消费自己对应的消息再推给本地连接。

扩容时只需要新增接入节点实例,配置好负载均衡策略(我们用IP哈希而不是轮询,目的是让同一用户的连接尽量落到同一节点,避免用户快速刷新时连接在节点间漂移),同时让新节点启动后自动注册到服务发现中心即可。这里有一个血的教训:接入节点缩容或发布时,一定要先通过健康检查接口把该节点的连接全部摘走,让客户端平滑切到其他节点,否则发布一次就是全体用户的一次强制断线重连。

4. 实操落地:核心流程与关键实现

4.1 连接接入流程:从握手到可推送

一个正经的连接接入流程,远不止“WebSocket握手成功”那么简单。我们当时在接入层加了一个状态机:连接建立后,客户端必须先在5秒内完成鉴权,鉴权通过后,再上报它要订阅的主题列表;如果5秒内没有完成鉴权,服务端直接断开。这样做的目的是防止有人扫描到WebSocket端口就挂着大量空连接占资源。

完整接入流程我拆成五步:

  1. 客户端携带Token发起WebSocket握手,握手参数里带上用户ID、设备ID、客户端版本号。
  2. 接入层校验Token,并检查该用户是否已被踢下线(同一用户新设备登录时旧连接是否要强退,由业务决定)。
  3. 鉴权通过后,接入层在本地创建连接对象,然后向注册中心写入在线状态记录。
  4. 客户端主动发送“订阅请求”,把需要的主题列表传给服务端,服务端更新订阅映射。
  5. 服务端返回“接入成功”响应,并立即把该用户断线期间的离线消息数量返回给客户端,提示客户端决定是否需要拉取补齐。

这套流程的每个环节都要有对应的日志和指标。线上排查问题时,第一步永远是看这个用户走到了哪一步——是握手没成功,还是鉴权挂了,还是订阅没传到。

4.2 推送下发流程:一条业务事件走完的完整链路

以“任务状态变更为已完成”为例,我把全链路拆开:

第一步,业务服务执行完更新数据库的操作后,不是直接把消息发给推送服务,而是构建一个事件对象,写入消息队列,事件对象里包含主题、目标用户ID、事件内容、幂等ID。这里有一个关键点:写入队列一定要和业务数据库更新放在同一个事务里,或者用“事务发件箱”模式,否则数据库更新成功而消息发送失败,业务状态和用户通知就永远不一致了。

第二步,消息层消费队列里的事件,根据事件里的主题和用户ID计算订阅关系,得到“这个用户当前应该在哪些节点上接收”,查注册中心得到节点列表。

第三步,消息层把推送指令投递到对应节点的本地通道。这里我们没有走HTTP二次转发,而是让消息层和接入层直接共享一个分布式消息队列的分区,每个节点只消费自己负责的用户ID段或按节点标识标记的消息,吞吐量上来后也比同步RPC稳。

第四步,接入节点收到推送指令后,在它的本地连接表里找目标连接,如果连接在线,立刻发送消息帧并记录待确认状态。如果目标连接已经下线,就把这条消息写入该用户的离线消息列表,等用户下次连接时推送。

第五步,客户端收到消息并处理成功后回ack,接入节点把待确认记录清除。如果有大量消息未收到ack,接入节点会定期重发或者触发离线补齐。

4.3 离线消息补偿:用拉取来兜底推送

即使有推送通道,也必须保证“用户断网期间漏掉的消息”在恢复后能被完整补齐。我们当时的做法是:每条推送给在线用户的消息,同时也会以“最近100条离线消息”为上限写入离线存储,存储结构用Redis的list或hash,按用户ID分key。客户端重连成功后,服务端会把离线消息数量随接入成功响应一起下发,客户端判断本地最后一条已处理消息的消息ID,通过REST接口传上来,服务端把大于这个ID的离线消息一次性返回。

这里有一个容易遗漏的设计盲区:如果客户端一直在线,只是网络抖动导致某一条消息丢了怎么办?靠ack重发机制解决,而不是把离线消息拉取逻辑混进来。离线消息拉取是给“真离线”用的,ack重发是给“假在线”用的,两个逻辑分开走,别搅到一起,否则大量消息被重复拉取,客户端的去重压力会非常大。

4.4 客户端SDK的状态管理与重连状态机

写客户端SDK是最考验细节的活。我们定义了几个状态:初始化中、已连接、连接中、重连等待、已断开。所有业务方在调用发送/订阅接口之前,SDK必须保证自己处于已连接状态;如果处于重连等待状态,调用要能缓存下来,等连接恢复后自动补发。这个“异步队列 + 状态机”的设计是保证上层业务不会把逻辑写乱的关键。

另外一个体验相关的小细节:页面切后台再回前台时,连接很可能已经被系统销毁。客户端SDK要监听应用生命周期事件,在回前台时先做一次探测(发送一个ping,看1秒内是否有响应),没有响应就主动断开并重连。不要等心跳超时再去重连,那在弱网环境下会让用户打开App后先等好几秒才能看到新消息。

function onAppResume(socket, pingTimeoutMs = 1000) { if (socket.readyState !== WebSocket.OPEN) { scheduleReconnect(0); return; } socket.send('ping'); const timer = setTimeout(() => { socket.close(); scheduleReconnect(0); }, pingTimeoutMs); socket.once('pong', () => clearTimeout(timer)); }

5. 常见问题排查与性能调优:那些坑我都替你踩过了

5.1 连接被频繁断开,到底是谁的锅

做实时推送系统,八成的时间都耗在“排查连接为什么会断”上。我总结过一套排查顺序:先看客户端日志,是不是心跳超时后自己断开重连的;再看服务端日志,是不是服务端主动close的;再看接入层和客户端之间的网络设备,是不是空闲超时时间设得太短。

一个经典案例是:有段时间所有用户每隔10分钟准时掉线一次,查了服务端没有报错,客户端也没有主动断开,后来抓包才发现是中间一条代理链路的空闲超时时间设成了600秒,而我们心跳周期是30秒,理论上是不会触发的——但问题出在某次网络抖动时,一条心跳消息卡了几秒才到达,刚好造成连续多次累计无数据时间超过某个内部窗口,代理设备以为连接空闲了就直接掐断。解决方案是:把心跳频率提高到20秒,同时让代理设备侧对长连接场景关闭或放宽空闲超时。另外也要确认自己是用了WebSocket内部的ping/pong还是应用层的心跳包,两种方式对网络设备的“保活效果”是不同的,很多中间设备不识别WebSocket控制帧,所以应用层心跳更稳。

5.2 消息延迟波动大,先查队列消费瓶颈

如果业务事件能秒到消息队列,但客户端收消息总是延迟三五秒,问题大概率出在队列消费和节点分发环节。排查重点有三个:第一,看消息队列的积压数,有没有单分区积压导致排队的现象;第二,看接入层的消息处理线程池是否打满了,打满的话帧发送会被阻塞;第三,看是否因为ack超时重发机制造成消息重复排队,重发风暴把通道塞满。

我遇到过最隐蔽的一次延迟问题是:消息队列里的事件全都带同一个目标用户ID,而这个用户长期在线不刷新页面,消费线程在查询注册中心时锁粒度太粗,导致这个用户后面所有推送事件都被串行处理了。后来把注册中心的查询改成批量并发查询,并且对单用户推送做异步化,延迟马上从3秒降到200毫秒以内。

5.3 消息丢或者重复,确认机制不能省

WebSocket默认的“发出去就完了”是不可靠的,一定要把ack和超时重发做起来。如果你发现压力测试时总有千分之一的消息丢失,不要先怀疑网络,先考虑是不是服务器在推送时正好赶上连接断开,消息没有进入离线补偿队列就丢了。我们当时就在消息写入离线队列之前没有加成功确认,导致某些并发断开场景下消息落地的时机刚好晚于连接销毁的时机,数据就没了。修复方案是:先把离线消息写入存储,再尝试在线推送,“先持久化、后投递”,顺序不能反。

重复消息的处理也一样,必须靠全局唯一的消息ID加客户端去重,不要指望服务端负责“只推一次”。在分布式系统里,“至多一次”和“至少一次”之间永远选“至少一次”,然后在上游做幂等,这是所有推送系统都逃不过的取舍。

5.4 连接数上不去:文件描述符和线程模型

在线人数到几千之后,单机连接数上不去的问题就暴露了。第一道坎是操作系统的文件描述符限制,默认1024肯定不够,要调成65535甚至更高;第二道坎是线程模型,如果用“一个连接一个线程”的老模型,几千连接就开几千线程,线程上下文切换会把CPU拖垮,必须切成epoll/事件驱动的模型。

我们在线人数破万后,接入层从多线程阻塞模型切换到了事件驱动模型,单机还能轻松保持一万五到两万连接。这里分享一个排查技巧:用ss -s查看当前TCP连接状态,如果发现TIME_WAIT数量暴涨,那不是推送系统本身的问题,而是客户端在快速重连,需要检查客户端的重连逻辑是否触发了大量新建连接,而不是复用了旧连接。

5.5 监控与告警:没有指标就看不见问题

推送系统上线后,至少要把以下几个指标盯住:

指标说明告警阈值建议
在线连接数当前所有节点连接总和低于历史均值30%或高于最大容量80%告警
心跳超时率超时连接数 / 总连接数超过5%持续5分钟告警
推送消息量每分钟下发帧数突增3倍以上提示异常
推送成功率ack成功数 / 下发总数低于99%持续3分钟告警
推送队列积压队列中未消费消息数持续上涨且超过阈值告警
客户端重连频率单位时间重连次数单个用户重连频率异常时重点观察

有了这些指标后,排查问题就不再是“猜”,而是先看哪个指标异常,再顺藤摸瓜定位具体链路环节。我们还额外做了一条“全链路模拟监控”:每隔1分钟,系统自动模拟一个正常用户连接、订阅、接收一条测试推送,测量从发起到收到ack的端到端延迟,这条链路能第一时间发现“系统看着正常但实际已经推不出消息”的致命故障。

结尾再分享一点我自己的一些体会

这个项目从零到一做完,最大的感受不是WebSocket协议多难写,而是“维护长连接系统“真正难在看不见的细节:连接静默断开、推送消息刚好错位、服务端发布导致全体重连,每一样都是在压力测试和线上故障里学到的。

如果你现在正要启动类似项目,我给三个很朴素的建议。第一,第一版一定把离线存档和补偿拉取接口做出来,哪怕推送主链路做简单些,有了兜底你在线上才不会心虚。第二,把客户端的连接状态、日志、埋点做完整,线上排查长连接问题没有客户端日志等于盲人摸象。第三,消息格式和序列化方式从第一天就定好版本号,推送到客户端的消息协议以后一定会变,缺了版本兼容性,将来每次升级都会引出“老版本用户收不到新消息”的骂声。

希望这套思路能帮你少踩几个坑。实时推送的落地没有一个放之四海而皆准的模板,按业务实时性诉求去匹配协议和架构复杂度,把可靠性兜底做扎实,比一味追求“新技术”更重要。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询