跳转至

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