# 实时通讯 架构模板 > **代表产品**:WhatsApp、微信、Slack、各类 IM / 群聊 > **一句话定位**:让一条消息又快、又不丢、又有序地从一个人到达另一个人(或一群人)。 --- ## 1. 一句话定位 一个实时通讯产品 = **一张「永远在线的消息投递网」**:几百万、上千万人同时挂着一条「随时能收到推送」的长连接,系统要保证任何人发出的每一条消息,**都尽快、不丢、按正确顺序**送到该收的人手里——对方在线就立刻送达,不在线就先存好、等他上线再补。 架构上最反直觉的一点:难点**不在「发」,而在「维持海量长连接」和「保证投递的可靠与有序」**。一条消息很小,但「让千万条持久连接同时挂着、还能精准找到要把消息推给哪条连接」,以及「在网络随时会断的现实里做到不丢、不重、不乱序」,会把整套架构逼向有状态连接、序号、确认重试这些核心机制。 ## 2. 业务本质:它在解决什么问题 用户要的是**「我发的消息,对方很快就能看到;对方发的,我立刻就收到;一条都不能丢、顺序还不能乱」**。它取代的是「打电话/发邮件」那种要么打断、要么很慢的沟通——IM 提供的是**即时、异步、有记录**的连续对话。 价值与钱从哪来: - **粘性与网络效应**:你的关系链都在这,迁移成本极高,这是最强的护城河; - **企业版 / 团队协作**(如 Slack):按席位订阅,卖的是团队沟通效率; - 增值:表情、会员、企业服务、平台生态。 **关键事实:这类系统的负载形态是「海量并发的持久连接 + 持续的小消息流」,而不是「短平快的请求-响应」。** 普通网站的连接「来了就走」,这里的连接「来了就一直挂着」。这一条决定了为什么「长连接网关」「连接是有状态的」「怎么找到该把消息推给哪台机器上的哪条连接」会成为架构的核心命题。 ## 3. 核心需求与约束 **功能性需求(系统要能做什么):** - [ ] 发送 / 接收消息:一对一、一对多(群)。 - [ ] 实时送达:对方在线时,消息近乎瞬间到达。 - [ ] 离线消息:对方不在线时,消息不丢,上线后补齐。 - [ ] 消息有序:同一会话里,消息按发送顺序呈现。 - [ ] 可靠投递:不丢、不重(发出去的最终都到、且只呈现一次)。 - [ ] 在线状态(Presence):显示对方在线/离线/正在输入。 - [ ] 群组:成员管理 + 群消息扩散到每个成员。 - [ ] 多媒体:发图片、语音、文件。 - [ ] 多端同步:手机、电脑同时在线,消息和已读状态一致。 **非功能性需求 / 质量属性(这才是架构的主战场):** | 质量属性 | 目标 | 为什么对这类系统重要 | |---|---|---| | **消息时延** | 在线时 < 几百毫秒 | IM 的灵魂就是「即时」,慢了就不叫即时通讯。 | | **可靠投递** | 不丢、不重 | 丢一条重要消息=信任崩塌。这是底线。 | | **消息有序** | 单会话内严格有序 | 顺序乱了对话就读不懂(「好啊」出现在「要不要吃饭」之前)。 | | **海量并发连接** | 百万~千万级长连接 | 连接数是这类系统独有的核心规模指标。 | | **可用性** | 99.9%+ | 连不上=用不了,用户立刻焦虑。 | | **在线状态实时性** | 最终一致即可 | 状态变化极频繁,允许短暂不准——这是宝贵的「松弛空间」。 | **关键约束(不可逾越的边界):** - 🔴 **长连接是「有状态」的**。一条连接物理上挂在某一台网关机器上,系统必须知道「某用户此刻连在哪台机器」才能把消息推给他。这与无状态 Web 服务的扩展方式完全不同,是头号约束。 - 🔴 **网络不可靠**。连接随时会断、消息可能丢、可能重发,可靠与有序必须靠应用层机制(序号、确认、重试、去重)自己保证。 - 🔴 **大群扩散**:一条消息发进万人大群,要触达每个成员——这又是一个「扇出」问题,和社交信息流的大 V 问题同源。 - 消息写入量大且持续:每条消息都要持久化以支持离线/历史/多端。 ## 4. 架构全景图 ``` 发送方(手机/电脑,挂着长连接) 接收方(在线 / 离线) │ ① 发消息 ▲ ▼ │ ⑤ 推送(在线时) ┌──────────────────────────────────────────────────────────────────────┐ │ 长连接网关层(有状态:每台机器挂着一批持久连接) │ │ 网关A 网关B 网关C ... (水平扩展,可挂百万级连接) │ └────┬───────────────────────────────────────────────┬──────────────────┘ │ ② 上行消息 ▲ ④ "推给用户U" → 查路由 ▼ │ → 找到U连在网关C → 投递 ┌────────────────────┐ 查询/更新 │ │ 消息服务(核心) │ ◀──── 用户↔网关 路由表 ────────┘ │ • 分配会话内序号 │ (用户U 此刻连在 网关C) │ • 持久化消息 │ ▲ │ • 判断收方在/离线 │ │ 连接建立/断开时更新 │ • 在线→投递 / 离线→存│ └───┬──────────┬───────┘ │ │ ③ 写入 离线分支 │ ▼ ┌────────────────────┐ │ ┌──────────────┐ │ 离线消息存储 │ │ │ 消息存储 │ │ + 离线推送服务 │ │ │ (持久/有序) │ │ (唤起厂商推送通道) │ │ └──────────────┘ └────────────────────┘ │ ▲ 收方上线后来拉取 │ 群消息扩散 ▼ ┌──────────────┐ ┌────────────────┐ ┌──────────────────────────────┐ │ 群组/成员服务 │ │ 在线状态服务 │ │ 媒体存储 │ │ (扇出到成员) │ │ (presence, │ │ (图/语音/文件,只传引用) │ │ │ │ 最终一致) │ │ │ └──────────────┘ └────────────────┘ └──────────────────────────────┘ ``` > 灵魂部件是两块:**「长连接网关层」**(有状态地挂着海量持久连接)和它依赖的**「用户↔网关路由表」**(要把消息推给某人时,先查到他此刻连在哪台网关)。整套系统的核心机制都围绕这两点,以及消息服务里的**「序号(保序)+ 持久化(不丢)+ 在线/离线分流」**展开。 ## 5. 组件职责 - **长连接网关层**:维持海量客户端的**持久连接**,做心跳保活,上行收消息、下行推消息。它是**有状态**的——每条连接物理绑定在某台网关上。*为什么需要*:实时推送要求服务端能主动找到客户端并推过去,这只能靠一直挂着的连接;轮询既慢又费。它是这类系统区别于普通 Web 的根本所在。 - **用户↔网关路由表**:记录「某用户此刻连在哪台网关机器上」。连接建立/断开时更新。*为什么需要*:连接有状态,要把消息投给用户 U,必须先知道 U 连在哪,才能把投递指令发到那台网关。**这是有状态连接得以水平扩展的关键拼图。** - **消息服务(核心)**:为每条消息**在会话内分配单调递增序号**(保序),**持久化**消息(不丢),判断收方在线与否并**分流**(在线直接投递、离线转存 + 触发推送)。*为什么需要*:可靠与有序不能靠网络自带,必须由这一层用序号、落库、确认来保证;它是消息流转的中枢。 - **消息存储**:持久保存消息,支持离线补齐、历史回溯、多端同步。写多、按会话有序读。*为什么需要*:消息不能只活在内存/连接里,否则一断线就丢;持久化是「不丢」和「多端一致」的基础。 - **离线消息存储 + 离线推送服务**:收方不在线时,消息存进他的「离线信箱」,并通过系统级推送通道**唤醒**对方;上线后他来拉取补齐。*为什么需要*:用户大量时间不在线,必须保证「人不在也不丢、回来能补上」。 - **群组 / 成员服务**:维护群成员关系,群消息要**扇出**到每个成员。*为什么需要*:群消息本质是「一条消息要投递给 N 个收方」,这是个扇出问题(和社交信息流的大 V 同源),大群尤其要小心。 - **在线状态服务(Presence)**:维护在线/离线/正在输入等状态。*为什么需要*:社交临场感的关键。但状态变化极频繁,**最终一致即可**——晚一两秒不致命。 - **媒体存储**:图片/语音/文件存对象存储,消息里只带**引用(地址)**,不把字节塞进消息流。*为什么需要*:大文件不能挤占轻量、要求低时延的消息通道,分开传输。 ## 6. 关键数据流 **场景一:一对一发消息,收方在线(最核心的实时路径)** ``` 1. 发送方(连在 网关A) ── ① 发消息 ──▶ 网关A ── ② 上行 ──▶ 消息服务 2. 消息服务: a. 给这条消息分配【会话内序号 seq=N】(保证顺序) b. 持久化这条消息(写入消息存储) ← 先落库,保证不丢 c. 查【路由表】:收方 U 此刻连在 网关C,且在线 3. 消息服务 ── ④ "投递给U" ──▶ 网关C ── ⑤ 推送 ──▶ 收方屏幕 4. 收方收到后回一个【ack(收到了 seq=N)】 5. 没收到 ack → 消息服务按策略重试投递;收方靠 seq 去重(重复的丢弃) ``` > 注意:**先落库、再投递**(不丢);**会话内单调序号**(有序);**ack + 重试 + 按序号去重**(可靠且不重)。可靠投递的三件套全在这条路径里。 **场景二:收方离线(存 + 推 + 上线补齐)** ``` 1~2. 同上,消息服务落库、查路由表 ── 发现收方 U 不在线(没有活动连接) 3. 消息服务:把消息写入 U 的【离线信箱】 4. ──▶ 离线推送服务:通过系统级推送通道,给 U 的设备发一条唤醒推送 5. (一段时间后)U 打开 App、建立长连接 ──▶ 网关更新路由表(U 现在连在 网关B) 6. U 上线后主动【拉取离线信箱】里 seq 大于"我已收到的最大序号"的所有消息 7. 按 seq 排好序呈现 → 一条不少、顺序正确 ``` > 注意第 6 步:**靠「我已收到的最大序号」做断点续传式拉取**,既不漏也不重——序号在这里同时解决了「补齐」和「去重」。 **场景三:群消息扩散(扇出问题)** ``` 某人在群里发一条消息 ──▶ 消息服务(分配该群会话的序号、落库一次) ──▶ 群组服务:取出群成员列表 ──▶ 对每个成员:在线的 → 查路由表投递;离线的 → 进各自离线信箱 + 推送 小群:直接逐个扇出,没问题。 万人大群:一条消息瞬间要触达上万人 → "扇出爆炸"(同社交流的大V问题) ✅ 大群优化:消息只存"一份共享的群消息流",成员各自维护"读到哪了"的位点, 上线/在群内时按位点来拉,而非给每人都推一份/存一份。 ``` > 大群扩散和社交信息流的「大 V 写扩散爆炸」是**同一个问题**:都是「一次写要触达海量接收方」。破解思路也同源——**别给每人都拷一份(写扩散),改成共享一份、各自按位点来拉(读扩散)**。 ## 7. 数据模型与存储选择 核心实体:`用户`;`会话(一对一 / 群)`;`消息(message,带会话内序号 seq)`;`用户↔网关 路由(谁连在哪)`;`会话成员关系`;`在线状态`;`已读位点`。 | 数据 | 存储类型 | 为什么 | |---|---|---| | 消息(持久) | 有序追加存储(按会话分片) | 写多、按会话有序读、要支持离线补齐与历史;序号天然适合追加 | | 用户↔网关 路由表 | 内存级 KV | 极高频读写(每条消息投递都要查)、要极低延迟、连接变动频繁 | | 在线状态(presence) | 内存级 KV(带过期) | 变化极频繁、读频繁、可最终一致,适合带 TTL 的高速缓存 | | 离线信箱 | KV / 有序队列(按用户) | 按用户存待收消息,上线后按序号拉取 | | 会话 / 群成员关系 | 关系型 / 宽列 | 成员管理要一致;大群成员列表可能很长 | | 媒体(图/语音/文件) | 对象存储 | 文件大、不可变;消息里只存引用 | | 用户账户 | 关系型 | 要事务、强一致(账号安全) | > 教学点:**消息流和「连接路由表」是两类访问形态完全不同的数据。** 消息要持久、有序、可回溯,适合有序追加存储;路由表要的是「每次投递都极速查一下 U 连在哪」,是典型的内存级 KV。**用「内存级 KV」描述路由表与 presence,是因为它们的访问形态是「超高频、低延迟、可接受最终一致」,与具体产品无关。** 把路由表放进慢速持久库,会让每条消息的投递都被拖慢。 ## 8. 关键架构决策与权衡 ⭐ **决策 1:长连接怎么维持与水平扩展?(本架构的命门)** - 问题根源:长连接是**有状态**的——一条连接物理挂在某台网关机器上。无状态 Web 服务「随便哪台机器都能处理任意请求」的扩展方式在这里**失效**了,因为「要把消息推给 U」必须送到「U 实际连着的那台机器」。 - 选项 A(单机/纵向):一台超强机器挂所有连接。简单,但连接数有物理天花板,且单点故障=全员掉线。 - 选项 B(水平扩展 + 路由层):**多台网关分担连接**,外加一张**用户↔网关路由表**记录「谁连在哪」;投递时先查路由、再把消息送到对应网关。 - **取向**:**必然走 B**。代价是:① 多了一层路由表,且它要随连接的建立/断开**实时更新**(高频写);② 投递路径多一跳查询;③ 网关机器故障时,其上所有连接要能**重连并刷新路由**。**「把有状态的连接 + 一张‘状态在哪’的路由表」组合起来,是让有状态服务水平扩展的通用套路**——它把「状态绑定」与「按状态路由」显式分离了出来。 **决策 2:消息时序怎么保证?** - 靠到达时间排序:网络抖动、重发会让到达顺序≠发送顺序,**乱序**。 - **靠会话内单调递增序号**:每条消息在其所属会话里拿一个递增的 seq,接收端**按 seq 排序呈现**,而非按到达顺序。 - **取向**:**用单调序号保序**,且**保序的范围限定在「单个会话内」**(不需要全局有序——全局有序代价极高且没必要,你只关心「这个对话里的顺序」)。代价是要有一个「为某会话发号」的机制(且要避免它成为瓶颈,通常按会话分片发号)。**「在真正需要顺序的最小范围内保序」**——这是个关键的范围收敛智慧。 **决策 3:可靠投递(不丢、不重)怎么做?** - 发出去就不管:最简单,但网络一抖就丢消息,违背底线。 - **确认 + 重试 + 幂等去重**:① 收方收到后回 **ack**;② 发送侧没收到 ack 就**重试**;③ 重试会导致重复,所以接收侧靠 **seq / 消息 ID 去重**(同一条只呈现一次)。再叠加「**先持久化、再投递**」保证即使投递失败,消息也没丢、可重投。 - **取向**:**三件套(ack + 重试 + 去重)缺一不可**,因为重试和去重是一对——你为了「不丢」而重试,就必然引入「重复」,于是必须去重。代价是协议变复杂、每条消息要带 ID/seq、要维护 ack 状态。**「至少一次投递 + 幂等去重 = 恰好一次的效果」**,这是分布式投递的经典组合。 **决策 4:群消息扩散——写扩散还是共享读取?** - **写扩散(给每个成员各投/各存一份)**:小群很自然、读时简单。但**大群爆炸**——万人群一条消息瞬间产生上万次投递/存储。 - **共享读取(只存一份群消息流,成员各记「读到哪」)**:写极轻(存一份),成员按**已读位点**来拉。大群友好,但读时要按位点聚合。 - **取向**:**小群写扩散、大群共享读取**(和社交流「普通人推、大 V 拉」同构)。代价是系统里同时存在两套群消息处理路径。**这再次印证:任何「一次写触达海量接收方」的扇出问题,解法都在‘拷贝一份 vs 共享一份按需拉’之间权衡。** **决策 5:在线状态(presence)要多准?** - 强一致、实时精确:状态变化极频繁(每次切前后台、断网、心跳都变),强一致代价极高,且没必要。 - **最终一致 + 高速缓存 + 过期**:状态写进带 TTL 的内存级 KV,允许短暂不准(对方刚下线你可能还显示在线一两秒)。 - **取向**:**最终一致足矣**。**presence 是典型的「可以不那么准」的数据**,把它当强一致来做是巨大的浪费。识别出「哪些数据允许短暂不准」,能省下大量成本——这是宝贵的设计自由度。 ## 9. 规模化与瓶颈 和普通系统不同:**这里的头号瓶颈是「长连接数」和「连接路由」,其次才是消息写入与大群扩散。** - **第一个瓶颈:长连接数。** 单机能挂的持久连接有上限,用户一多就触顶。 破解:① **网关水平扩展**(多台分担连接);② **连接路由层**(用户↔网关路由表)让消息能找到对应网关;③ 就近接入、连接负载均衡。 - **第二个瓶颈:大群扩散。** 一条消息要触达海量成员。 破解:**大群共享读取(读扩散)+ 已读位点拉取**,而非给每人推/存一份(决策 4)。 - **第三个瓶颈:消息存储写入量。** 每条消息都要落库,量极大且持续。 破解:① 按**会话分片**(把写入压力打散);② 冷消息归档到更廉价的存储层;③ 写入路径异步化、批量化。 - **第四个瓶颈:路由表 / presence 的高频读写。** 每次投递查路由、状态频繁变更。 破解:放**内存级 KV**、按用户分片、presence 用最终一致 + TTL 降低写压。 - **媒体流量**:图/语音/文件走对象存储 + (必要时)CDN,不挤占消息通道。 ## 10. 安全与合规要点 - **端到端加密(取决于产品定位)**:像 WhatsApp 这类强隐私定位的产品,消息**在两端加解密,服务器只转发密文、看不到内容**。这是最强的隐私承诺,但代价是:服务端无法做内容审核、无法在服务端做全文搜索、多端同步密钥管理复杂。是否上 E2EE 是一个产品级取舍。 - **消息隐私与最小留存**:消息是高度敏感数据,要做传输与存储加密、访问控制、明确的留存与删除策略。 - **传输加密**:即便不做端到端,客户端到服务器的链路也必须加密。 - **滥用与垃圾消息**:防骚扰、防垃圾、防批量拉群,需要风控与限流。 - **元数据隐私**:即便消息内容加密,「谁和谁在何时聊」这类元数据本身也敏感,要尽量少留、严控。 - **群与权限**:谁能进群、谁能看历史消息,权限要在服务端强制,不能只靠客户端隐藏。 ## 11. 常见误区 / 反模式 - ❌ **用轮询代替长连接** → ✅ 轮询要么慢(间隔长)、要么费(间隔短狂打服务器),都做不到真正实时。**实时推送要靠长连接。** - ❌ **不分配序号,按到达顺序展示** → ✅ 网络抖动/重发必然乱序。**用会话内单调序号保序。** - ❌ **不做去重,重试导致消息重复** → ✅ 为「不丢」而重试必然带来重复,**必须靠 seq/消息 ID 幂等去重**。 - ❌ **发出去就不管,不做 ack/重试** → ✅ 网络一抖就丢。**ack + 重试 + 先落库再投递**才能不丢。 - ❌ **大群同步写扩散** → ✅ 万人群一条消息瞬间上万次投递,卡死。**大群改共享读取 + 位点拉取。** - ❌ **把连接当无状态、随意路由** → ✅ 连接是有状态的,**必须有路由表知道用户连在哪**,否则消息推不到人。 - ❌ **presence 当强一致来做** → ✅ 状态变化极频繁,**最终一致足够**,强一致是巨大浪费。 - ❌ **把媒体字节塞进消息流** → ✅ 大文件挤占低时延消息通道,**应存对象存储、消息里只带引用**。 ## 12. 演进路线:MVP → 成长期 → 成熟期 | 阶段 | 用户/规模量级 | 架构长什么样 | 此时该操心什么 | |---|---|---|---| | **MVP** | 验证想法 / 几万用户 | **单机长连接**(或简单轮询)+ 单一消息存储;序号、ack、离线信箱的基本机制先有;无大群优化 | **先把「实时、不丢、有序」的基本盘做对**,连接都在一台机器上还撑得住 | | **成长期** | 百万级连接 | **分布式长连接网关 + 路由表**;离线消息存储 + 离线推送;消息存储按会话分片;presence 用高速缓存 | 长连接水平扩展、连接路由、可靠投递在分布式下依然成立;盯住第一批大群 | | **成熟期** | 千万级连接 / 海量消息 | **大群优化(共享读取+位点)** + **多活/多区域** + 就近接入 + 完善的多端同步 + (按定位)**端到端加密** + 风控合规 | 大群扇出、连接规模、容灾多活、消息存储成本、隐私与合规 | ## 13. 可复用要点 - 💡 **有状态服务靠「状态绑定 + 路由表」来水平扩展。** 长连接挂在某台机器上,就用一张「状态在哪」的路由表来定位它。任何有状态资源(会话、连接、分片主副本)的扩展,都可借鉴这种「把状态绑定与按状态路由显式分离」的套路。 - 💡 **在「真正需要顺序的最小范围」内保序,别追求全局有序。** 单会话内用序号保序就够,全局有序代价高且无意义。范围收敛是性能与复杂度的关键。 - 💡 **至少一次投递 + 幂等去重 = 恰好一次的效果。** 重试(防丢)与去重(防重)是一对孪生机制——任何「不可靠信道上要可靠送达」的场景都适用,从消息到支付回调到事件投递。 - 💡 **「一次写触达海量接收方」永远是扇出问题,解法在「各拷一份 vs 共享一份按需拉」之间。** 大群扩散、社交大 V、广播通知,本质同源,可套同一套权衡。 - 💡 **识别「允许不那么准/不那么实时」的数据,把它降级处理。** presence 用最终一致省下大量成本。哪里能松弛,哪里就能换来弹性与省钱。 - 💡 **先落库、再投递。** 把「持久化」放在「送出」之前,是「不丢」的根基——这条次序原则在任何要求可靠的写-投递链路里都成立。 ## 🎯 随堂检验 --- ## 参考原型与延伸阅读 > 本模板基于以下**官方工程博客**整理。 **📖 工程博客:** - [How Discord Stores Trillions of Messages](https://discord.com/blog/how-discord-stores-trillions-of-messages) — 海量消息存储从 Cassandra 迁到 ScyllaDB、请求合并降延迟。 - [Slack: Flannel, an edge cache to make Slack scale](https://slack.engineering/flannel-an-application-level-edge-cache-to-make-slack-scale/) — 承载数百万 WebSocket 长连接的边缘缓存。 - [WhatsApp: Scaling to Millions of Simultaneous Connections (Rick Reed)](http://www.erlang-factory.com/conference/SFBay2012/speakers/RickReed) — 单机支撑百万级并发长连接(每连接一进程)。 --- > 📌 一句话记住实时通讯:**它不是「能发消息的网站」,而是「一张永远在线的消息投递网」——核心难点是『维持海量有状态长连接 + 在不可靠网络上做到不丢、不重、不乱序』,所有架构机制(网关路由、序号、ack/重试/去重、离线存推)都在为这两件事服务。**