跳转至

ADR-0007 一期部署档位:简化形态 + 保留升级路径

状态

已接受(2026-08-14)。对应 docs/PLAN.md §2.2、§7.12、§10.4.2、§17.1、§18.1、§18.3、§19.4、§27.2.4。

背景

PLAN.md §25 的三档容量表以目标档(DAU 5000 万、千万连接、10 万人群、R_avg = 122) 为设计基线。按该档一期交付,意味着第一天就要建成:ScyllaDB 集群、完整 lane 向量水位、 成员 Bitmap 分片与槽位映射、检查点体系、MailboxNode 主备。

但 §25.0.2 的构成显示成本高度集中:

R_avg = 122 = 1.48(单聊) + 19.2(中群80人) + 100(大群5000人) + 1.25(控制事件)
                                            ^^^^^^^^^^^^^^^^
                                  2% 的消息贡献 82% 的邮箱写入

若一期产品上限为千人群,R_avg 降至约 22,邮箱层成本下降一个数量级, MailboxStore 的选型空间随之打开(Redis 从"不成立"变为"成立")。

同时 §18.1.1 已论证:自研 MailboxNode 等于自研一套分布式有状态存储, 把它作为一期基线的唯一方案会让交付风险集中在最难的一块上。

决策

1. 一期按简化形态建设

维度 一期取值 目标档取值
产品群规模上限 1000 人 10 万人
(参数名 max_members_per_group,附录 B.6.1;超限整批拒绝,禁止静默截断)
R_avg 约 22 122
MailboxStore 实现 Redis 7.0.0+ standalone(AOF + noeviction,30 天日桶) ScyllaDB → 自研 LSM
MailboxNode 容错 无主备,租约漂移接管(§10.4.2 形态 B) 同左(主备降级为可选 RTO 优化)
聊天室回放 RoomWriter 进程内环形缓冲(§7.12) 同左
分发日志 Redpanda(ADR-0006) 同左
检查点 仅 W[lane] + W_floor[lane] + DispatchProgress 快照 完整邮箱索引检查点

2. 不可变项一律按目标档定死

这是本 ADR 的核心:简化的是实现,不是契约。以下项建集群/建表后不可变 (§27.2.4),一期必须按目标档取值,否则升级路径被焊死:

virtual_bucket_count        = 65536      不可变
lane_count                  = 64         不可变 ← 即使千人群完全用不上
message_seq_bucket_width    = 4096       不可变
mailbox_seq / room_seq 位布局与 epoch 上界   不可变
bucket_hash / lane_id 的哈希族(blake3)     不可变
event_id / dispatch_id 的哈希输入元组        不可变
event_ordinal 的 event_type 优先级映射       不可变
全部分区键设计                              不可变
契约核心(§5.1 / §6 / §7 / 附录 A / 附录 B) 改动需 ADR

lane_count = 64 是本 ADR 最需要强调的一条:千人群的 dispatch 展开是毫秒级, §9.2.1 描述的队头阻塞根本不成立,lane 机制在一期毫无用处。但它已签入游标令牌(§6.8) 且标记为建集群后不可变(附录 B.1)——设 1 则升级到大群档时必须重建集群并换发全部游标; 设 64 的成本是 64 个计数器,约等于零。

这是唯一一条今天不做、以后就再也做不了的大群准备。

3. 一期可不做

  • 自研 LSM MailboxStore(ADR-0001 阶段二)
  • MailboxNode 主备
  • 成员 Bitmap 分片与槽位映射(千人群用有序 ID 列表足够;但在线 Bitmap 求交必须保留, 它的规模跟在线用户数相关,与群规模无关)
  • mailbox_write_policy = mention_only(ADR-0003,千人群下准入判据不成立)
  • 完整邮箱索引检查点(Redis 实现下权威数据不在节点本地)
  • 跨地域双活(§19.4 的高级租户档)

4. 一期不能砍的承重墙

以下机制与群规模无关,砍掉即丢消息或错未读,一期必须完整实现:

分发日志(提交与重放的载体,§6.9.5 永不丢弃已 COMMITTED 消息)
确定性物化(blake3 event_id、created_at 取自 dispatch 记录,§6.7)
lane 连续物化水位 + 追平才服务(§9.2、§10.4.2)
设备游标只由 MAILBOX_BATCH 连续推进(§6.8 不变量)
mailbox_trim_watermark + CURSOR_EXPIRED + REBUILD(§18.3、§9.6)
SESSION_DELTA / REACTION_UPDATE 的幂等绝对值语义(§12.6、§13.6.2)
未读权威定义式 + reconcile(§12.5)
fencing 单写 + 接管起始位点 = min_j(W[j])(§19.2、§10.4.2)
三层准入限流(§8.2)

5. 升级路径

当前:Redis 7.0.0+ standalone(AOF + noeviction)
优先:ScyllaDB

