00 系统总览¶
状态:可实施 更新日期:2026-09-01 上游契约:
docs/PLAN.md§3、§4、§5.1、§5.4、§17、§23 实施决策:ADR-0006(Rust + Redpanda)、ADR-0007(一期简化形态)、ADR-0012(Fanout 无 Kafka 事务)
0. 文档边界¶
本文定义:系统总览、完整架构图、服务清单、请求全链路时序。
本文不得重新定义:§5.1 的实体命名——只能使用该表写法, 禁止使用"禁止别名"列中的任何写法。
1. 一句话架构¶
消息提交、收件人索引、Socket 投递是三个独立阶段,
中间用可重放日志隔开,每一段都能独立重试与恢复。
三条派生原则:
正文只存一份,用户邮箱只放轻量引用
用户固定到逻辑分片(稳定虚拟桶),路由是纯计算不是查询
所有可能重试的步骤都有确定性幂等键
2. 架构图¶
客户端(iOS / Android / 桌面 / 浏览器)
│
TLS 1.3 / 443 / ALPN qim/1
浏览器降级 WSS(同一套帧,docs/01)
▼
┌───────────────────────────────────────────────────────┐
│ ConnectionNode 持有 ConnectionShard 租约 │
│ 帧编解码 · 认证 · 心跳 · 流控 · 多路复用写出 │
│ PresenceEntry 的唯一写入方 │
└───────┬───────────────────────────────────┬───────────┘
│ SEND_MESSAGE ▲ PUSH_EVENTS
▼ │
┌───────────────────────┐ │
│ ConversationWriter │ 单会话单写 │
│ 分配 message_id / │ 租约 + fencing │
│ conversation_seq / │ │
│ last_activity_id │ │
└───────┬───────────────┘ │
│ 写正文(Redis 热层,7 天) │
├──────────────▶ MessageStore(分层,ADR-0018)
│ Redis 热窗口 · ScyllaDB 永久归档
▼
┌───────────────────────┐
│ CommitLog(Redpanda) │ 基础设施,非服务
└───────┬───────────────┘
▼
┌───────────────────────┐
│ FanoutCoordinator │ 独立 qfanout 服务、异步阶段
│ 固化 membership_ver │ 按目标分片合并为 S 条 GroupDispatch
└───────┬───────────────┘
▼
┌───────────────────────┐ 分发日志分区 1:1 逻辑 MailboxShard
│ 分发日志(Redpanda) │ offset 低 48 位 == mailbox_seq
└───────┬───────────────┘
▼
┌───────────────────────────────────────────────────────┐
│ MailboxNode 持有 MailboxShard 租约(单归属)│
│ 个人邮箱物化(MailboxStore:一期 Redis) │
│ materialized_watermark[64] · mailbox_trim_watermark │
│ 成员 Bitmap ∩ 在线 Bitmap → 按 ConnectionShard 合并 │
│ durable 后尽力发送 Projection 补充(不推进 qsession 位点)│
│ 产生 PushTask │
└───┬───────────────────────────┬───────────────┬────────┘
│ PushBatch │ 读时 join 正文 │ PushTask
▼ ▼ ▼
ConnectionNode MessageStore NotificationService
(APNs / FCM / VoIP)
旁路:HistoryArchiver(qim-archiver)独立消费 Outbox → 批量归档 ScyllaDB → 发布归档水位;
writer 只裁剪「超过 7 天 ∧ 已归档」的热层历史(ADR-0018)。
qsession 独立消费 dispatch → Redis 会话集合 + per-partition checkpoint;
未读在读路径由 MessageStore + read state 定义式计算。
`UserConversationState` 权威兜底当前尚未接入,是发布阻断。
PresenceDirectory(compacted topic,按 user_bucket 分区)
ConnectionNode 写入 → MailboxNode 订阅式本地缓存,推送路径 0 次远程点查
ShardRegistry(etcd):分片归属唯一权威,租约驱动 assign()
图中三处容易画错的地方(v1 曾画错):
1. MessageStore 的读箭头挂在 MailboxNode,不是 FanoutCoordinator
—— 中心层不读正文,只有读路径 join 才读(每 dispatch 一次,LRU 未命中时)
2. ConversationWriter → MessageStore 是单向写
3. CommitLog 是基础设施不是服务,不进 §17 服务表
3. 服务清单¶
| 服务 | 主要职责 | 不负责 | 状态 |
|---|---|---|---|
| ConnectionNode | 帧编解码、认证、心跳、流控、Socket 写入、PresenceEntry 唯一写入方 |
消息持久化、成员展开、未读计算 | 有状态不进检查点(连接可重建) |
| Auth/Session | 认证、session_epoch 分配(单调)、路由令牌 |
消息顺序 | 无状态 |
| ShardRegistry | 虚拟桶、逻辑分片、租约、shard_epoch、边界表 |
每条消息的路由查询 | 强一致小集群 |
| PresenceDirectory | per-device 在线目录的发布通道、租约过期回收、吊销事件传播 | 消息投递、未读聚合 | compacted topic + 节点缓存 |
| ConversationWriter | 准入、幂等、分配三种序号、写正文与提交日志、更新 ConversationHead |
成员级推送、邮箱物化 | 有状态(租约 + 序号窗口 + HLC) |
| MessageStore | 单份正文、历史分页、MessageIndex、ClientDedup |
用户离线游标 | 分层(ADR-0018):Redis 热层承载提交状态与 7 天近期历史,ScyllaDB 为永久权威 |
| HistoryArchiver | 独立消费 Outbox,批量普通 INSERT 归档 canonical MessageRecord 到 ScyllaDB,全批确认后提交位点并发布归档水位 |
提交路径、投递、裁剪决策 | 无状态(位点在日志,水位在 Redis) |
| GroupMembership | 成员关系、不可变成员版本、分片 Bitmap、MemberSlotMap |
Socket 管理 | ScyllaDB + S3 |
| FanoutCoordinator | 消费提交日志、固化成员版本、按分片合并 dispatch、租户配额 | 逐用户 RPC、逐 Socket 写入 | 无状态(位点在日志) |
| MailboxNode | 邮箱物化、64-lane 水位格式、DispatchProgress、Bitmap 求交、durable 后投递 Projection/PushTask;当前按整条 record 统一推进 64 lane,目标独立推进尚未实现 |
原始媒体、正文权威存储、会话投影 checkpoint | 一期无本地权威数据 |
| SessionProjection | 独立消费 dispatch 日志、持久投影 checkpoint、会话集合、读时定义式未读、内存快照与 keyset 分页 | 消息正文与邮箱游标 | 保留窗口内可由 dispatch + MessageStore 重放;UserConversationState 窗口外兜底尚未实现,发布阻断 |
| RoomWriter | room_seq、进程内环形缓冲、按 ConnectionShard 广播 |
全员持久邮箱 | 可丢失状态 |
| MediaService | 上传授权、缩略图、对象生命周期、MediaOwnerIndex |
IM 长连接 | 无状态 |
| NotificationService | APNs/FCM/VoIP 接入、device_token 生命周期、去重频控 |
在线判定、未读聚合 | 私有 token 表 |
| ModerationService | 审计、封禁、内容治理、租户策略 | 核心投递顺序 | 无状态 |
4. 全链路时序¶
4.1 发一条群消息¶
1 客户端 SEND_MESSAGE(stream 1)
2 Connection L1 准入 → 按 conversation_id 路由到 Home Region 的 Writer
3 Writer 成员校验 → L1 复核 + L2 会话准入 + L3 租户配额
4 Writer ClientDedup 占位(IF NOT EXISTS)
5 Writer 同一临界区分配 message_id / conversation_seq / last_activity_id
6 Writer 写 MessageRecord 到 Redis 热层(与第 4~5 步同一 Lua;ScyllaDB 由 HistoryArchiver 异步归档,USING TTL 按 retention_class 在归档层执行)
7 Writer 追加 Outbox(Redpanda 幂等 producer,acks=all)
8 Writer SEND_ACK ─────────────────────────▶ 客户端 ← 到此为 COMMITTED
9 Writer 异步更新 ConversationHead(失败重试,不阻断 fanout)
10 Fanout 消费提交日志 → 固化 membership_version
11 Fanout 按目标分片写 S 条 GroupDispatch(每分片一条,与成员数无关)
12 Mailbox dispatch_id 去重 → fencing_epoch 过滤 → 加载成员 Bitmap
13 Mailbox 成员边界过滤 → 按 lane 拆 ≤64 个 chunk 组 → 分块写入
14 Mailbox 当前等整条 record 全部 durable → 统一推进 W[0..63] ← 到此为 MAILBOXED
(目标 lane 独立推进尚未实现)
15 Mailbox 成员 Bitmap ∩ 在线 Bitmap → 按 ConnectionShard 合并 PushBatch
16 Connection 展开 PUSH_EVENTS,公共正文编码一次,逐 Socket 写轻量帧头 ← PUSHED
17 客户端 应用并推进游标(只由 MAILBOX_BATCH 推进,PUSH 不越位) ← APPLIED
18 Mailbox 对无在线设备者产生 PushTask → NotificationService ← NOTIFIED
成本形态:正文 O(1)、会话头 O(1)、中心任务 O(S)、
邮箱引用 O(N)、Socket 写 O(在线设备数)。
4.2 登录同步¶
1 AUTH(携带游标)
2 服务端按固定顺序判定:
签名无效/越界 → CURSOR_INVALID
游标 < effective_trim → CURSOR_EXPIRED(走 REBUILD)
shard_epoch 落后 → CURSOR_REBASED
分片不在本节点 → REDIRECT
3 AUTH_OK(sync_to_seq = 该 lane 水位快照、has_offline、角标快照…)
4 PULL_MAILBOX × N(流水线窗口 4)→ MAILBOX_BATCH × N
5 SYNC_COMPLETE → ONLINE_READY ← 解除屏障,开始推 PUSH_EVENTS
sync_to_seq 把事件一刀切成"客户端拉"与"服务端推"两半,两半都不需要判断对方进度。
4.3 离线消息与历史消息¶
离线消息 设备游标之后的 UserMailboxEntry —— 拉一条个人队列即可
历史消息 按 conversation_seq 查 MessageRecord —— 进入会话或翻页时才拉
邮箱裁剪后,原本的"离线消息"降级为"历史消息",
由 CURSOR_EXPIRED 显式暴露,不静默丢失。
5. 五种序列速查¶
| 序列 | 类型 | 作用域 | 允许空洞 | 可参与 UI 排序 |
|---|---|---|---|---|
message_id |
u128 HLC | 全局 | — | 仅去重与追踪 |
conversation_seq |
u64 | 单会话 | 是 | 会话内主排序键 |
last_activity_id |
u128 | 单会话 | — | 会话列表与跨会话时间轴 |
mailbox_seq |
u64 复合 | 单 MailboxShard | 个人队列稀疏 | 禁止 |
room_seq |
u64 复合 | 单聊天室 | 同 epoch 内连续 | 仅房间内 |
丢消息检测的唯一锚点是邮箱层,不是序号连续性——
conversation_seq 与 mailbox_seq 对用户视角天然稀疏(§6.10.1)。
唯一例外是读扩散档的会话历史,由服务端显式下发 write_policy 告知。
6. 一期形态(ADR-0007)¶
群规模上限 1000 人 / R_avg ≈ 22
MailboxStore = Redis(按天分桶 zset,30 天固定绝对保留)
MailboxNode 无主备,租约漂移接管
聊天室回放 = RoomWriter 进程内环形缓冲
语言 Rust + 日志 Redpanda(ADR-0006,倾向性决策)
不可变项一律按目标档定死:
virtual_bucket_count 65536、lane_count 64、message_seq_bucket_width 4096
哈希族、分区键、契约核心
升级到 10 万人群档:只换 MailboxStore 实现,走 §18.1.2 灰度 + 影子读
不改协议、不改客户端、不改游标
7. 文档导航¶
| 文档 | 内容 |
|---|---|
PLAN.md |
契约核心:命名表、五种序列、数据模型、帧总表、参数表 |
01-connection-protocol.md |
opcode、帧头字节布局、编码、压缩、多路复用 |
02-message-model-and-storage.md |
ScyllaDB DDL、压缩策略、MessageStore 读写路径 |
03-mailbox-and-group-fanout.md |
MailboxStore 三实现、lane 调度、接管算法 |
04-session-list.md |
qsession durable 投影、目标态压缩器、会话权威、未读重算与分页 |
05-chatroom-and-control-message.md |
房间广播、控制消息、表情回应、RTC |
06-microservices-and-deployment.md |
部署拓扑、扩缩容、重分片手册 |
07-reliability-security-operations.md |
fencing、检查点、鉴权、限流、告警响应 |
08-test-and-capacity-plan.md |
测试分层、压测场景、容量回填 |
09-push-and-badge.md |
APNs/FCM、去重频控、角标 |
10-retention-deletion-compliance.md |
加密擦除、删除清单、合规 |
adr/ |
7 份决策记录 |
8. 阅读顺序建议¶
新人 00 → PLAN.md §1~§5 → 01 → 02 → 03
做协议 01 + PLAN.md 附录 A
做存储 02 + 03 + PLAN.md §7
做会话列表 04 + PLAN.md §12
做运维 06 + 07 + 08