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 走游标。 本设计选了更贵的路径,换的是"离线精确可达"。
决策¶
- 默认
mailbox_write_policy = always:所有群消息为全体成员写一条UserMailboxEntry, §2.2 的承诺不变。 - 保留
mention_only降级档作为预留能力,默认不启用,作用域为 租户 + 会话规模档(§27.4)。 mention_only的启用条件是触发一次 ADR 评审,而不是自动阈值:
T1 fanout_entries_per_sec 持续超过 platform_fanout_budget 的 70%
T2 大群邮箱写占 MailboxNode 总写入预算 > 50%
任一条成立时必须启动评审;评审可以决定扩容、限速或启用降级档,禁止由系统自动切换。
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 更新与在线实时推送,
只是不再为离线沉默成员写邮箱引用
mention_only可回退:回退后新消息恢复全员写扩散,存量邮箱不回填也不删除; 回退窗口内沉默成员的未读继续按delivered_conversation_seq估算,直至下一次MARK_READ或 REBUILD 后收敛为精确值。- 无论哪个档位,发送侧准入闸门始终生效(§8、附录 B.5):
per_conversation_msg_rate按群规模分档(≤1000 人 20 msg/s;1000~10000 人 5 msg/s;10000 人 2 msg/s)、
per_sender_in_conversation_rate1 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:
-
准入判据不再是单一人数阈值。改为同时满足
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, 它是回填阈值的唯一输入,缺该指标则阈值只能拍脑袋。 -
补齐读扩散档的历史缺口定位(原设计的真实漏洞)。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 例外条款)。 这是该档位可被默认开启的前置条件;在此之前启用会造成用户侧"历史缺失且不可检测"。 -
档位切换加滞回。退出阈值 = 等效阈值 ×
policy_switch_exit_ratio(0.8), 切换后保持policy_switch_min_interval(24 h)。否则成员数在阈值附近波动会使邮箱 出现"有条目/无条目"的交替分段,正好放大第 2 点的缺口问题。
三点均不改变默认值与 §2.2 的产品承诺。若未来要把 mention_only 改为某规模以上的默认档,
仍须重开本 ADR——那是产品承诺的改变(离线精确可达从平台保证降级为 ≤ 阈值的会话保证)。
复评条件¶
出现以下任一情况时必须重开本 ADR:
- T1 或 T2 在生产环境连续 7 天成立。
- §2.2 的群成员上限被上调(例如从 10 万到 50 万)。
per_conversation_msg_rate的分档被上调,导致平台 fanout 预算上界改变。- 附录 B.7 回填后显示邮箱层单位成本超过初始估算的 3 倍。
- E2EE 在大群启用(见 §22 降级矩阵):预览与
mention_type的服务端可判定性发生变化,需重新评估最小改动集是否仍成立。 MailboxStore切换为自研实现(ADR-0001阶段二)后邮箱写入成本大幅下降,应评估是否可永久删除降级档以简化代码。conversation_active_member_ratio回填后显示 α 的实测分布与假设显著偏离(例如大群 α > 0.5),此时准入判据的两个参数需重新取值,等效阈值随之变化。- 提出把
mention_only设为某规模以上的默认档——这会改变 §2.2 的产品承诺,必须重开本 ADR 并同步修订 §2.2 措辞,不得以配置变更的方式实施。