触发:产品放开群规模上限,或 standalone Redis 的资源/可靠性边界不再满足。
动作:优先更换到 ScyllaDB,实现走 §18.1 已定义的灰度流程
      单 MailboxShard 粒度切换 → 影子读比对(1% 采样)
      → mailbox_store_shadow_mismatch 非零即阻断 → 可随时回切

Redis Cluster 不在这条升级路径中。它需要独立的提交状态机、数据迁移、Cluster 路由和
端到端故障验证;现阶段必须启动拒绝,不能用局部 hash tag 宣称完成。

不需要:改协议、改客户端、改游标、改分片映射、改哈希族

客户端完全无感——这正是第 2 条把不可变项按目标档定死换来的回报。

理由

  1. 成本:一期邮箱层从 PB 级降到 TB 级,MailboxStore 从"自研分布式存储" 降到"一套 standalone Redis",交付风险不再集中在最难的一块。
  2. 风险:§18.1.1 论证的自研存储风险被推迟到有真实负载数据之后, 而 ADR-0001 的四条切换判据届时可用实测值判定,而不是拍脑袋。
  3. 不损失可选性:第 2 条保证升级只换实现不换契约。代价仅为 lane_count = 64 这类"当下无用但必须预置"的少量结构。
  4. 与 ADR-0006 协同:Rust 生态下 Redis 客户端(fred/redis-rs)成熟度高于 rust-rocksdb 的运维验证,一期用 Redis 也降低了语言选型的连带风险。

后果

正面

  • 一期 Redis 仅支持 7.0.0+ standalone,启动须严格证明版本;必须配置 AOF + noeviction;邮箱由 Redpanda 分发日志 驱动物化。DispatchProgress 默认不 GC,只有部署方显式提供并审计日志 replay-safety window 后才允许回收;Redis Cluster 配置必须拒绝启动。
  • 客户端提交/ClientDedup 窗口固定为 2 小时;邮箱保留 30 天不等于历史保留期, MessageRecord 历史只按 retention_class 生命周期处理。
  • TruncateBefore 在 Redis 实现下是真删(ZREMRANGEBYSCORE 或按天 key EXPIRE), 比 ScyllaDB 阶段的纯逻辑裁剪(受 TWCS 墓碑约束,§18.1.2)更干净。
  • 无主备使 MailboxNode 成为分片化无状态计算节点,扩缩容与故障处理显著简化。

负面

  • Redis 的 AOF everysec 有 1 秒持久性缺口。该缺口的可接受性完全依赖分发日志兜底: 崩溃后重放该窗口内的 dispatch 重新物化(确定性 event_id 保证同值覆盖)。 因此分发日志与 Redis 不能同时简化掉,§18.1 必须写明这一耦合。
  • 漂移接管的 RTO 高于热备(冷启动需重建 Bitmap 与重算水位),shard_epoch 递增更频繁, mailbox_cursor_rebase_count 的告警阈值需按漂移形态取值。
  • 产品对外承诺的群规模上限从 10 万降为 1000,§2.2 需相应标注一期口径。 该上限已作为 max_members_per_group 收进附录 B.6.1,由服务端强制执行。

替代方案及其否决理由

方案 否决理由
直接建目标档 交付周期长、初期资源利用率极低,且 ADR-0001 的切换判据在无真实负载时无法判定
简化形态但 lane_count = 1 省下的成本约等于零,却把升级路径焊死(需重建集群 + 换发全部游标)
只定契约不锁档位、两种实现都不建 抽象层容易过度设计;且 §18.1 的四原语抽象已足够承载后续切换,不需要再加一层
一期直接上 mention_only 支持大群 千人群下 α 判据不成立(§10.2.3),读扩散只省少量写入却牺牲未读精确性
Redis 承载正文与历史 正文按 365 天保留、体量远大于邮箱,且 §7.1 的 seq_bucket 分区设计针对宽列存储;正文仍用 ScyllaDB。2026-09-26 由 ADR-0018 修订:永久历史仍以 ScyllaDB 为准,但 history_hot_window(7 天)内的近期历史与提交状态留在 Redis,HistoryArchiver 异步归档——否决的是“Redis 永久承载历史”,不是“近期历史先在 Redis”

复评条件

  1. 实测 R_avg 超过 25,或 30 天邮箱日桶驻留超过 standalone Redis 的资源边界;
  2. 产品决定放开群规模上限至 1000 人以上;
  3. standalone Redis 出现单 key 倾斜或大 key 问题,且按天分桶(§18.1)无法缓解;
  4. 分发日志保留期被下调到无法覆盖 Redis 的持久性缺口窗口——此时"负面"第 1 条的 兜底论证失效,必须重新评估;
  5. 漂移接管的实测 RTO 超过 mailbox_takeover_rto_target,需要重新引入热备。