题干与适用场景
设计一个文本聊天服务:日活用户 5000 万、峰值并发连接 500 万、每天消息 20 亿条。它支持私聊、最多 200 人的群聊、每位用户多台设备、离线补齐、送达与已读回执、在线状态和正在输入提示。附件、公开频道、跨地域双活写入、全文搜索和端到端加密留作追问范围。
服务端只在消息持久化以后确认发送成功。任何已确认消息都不得静默丢失。对于正常负载下已经在线的接收设备,从持久化接收到到达设备的 p99 应低于 1 秒。传输层可以重复投递,但客户端去重后只能看到一条逻辑消息。只要求同一会话内有序,不要求互不相关的会话共享全局顺序。
三篇在 2026 年发布或更新的公开系统设计指南都直接以聊天系统为题,并反复考察长连接、连接路由、会话内顺序、离线同步、回执和在线状态。WebSocket 标准定义双向传输和控制帧,Matrix 客户端—服务端规范则给出客户端事务 ID、增量同步令牌、已读回执和短暂输入事件的生产级实例。这些资料证明题型与故障边界具有当前代表性和技术依据;本题的规模与 SLO 是虚构的面试输入。
面试官考察点
第一,看候选人能否精确定义契约。“已发送”“已送达”和“已读”是三件事。服务端确认表示已经持久化接收;送达回执表示指定接收设备拿到了消息;已读回执还需要产品规则确认用户实际展示了会话。把三者合成一个状态,会产生虚假的可靠性结论和错误的未读数。
第二,看候选人是否会回避端到端“恰好一次”承诺。服务端提交后、确认到达前,客户端可能超时;网关也可能在重连后重复投递。可实现的契约是至少一次传输,加上稳定的客户端消息 ID、服务端幂等记录,以及客户端按服务端消息 ID 去重。
第三,看排序边界。全局顺序会让无关会话共用串行瓶颈。更有用的产品契约是:每条已接收消息在所属会话内取得单调递增序号。因此,同一会话的写入必须到达当前唯一所有者或定序器;不同会话仍可独立扩展,同时也清楚暴露单个热点会话的上限。
第四,看是否拆分持久状态和短暂状态。消息、成员资格区间和已读游标需要经受故障;在线状态与正在输入可以过期和丢弃。如果输入提示也进入持久消息日志,不但浪费存储,装饰性流量还可能拖慢真实消息。
最后,看恢复思维。答案应覆盖发送确认丢失、网关故障、重连风暴、陈旧会话所有者、慢设备、成员变更、重复扇出和热点群聊。容量数字要从题干推导,再通过负载测试校准,不能假装是所有服务器都适用的极限。
回答前需要澄清的问题
- 哪些内容和会话类型在范围内? 本答案处理文本、私聊和最多 200 人的群聊。附件使用对象存储加元数据消息;公开频道需要不同的扇出与历史读取策略。
- 每类回执代表什么? 服务端接收、设备收到、用户已读是独立的单调事实。“已读”只在客户端展示相关会话时发生,收到推送不算已读。
- 需要什么顺序? 本题要求会话内稳定顺序,不承诺按用户设备时钟排序,也不让两个不同会话共用序号。
- 用户能否从多台设备发送? 可以。每台设备有自己的认证连接和同步游标;发送方重试时复用同一个客户端消息 ID。
- 成员变化时如何处理? 接收消息时检查权限。产品还要定义新成员能否读取旧历史,以及被移除成员可在本地保留什么。本设计按会话序号存储成员资格区间。
- 消息与幂等记录保留多久? 消息保留期由产品与合规决定。幂等映射必须覆盖最长客户端重试窗口;更早删除会重新制造重复消息。
- 使用哪些地域? 基础答案为每个会话分配主地域,并执行受控故障转移。多地域同时可写需要更复杂的冲突与排序模型。
- 哪些时间不计入 1 秒目标? 离线推送展示、重连补齐、用户阅读时间和跨地域灾难恢复,要与已在线设备的实时送达分别计量。
30 秒回答框架
“我会把连接管理与持久消息链路拆开。客户端通过认证后的 WebSocket 发送消息,并携带稳定的客户端消息 ID。按会话路由的服务重新检查成员资格,分配下一个会话内序号,在同一事务里保存消息和幂等结果,然后才返回确认。异步扇出任务查找接收方的在线设备,并把消息送到对应网关;离线或失败设备通过基于游标的同步接口恢复。送达和已读游标只能单调前进,在线状态与正在输入走独立的过期链路。传输层是至少一次,因此服务端和客户端都要去重。按题目规模,平均约每秒 23000 条、峰值按十倍约每秒 230000 条;若每条存储 1 KB,每天逻辑数据约 2 TB。我会针对长连接、热点群聊、重连风暴、重试、成员变化和所有者故障转移做负载与故障测试。”
分步骤深入解答
步骤一:冻结产品契约,计算第一版容量包络。
每天 20 亿条消息,平均约为每秒 23148 条。若峰值是平均值的十倍,入口应先按每秒 230000 条规划。假设每条持久化消息连同普通元数据和索引平均占 1 KB,每天逻辑写入约 2 TB;三副本约 6 TB,还没有包含压缩、文件系统余量、备份和回执写入。这些规划假设仍需通过硬件实测校准。
500 万条并发连接决定网关规模。假设在选定实例、TLS 配置、心跳周期和目标 p99 下,负载测试证明每台网关可健康维持 50000 条连接,裸下限是 100 台。若只运行在实测上限的 70%,需要约 143 台,可取整为 150 台,再增加可用区故障余量。测试必须包含消息、重连和慢客户端;只数空闲套接字会严重失真。
步骤二:定义客户端协议与各类身份。
每台设备与地域网关建立认证后的 WebSocket。网关在适用时验证来源、限制帧大小和发送频率、刷新授权,并通过 ping/pong 与租约识别死连接。RFC 6455 只提供连接机制;认证、确认、序号、重试和背压仍由应用协议负责。
核心帧可以写成:
SEND {
conversation_id, client_message_id, body, client_sent_at
}
ACK {
client_message_id, message_id, conversation_seq, accepted_at
}
MESSAGE {
conversation_id, message_id, conversation_seq, sender_id, body, accepted_at
}
SYNC {
device_sync_cursor, limit
}clientmessageid 只生成一次,同一发送设备的每次重试都复用它。messageid 是服务端身份,供接收方去重。conversationseq 决定单个会话内的展示和补齐顺序。accepted_at 是诊断所用的服务端时间,不能充当排序权威。
步骤三:每条逻辑发送只提交一次,提交后才确认。
网关把 SEND 转发给聊天服务。服务定位当前会话分片,认证发送者,按当前成员版本检查资格,验证大小,并执行用户级与会话级限流。分片所有者串行处理该会话内已接收的写入。
在同一个持久事务里,它分配下一个序号、写消息,并写入类似 (senderdeviceid, clientmessageid) → message_id 的唯一映射。重复请求返回既有结果,不再追加消息。只有完成法定副本提交的记录才收到 ACK。确认丢失时可以安全重试;存储在提交前失败时不发送确认。
消息按 conversationid 分区,并按 conversationseq 排序。成员变更也要进入顺序,或引用生效序号边界,防止授权只依赖最终一致的当前成员缓存。带任期的所有者栅栏会阻止故障转移后的旧所有者继续写入。
步骤四:持久提交后再扇出。
已提交日志产生送达任务。连接目录把 (userid, deviceid) 映射到网关与连接租约。对于私聊或最多 200 人的群聊,扇出工作器读取在该消息序号生效的成员快照,按网关归并在线设备,再批量下发网关命令。消息正文只持久化一次;扇出携带引用或紧凑事件,不为每个接收方复制一份持久正文。
网关把事件放入每条连接的有界出站队列。设备在本地接收事件后返回单调送达游标。如果连接过慢,网关不能无限缓存;它会标记该设备需要重新同步,并带应用原因关闭连接。持久日志仍是真相,所以丢弃一次内存实时投递不会丢失消息。
对于离线用户,系统可以发送不暴露敏感内容的移动推送提示。推送只负责唤醒;持久服务保存消息,设备回执证明送达。用户打开应用后,由客户端认证并从持久服务同步。
步骤五:明确设计重连与多设备同步。
每台设备持久化一个不透明的 devicesynccursor。GET /sync?after=cursor 或等价帧返回有序的会话增量、成员变更、回执增量及新游标。服务端不能把游标推进到本次响应遗漏的事件之后。游标过期或缺口过大时,服务端返回有界快照和续页令牌,不在一个连接里尝试无限回放。
实时送达与同步可能重叠,因此客户端按 messageid 合并,再按 (conversationid, conversation_seq) 排序。新设备只获得策略允许的历史,并建立自己的游标。已读状态按用户和会话单调前进;送达状态可以保留到设备级。聚合规则必须说明“已送达”是任意设备已收到,还是所有活跃设备都收到。
步骤六:如实处理回执、在线状态与正在输入。
已读更新携带客户端已经展示的最大 conversation_seq;服务端使用条件最大值,防止迟到请求让游标倒退。Matrix 规范中的回执模型说明了为什么已读是替换旧位置的增量,也说明仅仅收到事件不足以证明用户看过内容。
在线状态与正在输入走另一条链路。网关刷新短期在线租约。输入事件要通过授权、限流,只作用于一个会话,进行合并,并在数秒后过期。过载时可以丢弃,永远不写入持久消息日志。“最后在线”需要明确隐私策略,并使用粗粒度更新,防止心跳流量演变成写入风暴。
步骤七:先设计故障路径,再声称可靠。
ACK丢失:客户端用同一个clientmessageid重试,服务端返回已存结果。- 网关崩溃:设备带抖动重连并从游标恢复;原连接租约到期。
- 扇出工作器崩溃:持久送达任务重试;网关与客户端去重。
- 会话所有者崩溃:通过新任期与法定副本状态选出接替者;旧所有者被栅栏阻止。
- 慢设备:有界队列触发重新同步,不让它耗尽内存。
- 热点会话:单个定序器限制写入速率。批量提交、隔离热点分片并执行产品级限流;增加普通哈希分区无法并行化同一条严格顺序。
- 成员竞态:把成员变化与消息一起排序,并按生效区间授权。
- 地域故障:只有在栅栏隔离旧主地域后才提升副本;恢复时间和未确认消息的可能丢失,与“已确认消息不丢”分开描述。
重连采用带抖动的指数退避和准入控制。否则一次地域网关重启,可能把 500 万个健康客户端变成握手风暴,反过来阻断恢复。
步骤八:用分层测试与可观测性证明不变量。
属性测试生成重试、乱序送达、成员变化和所有者任期,再断言每个客户端 ID 只有一条逻辑消息、会话序号唯一、游标单调,以及移除边界之后没有越权消息。集成测试分别在提交前、提交后确认前、扇出期间终止进程。混沌测试移除网关、工作器、分片所有者、可用区和一部分连接目录。
负载测试模拟 500 万长连接、每秒 230000 条峰值发送、群聊扇出、慢接收方和大规模重连。观测接收延迟、实时送达延迟、同步滞后、客户端去重前后的重复率、序号缺口、热点分片饱和度、出站队列字节数、重连准入和越权读取拒绝。灰度发布只在持久性与授权不变量保持干净时扩大。
高质量示范回答
“我会先定义三个不同结果:服务端持久化接收、设备送达和用户已读。只有法定副本保存消息后,服务才返回确认。网络投递仍是至少一次,因此发送方生成的客户端消息 ID 让重试幂等,接收方按服务端消息 ID 去重。
客户端连接到地域 WebSocket 网关。连接目录告诉扇出工作器每台用户设备在哪个网关,但网关不持有历史。聊天服务按会话 ID 把每次写入路由到当前带栅栏的所有者。所有者重新检查成员资格,分配会话内序号,并原子写入消息与幂等结果。无关会话可以并行;单个超热会话则保留为明确的串行瓶颈。
提交后,工作器向在线设备扇出。失败或离线投递通过基于游标的同步接口修复。实时与回放可能重叠,因此消息 ID 吸收重复,会话序号恢复局部顺序。已读游标按最大值前进。在线状态与正在输入使用会过期的尽力而为状态,不能阻塞持久消息。
每天 20 亿条消息,平均入口约每秒 23000 条;我会按十倍峰值规划每秒 230000 条。按题目假设的每条 1 KB,逻辑消息存储约每天 2 TB,三副本约 6 TB。对于 500 万连接,实测每台 50000 条的网关上限意味着裸下限 100 台;按 70% 利用率约需 150 台,之后再加可用区余量。
我会验证确认丢失、网关与所有者故障、陈旧任期、重复扇出、慢设备、成员竞态、热点群聊和重连风暴。发布门槛是:已确认消息零丢失、去重后无重复逻辑消息、序号不倒退、成员区间外没有越权访问,以及代表性负载下实时送达 p99 达标。”
常见错误
- 错误:把服务端接收、送达和已读称作同一状态 → 后果:可靠性与未读指标无法验证 → 修正:分别定义单调状态及其责任方。
- 错误:承诺网络端到端恰好一次 → 后果:确认丢失或重连会产生无法解释的重复 → 修正:使用至少一次传输、稳定发送 ID 和去重。
- 错误:全局排序所有消息 → 后果:无关会话共用一个瓶颈 → 修正:只在会话内分配序号。
- 错误:持久提交前就确认 → 后果:进程或可用区故障会静默抹掉已接收消息 → 修正:只确认完成法定副本提交的记录。
- 错误:把历史存在 WebSocket 网关 → 后果:连接迁移等于数据迁移,网关丢失还会威胁历史 → 修正:网关只保留有界连接状态。
- 错误:为慢设备无限缓存 → 后果:单个客户端就能耗尽网关内存 → 修正:限制队列、断开连接并从持久存储重同步。
- 错误:把在线状态和输入提示写入持久日志 → 后果:短暂装饰流量增加成本并拖慢消息 → 修正:使用经过授权、限流且带 TTL 的尽力而为链路。
- 错误:只按陈旧的当前成员缓存授权 → 后果:被移除用户可能跨越竞态发送或接收 → 修正:排序成员边界,并按其生效区间栅栏接收。
- 错误:只按空闲套接字数量给网关定容 → 后果:TLS、心跳、扇出和重连会击穿方案 → 修正:以目标 p99 压测完整负载并保留故障余量。
追问及应对
追问一:如何增加端到端加密?
在发送设备上加密消息正文,服务端只存密文和路由所需元数据。每台设备需要身份密钥、签名设备列表及会话密钥分发;新增或移除设备、成员时,要按策略轮换或重新分发密钥。服务端仍能给密文排序和路由,但服务端搜索、内容审核、恢复、预览及滥用处理都会受限。送达元数据、参与者、时间和大小可能仍然可见,所以加密不会消除元数据隐私工作。
追问二:能否让多个定序器拆分一个热点群聊?
如果仍要求一条严格连续的顺序,就不能在没有另一个排序权威的情况下直接拆分。给会话增加普通哈希分片,最终仍需要合并或共识点。先批量提交和隔离分片,再执行会话级限流。若产品接受局部顺序,可以按话题或发送方分区并暴露因果关系,但这会改变契约和客户端复杂度。
追问三:发送方已看到 ACK,但接收方离线一周,这条消息算送达吗?
它已被接收,但尚未送达该设备。持久消息在保留策略内继续存在,推送提示可以尝试唤醒设备。设备重连后从游标同步,再报告送达。产品界面和指标应分别保留服务端已接收、任一设备已送达、所有活跃设备已送达和已读。
追问四:编辑和删除如何与排序结合?
把它们表示为引用原消息的新有序事件,保留可追踪的历史。客户端折叠事件流得到当前视图。接收编辑或删除时重新检查权限;产品策略定义时间限制,以及删除是移除内容、留下墓碑,还是异步清除二级存储。
追问五:被移除成员带着旧游标重连怎么办?
同步服务检查该用户的成员资格区间,不能只看其是否持有游标。它只返回该用户有权读取的事件,并包含成员移除边界。游标仅表示不透明位置;访问权仍取决于身份与成员区间。已缓存附件与本地副本还需要单独的撤销和保留预期,因为服务端无法擦除已经保存到设备的数据。
追问六:如何支持超大型公开频道?
面向 200 人群聊的写时扇出不再适用。频道日志只存一份,关注者按游标拉取;只扇出紧凑的未读或推送提示,缓存热门片段,并放宽个性化送达回执。分区、审核、发现和明星频道热点都成为一等问题,需要独立的工作负载模型;沿用同一方案并扩大数字会失真。