跳转至

ADR-0003 大群投递策略与降级档

状态

已接受(2026-08-13)。对应 docs/PLAN.md §2.2、§10.2、§7.5、§27.4。

背景

§2.2 承诺"普通群上限 10 万成员,保证所有成员获得持久邮箱引用"。这条承诺直接决定了 §10.2 成本模型中的 O(N) 项:

消息正文存储       O(1)
ConversationHead   O(1)
中心跨节点任务      O(S)    S = 目标 MailboxShard 数
邮箱轻量引用写入    O(N)    ← 全系统最大成本项
最终 Socket 写入   O(O)    O = 在线设备数

本设计没有消除写扩散,它消除的是 N 份正文、N 次跨服务 RPC、N 次正文编码。 评审 001 的问题 5 指出:§3 的禁令把"读扩散混合方案"一并误杀,导致只剩 "10 万人群全员写扩散"与"100 万聊天室无持久语义"两个极端,中间缺一整档; 而这个取舍如果不写进文档,后续文档作者会反复重开该议题。

对标:微信群上限 500,Telegram 超级群走拉模式,Slack channel 走游标。 本设计选了更贵的路径,换的是"离线精确可达"。

决策

  1. 默认 mailbox_write_policy = always:所有群消息为全体成员写一条 UserMailboxEntry, §2.2 的承诺不变。
  2. 保留 mention_only 降级档作为预留能力,默认不启用,作用域为 租户 + 会话规模档(§27.4)。
  3. mention_only 的启用条件是触发一次 ADR 评审,而不是自动阈值:
T1  fanout_entries_per_sec 持续超过 platform_fanout_budget 的 70%
T2  大群邮箱写占 MailboxNode 总写入预算 > 50%

任一条成立时必须启动评审;评审可以决定扩容、限速或启用降级档,禁止由系统自动切换。

  1. mention_only 的最小改动集(写死,不得在专题文档中扩大):
a. UserConversationState 增加 delivered_conversation_seq(§7.5 已预留该字段)
b. 登录 SYNC 阶段在 AUTH_OK 之后追加一批会话头:
   BIG_GROUP_HEADS[]{conversation_id, latest_conversation_seq,
                     last_activity_id, preview, unread_estimate}
   —— 复用现有 SESSION_LIST_BATCH 通道承载,不新增帧
c. 沉默成员的未读改为 latest_conversation_seq - read_conversation_seq 的估算值,
   并置 unread_exact = false
d. @提及、回复我、群公告一律无条件逐条物化,mention_count 保持精确
e. 被降级的会话仍然产生 ConversationHead 更新与在线实时推送,
   只是不再为离线沉默成员写邮箱引用
  1. mention_only 可回退:回退后新消息恢复全员写扩散,存量邮箱不回填也不删除; 回退窗口内沉默成员的未读继续按 delivered_conversation_seq 估算,直至下一次 MARK_READ 或 REBUILD 后收敛为精确值。
  2. 无论哪个档位,发送侧准入闸门始终生效(§8、附录 B.5): per_conversation_msg_rate 按群规模分档(≤1000 人 20 msg/s;1000~10000 人 5 msg/s;

    10000 人 2 msg/s)、per_sender_in_conversation_rate 1 msg/3 s、 tenant_fanout_quota 令牌桶。超限必须显式返回 RATE_LIMITED 或 FANOUT_QUOTA_EXCEEDED,绝不静默丢弃邮箱引用。

理由

  • 默认 always 是产品承诺的直接兑现:离线成员精确收到每一条群消息,是本系统相对 "超级群走拉模式"的核心差异化能力,不能在基线阶段就打折。
  • 不设自动切换阈值,因为切换是语义降级而非性能调优:沉默成员的未读会从精确变估算, 这是用户可感知的行为变化,必须有人为决策与公告,不能由容量指标自动触发。
  • 最小改动集必须提前写死,否则真到了需要降级的那天,改动会被临时设计成一个大工程, 失去"降级档"的意义。四处改动全部落在已有字段与已有帧上,无新增契约。
  • 闸门优先于降级:per_conversation_msg_rate 分档已经把 10 万人群的 fanout 上界 钉死在 2 msg/s × 10 万 = 20 万 entry/s,这是容量规划的硬输入;先用闸门保证可控, 再考虑是否降级。§2.2 的 10 万成员上限必须与这条限速一并理解。
  • @提及无条件精确,因为它是大群里唯一"必须送达"的信号;把它降级会立刻造成业务事故。

后果(正面 / 负面)

正面

  • 离线语义在默认档下是精确的:unread_count、mention_count、消息可达性都不打折。
  • 降级路径已被提前设计并限定范围,真需要时是配置变更 + 小改动,不是重构。
  • 准入闸门把最坏 fanout 量变成一个可计算的常数,使 §25 的容量公式有确定的上界输入。
  • 降级可回退,不产生不可逆的数据形态变化(存量邮箱不删)。

负面

  • 默认档下邮箱层是全系统最大的成本项,且随群规模线性增长;这是明确接受的取舍。
  • 大群限速(>10000 人 2 msg/s)会影响"万人群刷屏"类玩法;产品必须提前知晓这条约束。
  • 保留降级档意味着 delivered_conversation_seq、unread_exact 这些字段在默认档下 长期空转,属于为未来付的少量复杂度成本。
  • 降级期与回退期的未读估算与精确值之间存在过渡态,客户端 UI 必须支持 unread_exact = false 的展示(与 99+ 闭环)。

替代方案及其否决理由

方案 否决理由
直接采用 mention_only 作为默认 违反 §2.2 的"所有成员获得持久邮箱引用"承诺;沉默成员的未读永久为估算值,产品体验倒退
降低群成员上限到 500(对标微信) 与产品目标(10 万人群)冲突;且规避而非解决扩散问题
大群一律转为聊天室语义 聊天室无个人邮箱、无持久未读、只有 30 分钟回放;对"工作群"类场景不可用
按在线状态决定是否写邮箱 在线状态是软状态且有传播延迟;投递决策依赖软状态会造成不可恢复的漏投,直接违反 §11 "在线推送前必须先可靠物化邮箱引用"
按活跃度自动降级(长期不读的成员停写) "长期不读"的判定依赖用户行为,会造成同一条消息对不同成员语义不同,且用户回归时无法界定补齐范围
用 conversation_seq 游标替代邮箱(纯读扩散) 登录时必须逐会话查询,直接违反 §3 的第二、三条禁令;5000 会话用户的首屏延迟不可控
设置自动阈值切换降级档 语义降级用户可感知,必须人为决策与公告;自动切换会造成"某天未读突然变得不准"的无解客诉

补充(2026-08-14):准入判据从人数改为 α 判据,并补齐缺口定位

本 ADR 的核心决策(默认 always,mention_only 为预留档、启用需 ADR)不变。 以下三点是使该档位在被启用时正确且可决策的前置补充,已合入 §10.2.3:

  1. 准入判据不再是单一人数阈值。改为同时满足 N × (1 - α) > read_diffusion_min_saving 与 α < read_diffusion_max_active_ratio, 其中 α 为活跃成员比例。big_group_lazy_threshold 降级为由该判据反推的等效值, 标记为待实测参数。理由:α 才是决定性变量——200 人工作群 α 可达 0.7, 读扩散只省 30% 写入却让全群未读变估算;5000 人兴趣群 α 约 0.15,省 85% 才划算。 α 的观测指标 conversation_active_member_ratio{size_bucket} 已加入 §24.1.3, 它是回填阈值的唯一输入,缺该指标则阈值只能拍脑袋。

  2. 补齐读扩散档的历史缺口定位(原设计的真实漏洞)。silent 期间的消息不产生邮箱条目, 而 §6.10.1 禁止用 conversation_seq 差值判缺口,且 delivered_conversation_seq 此前是纯服务端字段——客户端因此无法发现本地历史缺了哪一段。 现规定 SESSION_LIST_BATCH.sessions[] 必须下发 write_policy, mention_only 时必带 delivered_conversation_seq,客户端据此按 latest_conversation_seq 校验并用 PULL_HISTORY 补齐(§6.10.1 例外条款)。 这是该档位可被默认开启的前置条件;在此之前启用会造成用户侧"历史缺失且不可检测"。

  3. 档位切换加滞回。退出阈值 = 等效阈值 × policy_switch_exit_ratio(0.8), 切换后保持 policy_switch_min_interval(24 h)。否则成员数在阈值附近波动会使邮箱 出现"有条目/无条目"的交替分段,正好放大第 2 点的缺口问题。

三点均不改变默认值与 §2.2 的产品承诺。若未来要把 mention_only 改为某规模以上的默认档, 仍须重开本 ADR——那是产品承诺的改变(离线精确可达从平台保证降级为 ≤ 阈值的会话保证)。

复评条件

出现以下任一情况时必须重开本 ADR:

  1. T1 或 T2 在生产环境连续 7 天成立。
  2. §2.2 的群成员上限被上调(例如从 10 万到 50 万)。
  3. per_conversation_msg_rate 的分档被上调,导致平台 fanout 预算上界改变。
  4. 附录 B.7 回填后显示邮箱层单位成本超过初始估算的 3 倍。
  5. E2EE 在大群启用(见 §22 降级矩阵):预览与 mention_type 的服务端可判定性发生变化,需重新评估最小改动集是否仍成立。
  6. MailboxStore 切换为自研实现(ADR-0001 阶段二)后邮箱写入成本大幅下降,应评估是否可永久删除降级档以简化代码。
  7. conversation_active_member_ratio 回填后显示 α 的实测分布与假设显著偏离(例如大群 α > 0.5),此时准入判据的两个参数需重新取值,等效阈值随之变化。
  8. 提出把 mention_only 设为某规模以上的默认档——这会改变 §2.2 的产品承诺,必须重开本 ADR 并同步修订 §2.2 措辞,不得以配置变更的方式实施。