Q-IM 名词解释 × 模块对照¶
更新日期:2026-09-01(按
docs/jargon.md做了一轮去黑话:行话首次出现处附大白话,或改写为平实说法) 与docs/glossary.md的分工:那份按设计概念分类;本份逐名词解释并绑定到具体模块与代码标识符, 重点消歧同一个词在 Kafka/Redpanda、Redis、ScyllaDB、服务进程之间的不同含义。 全部数值均核对自crates/qim-proto/src/consts.rs与各 crate 源码;契约核心以docs/PLAN.md为准。 本表使用的工程行话(失败闭合、fence、HOL、ρ、CAS、TOCTOU 等)的大白话解释见docs/jargon.md。
阅读约定:每条格式为 名词 — 解释。归属:拥有它的 crate/模块。标识符:代码里的实际名字。约束:改它之前必须知道的事。
0. 先读:七组最容易混淆的词¶
同一个词在不同层指不同东西。遇到不加限定的用法时按下表的默认含义理解。
| 词 | 默认含义 | 其他含义 | 怎么区分 |
|---|---|---|---|
| shard / 分片 | MailboxShard(64 个逻辑投递分片) | Redis 邮箱实例数、MessageStore 提交桶分片、ScyllaDB 内部分片 | Redis 那个一律说「实例」,不说分片 |
| partition / 分区 | Kafka/Redpanda topic 分区 | 无——Redis 与本项目无 partition 概念 | dispatch 分区号 == MailboxShardId |
| bucket / 桶 | 虚拟桶(用户→shard,65,536 个) | 提交桶(会话→Redis 实例,256 个)、历史分页桶(4,096 seq 一桶)、邮箱日桶 | 看前缀:VIRTUAL_/COMMIT_/MESSAGE_SEQ_/mailbox:…:{day} |
| epoch | shard_epoch(MailboxSeq 高 16 位:恒零 bit63 + 15 位有效值) |
session_epoch(登录代际)、wm_epoch(水位里存的 shard_epoch 副本) |
三者互不换算 |
| offset | Kafka log offset(48 位) | canonical_log_offset(首次观察)、observed_log_offset(本次观察) |
后两者的差别是重放安全核心,见 §5 |
| consumer | Kafka 静态 assign 消费者 | mailbox 进程内的 shard FIFO worker(不是 Kafka consumer) | 看类型名:RdkafkaDispatchLogConsumer vs DispatchShardWorkers |
| watermark / 水位 | W[lane] 连续投递水位 |
W_floor、W_recomputed、read_conversation_seq(已读水位) |
已读水位与投递水位无关 |
高频动词的大白话(全文通用,不再逐处解释):物化 = 把投递指令落成持久数据; 推进水位 = 把「处理到哪了」的进度标记往前挪;重放 = 把日志里的记录重新执行一遍 (靠幂等保证重复执行无害);落盘 = 写进持久存储并确认成功;在途/销账 = 已开始未完成的任务集合 / 完成后从该集合移除(旧直写模型的连续水位算法;当前水位语义见 §5);durable = 持久的、崩溃后仍在; 透传 = 原样转发不修改;尽力而为 = 不保证成功、失败不补偿(须另有兜底)。
1. 系统实体(服务进程与逻辑组件)¶
ConnectionNode — 持有客户端 TCP/TLS 长连接的接入进程,负责认证、帧解析、推送下发、游标推进。归属:qim-gateway。约束:backend.rs 是游标推进唯一入口;push.rs 不得推进游标。指标端口 9100。
ConversationWriter — 消息提交的权威。为每条消息分配 conversation_seq、写 MessageRecord、发布 Outbox,只有它能产生 SEND_ACK。归属:qim-writer。标识符:DurableMessageCommitter。约束:trace_id 由它生成(§24.2),下游只透传;多实例按 conversation_id 做会合哈希亲和路由(尽量把同一会话送到同一实例,见 §2;gateway/state.rs::rendezvous_pick),路由只是优化,任何 writer 处理任何会话都正确。指标端口 9101。
FanoutCoordinator — 消费 Outbox,把一条已提交消息按冻结的成员快照展开为每目标 MailboxShard 恰好一条 GroupDispatch,写入 dispatch 日志。归属:qim-fanout。约束:不用 Kafka 事务(ADR-0012);「先取得全部消息已写出的证据、再提交自己的消费进度(位点)」这个顺序不得调换——反过来会在崩溃重启后跳过没写出的消息,永久丢失;重试永不丢弃已 COMMITTED 的消息。指标端口 9106。
MailboxNode — 消费 dispatch 日志,把投递指令落成每个用户邮箱里的真实条目(物化),并服务 PULL_MAILBOX。归属:qim-mailbox。标识符:MailboxMaterializer。约束:已取消 writer→mailbox 的 TCP 热路径,物化只从 Redpanda 进入;pull.rs 是唯一推进设备游标的路径。指标端口 9102。
HistoryArchiver — 独立消费 Outbox,把 canonical MessageRecord 批量普通 INSERT 归档到 ScyllaDB(先记录后索引),全批取得确认后同步提交位点,再发布归档水位 msgarchive:watermark(ADR-0018)。归属:qim-archiver(消费端 qim-commit-log::archive,写入 ScyllaMessageStore::archive_records)。约束:独立消费组;位点先于归档提交 = 历史永久缺失,顺序不得调换;批内同 conversation_seq 异内容必须拒绝;归档器停摆时水位停住、Redis 只涨不删。指标端口 9107。
SessionProjection — 维护会话列表的物化视图(预先算好存起来、读时不必重算的视图)与定义式未读。归属:qim-session。约束:独立消费同一条 dispatch 日志,有自己的 Redis checkpoint;MARK_READ 成功后必须失效该用户的内存快照,否则最长 5 分钟返回旧未读。指标端口 9103。
MessageStore — 消息历史与正文、提交状态的存储抽象。归属:qim-store::message。标识符:trait MessageStore,实现 RedisMessageStore / ScyllaMessageStore / MemoryMessageStore。约束:禁止显式 DELETE,删除走 TTL 或加密擦除;message_record 表级 TTL 必须为 0,按行 USING TTL。
MailboxStore — 邮箱引用、DispatchProgress、lane 水位的存储抽象。归属:qim-store::store。标识符:trait MailboxStore,实现 RedisMailboxStore / ScyllaMailboxStore / MemoryMailboxStore。约束:三级演进——一期 Redis(ADR-0007)→ 阶段一 ScyllaDB → 阶段二自研 LSM(ADR-0001)。Redis 与 Scylla 两条线长期并存(BRANCHES.md)。
GroupMembership — 群成员权威、冻结快照与成员可见区间。归属:qim-common::membership(建群、读取、键布局)与 qim-store 提交脚本 membership_change.lua(成员变更,ADR-0021)。标识符:GroupMembership,方法 create_group / snapshot / snapshot_at / is_member / member_interval(s) / page;事件类型 qim_common::membership_event::{MembershipOp, MembershipChange}。Redis key grp:{bNNN}: / grpmeta:{bNNN}: / grpsnap:{bNNN}: / grpmember:{bNNN}:(与该群会话提交事实同桶、同实例)。约束:成员变更只经 MessageStore 的提交脚本(与序号分配同一原子区),区间 A:j / L:j:l / R:j:l;GroupMembership 必须用 MessageStore 的同一实例列表装配(QIM_REDIS_ADDR + QIM_MESSAGE_STORE_ADDRS,同序)。
ShardRegistry — 分片归属的唯一权威(设计层)。约束:日志消费用静态 assign(),禁用 Kafka 消费者组的自动重分配(rebalance)——分区归属必须静态指派,不允许在不可控的时机漂移;Redis 实例映射当前绕过了它(见 §11)。
PresenceDirectory / NotificationService / ModerationService / MediaService / RoomWriter — 设计层实体,前四者尚未实现;聊天室为进程内环形缓冲(qim-gateway::room)。
qimctl — 运维控制台。约束:GroupAdd 即使 loopback 也强制 Admin mTLS。
qim-loadgen — 验收与压测。约束:不得自带状态机/游标/去重副本,依赖 qim-sdk;5% 连接由真实 SDK 驱动(SDK 回归泳道),其余留在帧层以取得 ACK/PUSH 精确时刻。
2. 分片、桶与路由¶
虚拟桶(user_bucket) — 用户到 MailboxShard/ConnectionShard 的稳定中间层。归属:qim-proto::routing。标识符:VIRTUAL_BUCKET_COUNT = 65_536,routing::route(tenant, user, shard_count)。约束:建集群后不可变;禁止用物理节点数直接取模分配用户。
MailboxShard — 逻辑投递分片。每个用户经虚拟桶固定归属一个。归属:qim-proto 定义,qim-writer / qim-fanout / qim-mailbox 三者共享。标识符:MailboxShardId(u32 newtype)、QIM_MAILBOX_SHARD_COUNT(默认 MAILBOX_SHARD_COUNT_DEFAULT = 64)。约束:必须为 1..=VIRTUAL_BUCKET_COUNT 的二次幂;三个服务必须读到同一个已验证值,任一方硬编码 64 即失败;必须等于 dispatch topic 分区数。
ConnectionShard — 连接层逻辑分片。标识符:CONNECTION_SHARD_COUNT_DEFAULT = 1_024。
lane — MailboxShard 内的目标可见性隔离通道。归属:qim-proto。标识符:LaneId,LANE_COUNT = 64(一期定死,LANE_COUNT_MAX = 256)。约束:lane_id = blake3(tenant_id, user_id)[0] & (lane_count-1),独立哈希、不从 user_bucket 推导,故分片分裂不改任何用户的 lane 归属。目标语义是每 lane 独立推进、一个大群只阻塞同 lane 用户;当前 finish_dispatch 等整条 record 完成后统一推进 64 lane,仅数据格式与映射已落地,独立推进仍是发布阻断。
提交桶(commit bucket) — 会话到 MessageStore Redis 实例的中间层。归属:qim-store::message::redis。标识符:COMMIT_BUCKET_COUNT = 256,commit_bucket(tenant, conversation),key 标签 {b042} 形式。约束:与邮箱分片是独立的两套映射。
Redis 邮箱实例 — 承载 MailboxStore 的物理 standalone Redis 进程。标识符:QIM_MAILBOX_REDIS_SHARDS(1..8)、RedisMailboxStore::conns。约束:映射为 shard % 实例数,实例数必须整除 64;这是已知缺陷(§11)。
Redis core 实例 — 承载 MessageStore 的物理 Redis。标识符:QIM_MESSAGE_STORE_SHARDS(1..8)。约束:映射为 bucket % 实例数,实例数必须整除 256。
ScyllaDB 分片 — 驱动内部 token-aware 路由。归属:qim-store::scylla。标识符:SessionBuilder::known_nodes。约束:应用层不感知也无需感知;生产门禁 RF=3 + LOCAL_QUORUM + Tablets 禁用。
会合哈希(rendezvous / HRW) — 对每个候选实例算 hash(key, 实例) 取最高分的选点算法:加减实例只挪动最少量 key。gateway 用它选 writer(亲和路由:尽量送同一实例,纯属优化)。归属:qim-gateway::state。标识符:rendezvous_pick。约束:只是亲和性优化,实例断线就近回退无需补偿。
历史分页桶 — conversation_seq 按固定宽度分桶以便翻页。标识符:MESSAGE_SEQ_BUCKET_WIDTH = 4_096,HISTORY_MAX_EMPTY_BUCKET_SCAN = 8。约束:conversation_seq 允许空洞,必须有连续空桶扫描上限。
3. 日志(Redpanda)¶
Outbox — writer 把已提交消息事实写入的 durable 日志,是 fanout 的输入。归属:qim-commit-log。标识符:OutboxPublisher / RdkafkaOutboxPublisher,topic QIM_OUTBOX_TOPIC(默认 qim-outbox),固定单分区 QIM_OUTBOX_PARTITION = 0,LogStream::Outbox。约束:key 为 message_id;允许「发布成功、状态标记失败」后产生重复记录,去重边界在下游 dispatch_id;Kafka 的幂等生产者(enable.idempotence)只防同一个生产者的网络层重发,防不了应用层重发,不是应用级去重。
dispatch 日志 — fanout 的输出、mailbox 与 session 的输入。标识符:topic QIM_DISPATCH_TOPIC(默认 qim-dispatch),LogStream::MailboxDispatch { shard }。约束:分区数 == QIM_MAILBOX_SHARD_COUNT == QIM_SESSION_DISPATCH_PARTITION_COUNT;partition index == MailboxShardId。
Kafka 分区(partition) — 只在上述两个 topic 上有意义。归属:只有 qim-commit-log 允许出现 Kafka 概念。约束:分区级暂停/恢复(pause_shard / resume_shard)操作对象是 Kafka 分区,不是 Redis。
log offset — 分区内物理位点。标识符:log_offset,LOG_OFFSET_BITS = 48。约束:构成 MailboxSeq 低 48 位。
GroupDispatch — 一条 dispatch 记录:某消息投向某 MailboxShard 的全部收件人。归属:qim-common::commit。标识符:GroupDispatch,当前含 dispatch_id / target_shard / sender / client_message_id / membership_version / recipient_chunks。约束:必须携带 canonical sender 与 client_message_id(v1 缺此两字段者升级时显式拒绝);每目标 shard 恰好一条。目标契约另要求 fencing_epoch 与消费侧单调过滤,但当前结构/编解码尚缺,属于发布阻断。
dispatch_id — dispatch 的确定性身份,重放去重的逻辑边界。标识符:DispatchId,blake3("qim.group-dispatch.v1\0" || message_id_be16 || shard_be4)[0..16]。约束:禁随机 UUID。
payload_digest — dispatch 编码后的 BLAKE3。标识符:DispatchDigest。约束:同 dispatch_id 异 digest 必须停止服务(数据损坏)。
DispatchLogRecord — 已解码的 dispatch + 其日志位置。标识符:DispatchLogRecord { position: LogPosition, dispatch: GroupDispatch }。RawDispatchLogRecord 为未解码版本(当前无调用方,read_next_raw 是死代码)。
DispatchLogConsumer — dispatch 日志消费者抽象。归属:qim-commit-log。标识符:trait DispatchLogConsumer(read_next / pause_shard / resume_shard / readiness),实现 RdkafkaDispatchLogConsumer、测试用 InMemoryDispatchLogConsumer。约束:静态 assign() 全部分区,从不向 Kafka 提交消费进度(offset)——崩溃后从哪继续,以自己持久化的水位 W 为准;禁 subscribe()(CI 门禁)。
Outbox 消费者(fanout) — 唯一使用消费者组的地方。标识符:BaseConsumer + QIM_FANOUT_GROUP_ID。约束:只能在首次定位或 source 不连续时跳转读取位置(seek),每次成功拉取(poll)后更新 positioned_after——曾因反复 seek 把吞吐限死在 2 条/s。
DispatchPartitionStart — 一个静态分区的起始读取位置。标识符:DispatchPartitionStart { shard, offset }。约束:起点 = min(W) + 1,不取消费者组已提交位点。
DispatchPartitionPosition::Pending — librdkafka 静态 assign 后、空 topic 未 poll 时 position() 短暂返回 Offset::Invalid。约束:这是「尚未初始化」不是越界,qsession 必须保持 checkpoint 不动并重试;其他负 offset 仍直接报错停下(失败闭合)。
Redpanda — 选定的日志实现(非 Apache Kafka,ADR-0006)。约束:最低 v26.2.2(需 KIP-516 非零 topic UUID);qsession checkpoint 绑定 cluster_id + topic_id + partition_count。
4. 五种序列与两种位点¶
message_id — 全局唯一、时间有序的消息 ID。归属:qim-proto::ids。标识符:MessageId(u128,HLC)。约束:按 §6.2 位布局含 region_id / writer_id,缺 writer_id 时两 writer 同毫秒会撞 ID;客户端按它去重。
conversation_seq — 单会话严格递增的历史顺序。标识符:ConversationSeq。约束:允许空洞(预留窗口 CONVERSATION_SEQ_RESERVE_WINDOW = 4_096、治理删除、可见性裁剪);禁用差值判定丢消息;禁 latest - read 算未读。
last_activity_id — 会话列表排序键,与 message_id 同源同格式。标识符:LastActivityId。
mailbox_seq — MailboxShard 的复合投递序号。标识符:MailboxSeq = (shard_epoch:16, log_offset:48)(ADR-0004;高 16 位 = 恒零的 bit63 + 15 位有效 epoch,SHARD_EPOCH_MAX = 0x7FFF)。约束:跨 epoch 单调不回退;个人队列稀疏;同群消息在同一 shard 只分配一个;bit63 恒零(CQL bigint 有符号排序在跨 2^63 时翻转)。
room_seq — 聊天室短期顺序。标识符:RoomSeq。
canonical_log_offset — 某 dispatch 首次被观察到的 offset,决定其 mailbox_seq。归属:qim-store DispatchProgress。约束:重放不变。
observed_log_offset — 某 dispatch 本次被观察到的 offset。约束:仅当该 record 的全部 lane/chunk/entries 已 durable,且各 lane 的 expected_previous CAS 连续校验通过时,才用该值推进 64 lane W;重复记录也使用本次观察到的新 offset,绝不复用旧值。DispatchProgress GC 的安全证明必须用最大 observed,用 canonical 会误删后重分配 seq。
HLC — 混合逻辑时钟,message_id 的时间来源。归属:qim-writer::hlc / identity。标识符:HLC_EPOCH_MILLIS = 1_577_836_800_000,reserve_hlc,CLOCK_REGRESSION_REJECT_MS = 5_000。约束:writer 启动时从 Redis 原子领取严格越过历史高水位的 60s 毫秒窗口,提前 20s 续领,窗口耗尽即拒绝发号(时钟回拨不复用旧 ID)。
writer_id — writer 实例序号,进入 message_id 位布局。标识符:QIM_WRITER_ID(显式)或 Redis 租约自动分配(identity::acquire,20s 续约)。约束:显式配置仅用于有稳定序号的环境(k8s StatefulSet)。
region_id — 地域 ID,进入 message_id 位布局。标识符:cfg.region_id。
5. 游标与水位¶
W[lane](连续水位) — 该 lane 已完整物化并可安全暴露给客户端的最大 observed_log_offset。归属:qim-store 持久,qim-mailbox 缓存。标识符:lane_watermarks(shard)、LaneWatermark、WatermarkCache。约束:当前 mailbox_seq 来源是 dispatch partition offset;同一 partition/shard 顺序消费时,只有该 record 的全部 lane/chunk/entries 已 durable,且每个 lane 的 expected_previous 通过 CAS 连续校验,W 才推进到本次 observed offset。任一失败或 CAS 缺口都不得推进;不能以「见过的最大值」跨过未物化区间,否则客户端游标会静默跳过消息。W = min(在途) - 1 与分配时登记、完成时销账仅是历史直写模型,不是现行实现。
W_floor — 已证明不会再回退的下界。W_recomputed — 重算校验值,必须 == W。约束:三者构成一条 lane 记录 presence(1) + W(6) + W_floor(6) + W_recomputed(6) = 19 字节(PACKED_WATERMARK_LANE_BYTES)。
packed watermark — 64 lane 水位在 Redis 的存储表示。归属:qim-store::redis::watermark + 5 个 Lua 脚本。标识符:key wm:{shard},字段恰好 3 个 wm_layout / wm_epoch / wm_blob。约束:v2(ADR-0015)——wm_blob 长度自身编码是否全同:19 字节 = 64 lane 均等于该记录,1216 字节 = 逐 lane,合法长度只有这两个;读兼容 v1,写一律 v2;旧代码读到 v2 会在 layout 与长度两道独立检查上拒绝服务(失败闭合:安全但停服),因此不支持新旧版本在同一 shard 上并存换版(滚动升级);升级必须先停掉旧进程、确保它再也写不进来(fence),再启动新实例;legacy 192-field 布局禁运行期在线迁移。
materialized_watermark — 设计层名词,即按 lane 分组的 W[lane] 向量(§6.5.1)。
mailbox_trim_watermark — 已物理删除的最大 mailbox_seq,是「空洞可安全跳过」规则的前置条件。
MailboxCursor(设备游标) — 设备已应用完成的邮箱位置,≠ 网络已发送位置。归属:qim-gateway::backend(推进唯一入口)、qim-mailbox::pull(服务端唯一推进路径)、qim-sdk::cursor(本地持久)。约束:只能由 MAILBOX_BATCH 连续推进,PUSH_EVENTS 不得越位推进;每设备独立;禁把分片级水位与个人游标直接比较(全网空拉)。
read_conversation_seq(已读水位) — user 级、跨设备共享的已读位置。归属:qim-session,Redis key read:{user}。约束:与投递水位无关;是定义式未读的权威输入之一;经 SESSION_LIST_BATCH.sessions[] 下发,客户端合入只进不退。
delivered_conversation_seq — 读扩散降级档(mention_only)里显式告知的历史缺口边界。约束:是「邮箱层唯一锚点」的唯一例外。
DispatchProgress(DP) — 某 dispatch 的物化进度与稳定身份。归属:qim-store。标识符:Redis key dp:{shard}:{dispatch_id},完成索引 dpcomplete:{shard};类型 DispatchProgress / LaneDispatchProgress / DispatchReceipt;begin_dispatch 返回 BeginDispatchResult::Fresh | Existing。约束:DISPATCH_PROGRESS_RETENTION_MILLIS = 7 天;GC 由 MailboxGcConfig 控制,默认 u64::MAX 禁用,只有显式注入经审计的 replay safety window 才允许删;安全证明需所有 lane 的 W/W_floor 越过该 dispatch 最大 observed offset 且最后一次重复记录被观察后已超 replay window。
lane manifest — begin_dispatch 时持久化的每 lane 期望 chunk 清单。标识符:LaneChunkManifest,DP 字段 manifest(DPM\1 头)与稀疏 m:N。约束:advance 不能信任调用方临时传入的空列表;canonical manifest 的最高 (lane, chunk) 是唯一 fixed final。
Fresh 融合快路径 — standalone Redis 专用:仅 Fresh、唯一收件人、单 chunk、可完整证明的空或同日 V4 热态时,一条 Lua 原子写 DP + 邮箱事件组 + V4 proof + 完成索引 + 64 lane W。标识符:try_materialize_fresh_single_recipient,脚本 materialize_fresh_single_recipient.lua。约束:Ok(None) 保证 7 个关联 key 零写并走通用链;Err 必须停止 shard;网络结果未知时禁止改走普通路径重试(此刻可能已写入一半,重做会写重);是跨 hash tag 事务,不代表 Redis Cluster 支持。
6. 提交与消息模型¶
CommitState(四态) — RESERVED → RECORDED → OUTBOXED → COMMITTED。归属:qim-writer::committer + qim-store::message。标识符:CommitState,CommitIntent(RESERVED 时必须持久包含完整 canonical MessageRecord)。约束:只有 CommittedMessage 能生成 SEND_ACK;任一中间态由请求重试或恢复器(scan_recoverable_commits)续做;恢复器不得依赖客户端再次上传 payload。
client_message_id(cmid) — 客户端的幂等键(识别「这是同一次发送的重发」的唯一标识),每设备由 SQLite BEGIN IMMEDIATE 原子预留 ordinal 号段后以域分隔 BLAKE3 映射为 u128。标识符:ClientMessageId,Redis dedup: / cmidown:{cmid}。约束:gateway 必须校验非零 16 字节;同 cmid 不同消息必须拒绝(assert_cmid_conversation);CLIENT_DEDUP_TTL_SECONDS = 7_200(ADR-0008 从 24h 收窄);仅发送者本人的邮箱条目回带 cmid 作对账锚点。
MessageRecord — canonical 消息事实。标识符:MessageRecord,Redis msgrecord:{b}:… / msghistory:{b}:…;Scylla 表 message_record / history_index / message_commit / commit_recovery / conversation_sequence。约束:body_included 为 optional 尾部字段,旧记录缺失按 true 解释;MessageType raw 值逐字节遵守 proto(UNKNOWN=0、TEXT=1、MEDIA=2、CUSTOM=3、CONTROL=4),未知 u32 禁降级。
MessageState — Normal / Recalled / Edited / Deleted。约束:未读定义式排除 Recalled/Deleted。
RetentionClass — Default / Ephemeral24h / ComplianceHold / TenantCustom。约束:当前只有 Ephemeral 自动 24h 到期,其余需显式租户/合规策略驱动(未实现,发布阻断);提交辅助状态保留 2 小时由 writer 每分钟最多清 256 条。
membership_version — 群消息在 conversation_seq 分配后、Outbox 前固化的成员版本。标识符:MembershipVersion,GroupMembership::snapshot_at。约束:fanout 只能按精确版本读快照,禁重放时改用当前成员。
Entry(邮箱条目) — 邮箱里的一条投递引用。标识符:Entry,Redis mailbox:{tenant:user}:e{epoch}:{day}(zset,30 天绝对到期);Scylla mailbox_entry。约束:只存引用不复制正文,小会话例外(INLINE_BODY_BUDGET_BYTES = 8 KiB、INLINE_BODY_MAX_BYTES = 2 KiB,ADR-0005);事件组每 (user, mailbox_seq) 至多 MAILBOX_EVENT_GROUP_MAX_ITEMS = 8 条 / 8 KiB,每 EventType 至多 1 条。
EventType — Message = 0 / Mention = 1 / Membership = 2 / Control = 3。
V4 retention proof — 邮箱元数据的保留证明,区分自然 TTL 缺口与数据丢失。标识符:retention_format_version = 4(不是 epoch),key mbmeta: / mbseqbucket: / mbdays:,字段 expired_gap_boundary / user_trim_seq;Scylla mailbox_retention_meta / mailbox_retention_bucket。约束:V1~V3、身份索引缺失、跨日同序号或损坏 proof 一律判损坏并拒绝服务(失败闭合);MAX_MAILBOX_ENTRIES_PER_USER = 50_000 触发强制裁剪并发 CURSOR_EXPIRED。
ConversationKind — Direct = 0 / Group = 1 / Room = 2,编码在 conversation_id 高位。标识符:ConversationKind::of,routing::direct_members(Direct 成员纯计算不碰存储)。约束:wire group_id 经 strip_kind 后用 group_conversation_id 归一,唯一性与授权均用 (tenant_id, canonical ConversationId)。
HistoryAuthorizer — history_page 在任何 key 查询前必须执行的鉴权。标识符:trait HistoryAuthorizer,RedisHistoryAuthorizer(Group 走 GroupMembership)、DirectHistoryAuthorizer。约束:授权依赖不可用时整页失败,禁返回空页伪装成功。
UserKey — (tenant_id, user_id)。约束:当前单租户 legacy tenant 固定 0。
7. 会话列表与未读¶
UserSessionProjection — 从邮箱派生、可重建的会话列表物化视图。归属:qim-session::projection。约束:投影行只留会话集合 convs:{user}(zset,成员 hex(cid),分值 created_at,ZADD GT 只增不减);会话头不额外写第二份,读时经 qstore 两轮 pipeline 取最新 MessageRecord。
UserConversationState — 置顶、静音、已读、隐藏等用户主动状态,是会话集合的持久权威(设计层)。一期只实现会话集合子集:ScyllaDB user_conversation_state(qim-store::conversation_state),由 qsession 在会话首次进入用户集合时登记,投影越窗或丢失后按投影纪元惰性重建(ADR-0021);成员区间与已读水位分别在 GroupMembership 与 read:{user}。
ConversationHead — 每条消息只更新一次的会话公共头(设计层)。
Projection RPC — mailbox 物化完成后发给 session 的尽力而为通知。标识符:RpcKind::Projection,try_send_keyed。约束:满则计 qim_projection_dropped_total,不得等待并阻塞同 shard FIFO;qsession 的独立 durable consumer 承担最终恢复。
定义式未读 — |{m | m.seq > read_conversation_seq ∧ m.sender_id ≠ 自己}|。归属:qim-session::unread(服务端)与 qim-sdk(本地,同一式子)。约束:精确窗口 UNREAD_PRECISE_LIMIT = 200,超过返回饱和值;在读路径算,不在写路径算;SESSION_DELTA 禁增量语义。
UserBadgeState — 用户级全局未读聚合(设计层,BADGE_UPDATE 帧)。
session_projection checkpoint — qsession 消费 dispatch 的持久位点。标识符:Redis session_projection:checkpoint: / session_projection:meta。约束:绑定 cluster_id + topic_id + partition_count;写完 partition record 的全部 recipients 后才做带前值比对的原子推进(CAS);缺失/全零 topic UUID、同名重建、位点越界一律拒绝启动或推进(fail closed)。
8. 连接、协议与客户端¶
session_epoch — (tenant, user, device) 维度单调递增的登录代际。约束:由 Auth/Session 服务分配,ConnectionNode 只校验不生成;与 shard_epoch 无关。
帧头 — 固定 FRAME_HEADER_LEN = 20 字节大端,FRAME_MAGIC = 0x514D,PROTOCOL_VERSION = 1。归属:qim-codec。约束:header_crc 只覆盖帧头,magic/crc 失配立即断连禁重同步;MAX_FRAME_BYTES = 4 MiB;多路复用按 STREAM_WRITE_CHUNK_BYTES = 64 KiB 分片写出(否则大帧阻塞 PONG);FRAME_COMPRESS_MIN_BYTES = 1024;帧层 NEED_ACK 保留不用。
Opcode — 帧操作码。归属:qim-proto::opcode。主链路:Auth 0x0001 / AuthOk 0x0002 / Ping 0x0003 / Pong 0x0004 / Kicked 0x0006 / Error 0x0007 / PullMailbox 0x0101 / MailboxBatch 0x0102 / SendMessage 0x0201 / SendAck 0x0202 / PushEvents 0x0203 / PullHistory 0x0301 / HistoryBatch 0x0302 / PullSessionList 0x0303 / SessionListBatch 0x0304 / SessionDelta 0x0305 / MarkRead 0x0401 / ReadSync 0x0406(契约已定、故意不实现) / RoomJoin 0x0501 / PullMembers 0x0A01 / CreateGroup 0x0A03。完整表见 opcode.rs 与附录 A。
ErrorCode — proto 错误码。归属:qim-proto/proto/qim/common.proto。CURSOR_EXPIRED = 1(触发 REBUILD) / CURSOR_REBASED = 2 / CURSOR_INVALID = 3 / MAILBOX_DIRTY = 4 / SYNC_INCOMPLETE = 5 / SHARD_MOVED = 6 / RATE_LIMITED = 8 / TOKEN_EXPIRED = 10 / CLOCK_UNSAFE = 13 / PAYLOAD_TOO_LARGE = 14 / PERMISSION_DENIED = 15 / DEVICE_LIMIT_EXCEEDED = 17。
KickedReason — REPLACED = 1(同设备重复登录顶号,一期按 uid) / ADMIN / BANNED / TOKEN_REVOKED。
心跳 — PING_INTERVAL_INITIAL_SECS = 60,前台上限 120、后台上限 240,服务端经 PONG 下发;SEND_ACK_TIMEOUT_SECS = 3 触发 probe;TCP_USER_TIMEOUT_SECS = 15;UNAUTH_CONNECTION_TIMEOUT_SECS = 10。
限流 — PER_USER_MSG_RATE = 20.0/s,PER_USER_MSG_RATE_BURST = 40;CONN_SEND_HARD_WATERMARK_ITEMS = 8_000 触发慢消费者断连。归属:qim-common::ratelimit。
内部 RPC — 服务间私有协议。归属:qim-common::rpc。标识符:RpcKind(SendMessage / SendAck / PushBatch / Projection / SessionListReq / SessionList / PullMailbox / MailboxBatch;Dispatch 已废弃),RpcSenderPool(RetryPolicy::Drop,500ms 重连退避)。约束:生产边界 TLS 1.3 mTLS + 叶证书指纹角色白名单(InternalPeer:Gateway / Writer / Mailbox / Session / Admin / Local);方法矩阵固定,越权计 qim_internal_rpc_unauthorized_total。
REBUILD — 客户端收到 CURSOR_EXPIRED 后清本地库并全量重拉。归属:qim-sdk::state,事件 LocalStoreReset。
qim-sdk — 一份 Rust 内核 + 三个平行绑定(qim-sdk-ffi C ABI / qim-sdk-uniffi / wasm 未立项)。标识符:Command(Connect / Disconnect / SendMessage / Resend / PullHistory / PullMembers / CreateGroup)、Event(ConnectionChanged / SyncProgress / MessagesAdded / MessageStateChanged / ConversationsUpdated / ReadStateChanged / MembersUpdated / MemberOperationFailed / Kicked / Failed)、SendState(Pending / Committed / Failed)。SQLite 表 local_message / local_pending / local_conversation / local_cursor / local_dedup / local_read / local_client_message_id。约束:绑定层不得反向影响核心(ADR-0010);C QimEvent 是永久 V1 ABI,新字段只经 QimEventV2;进程内去重与 pending 解除只能在 SQLite 原子落库后提交;PUSH_EVENTS 只落库不推进游标,收到后单飞 PULL_MAILBOX;同一批新消息先发 ConversationsUpdated(已含未读)再发 MessagesAdded。
endpoint scope — 客户端本地库路径段:blake3("qim.endpoint-scope.v1\0" || canonical_endpoint)[0..16] 的 32 位十六进制 + 显式 tenant + user。约束:禁 token 入路径;Tauri 跨 JS 边界的 u64/u128 一律字符串。
9. 物化并发与反压(qim-mailbox 内部)¶
DispatchShardWorkers — 每 shard 一条容量 128 的 FIFO + worker:同 Kafka 分区严格串行、不同 shard 并行。标识符:DispatchShardWorkers,QueuedDispatch,TryEnqueue。约束:不是 Kafka consumer;任一 worker 失败立即标记 MailboxDirty、停止中心消费并取消其他 worker。
分区级暂停(队头阻塞/HOL 修复:不再让一个卡住的分片拖着其余 63 个陪等) — 中心消费循环在 shard FIFO 队满时只 pause_shard 该 Kafka 分区并暂存那一条记录,其余分区继续流动;worker 腾位后先入队挂起记录再 resume_shard(顺序不可颠倒)。约束:是隔离不是扩容——过载时 P50 大幅改善但 P99 更差、吞吐略降;不过载时暂停 0 次、完全惰性(机制存在但零开销,一次也不触发);饱和度看 qim_mailbox_dispatch_partition_paused_total{shard}(ADR-0014)。
DISPATCH_INFLIGHT_LIMIT — 全局 durable materialize 并发。标识符:DISPATCH_INFLIGHT_LIMIT_DEFAULT = 64,上限 256,QIM_DISPATCH_INFLIGHT_LIMIT。约束:只限 durable 段;完成 W 后的 Push/Projection 是易失优化。
VolatileDispatch — durable 步骤全部成功后才产生的易失输出(PushBatch + Projection)。约束:任一投递失败都不得回滚 chunk 完成或 W。
materialize 分段指标 — qim_materialize_stage_{normalize,encode_digest,entry_build,cache_read,store_call,cache_commit}_us。约束:总量健康时用于确认,异常时区分「Redis 排队」与「本地 CPU」(处方相反);实测 store_call 占 98.8%。
broker_to_enqueue — qim_mailbox_dispatch_broker_to_enqueue_us{shard}。约束:名不副实——计时起点是消费者已读到 record 之后,不含 broker 滞后;分区暂停落地后 sum/墙钟会超过 100%,不可再用作饱和度。
TakeoverPlan — MailboxNode 接管时从 lane_watermarks(shard) 一次推导 seek 起点并初始化 W cache。约束:启动 catch-up 在 ready 前完整完成,不能先监听再追赶。
FANOUT_BATCH_SIZE — 每轮 fanout 处理的 Outbox 记录数。标识符:DEFAULT_FANOUT_BATCH_SIZE = 512,合法 1..=1024,QIM_FANOUT_BATCH_SIZE。约束:fanout 是串行循环,吞吐 = batch / 固定周期;128 时曾锁死于 9,510/s。
10. 可观测性与门禁¶
trace_id — 由 ConversationWriter 提交时生成,经 Dispatch → PushBatch/Projection 原样透传。归属:qim-common::obs::trace。约束:各服务各自生成等于没有追踪。
恒零指标(ZeroInvariant) — 语义是「停止发布」,/healthz 非零返回 503。标识符:qim_zero_invariant,ZeroInvariant。约束:不允许假阳性;并发路径无法本地判定的不变量移到启动自检。
脱敏 — qim_common::obs::{secret_digest, body_shape, text_shape}。约束:日志禁输出 token/游标签名/密钥/明文正文;check-contract.sh 拦截。
check-contract.sh — CI 契约断言:禁别名、禁 subscribe()、禁累加型未读、禁显式 DELETE、禁重复定义契约常量、常量可追溯、日志脱敏。
acceptance.sh — 九道门禁隔离验收(认证 hybrid 形态):格式/lint/契约 → 测试(含历史分层门禁)→ release + 六服务 → 功能 15 项 → 180 秒到达率/SLO → T-HEAVY → Redis 最终工件 → 不变量证据 → orphan 独立审计。约束:只允许 qim-it-* 隔离项目与精确 PID;orphan 审计的持久事实源未完成前,该步骤必须保持失败(失败闭合),不得放行完整验收。
T-HEAVY — 5000 会话用户的会话列表首屏 P99 ≤ 500ms。约束:会话必须经真实写路径造;client_message_id 每轮不同。
隔离集成环境 — compose.integration.yml + scripts/integration/{up,wait,run,down}.sh,项目名必须 qim-it-*,端口按 offset 0..500 确定性选址(存在 TOCTOU 竞态窗口:检查时空闲、绑定时可能已被别人抢占;Docker 绑定冲突时安全失败退出)。约束:清理只允许 docker compose down --volumes 命中该项目,禁 FLUSHALL / 全局 prune / 按进程名杀宿主服务。QIM_TEST_MAILBOX_REDIS_ADDR 始终指向第二实例。
Redis 安全预检 — 生产仅 7.0.0+ standalone:INFO server 三段式版本、cluster_enabled=0、AOF 开启、appendfsync=everysec|always、maxmemory-policy=noeviction。归属:qim-common::redis_runtime。约束:QIM_REDIS_ALLOW_UNSAFE_DEV=1 只对 loopback 跳过 AOF/fsync/noeviction,绝不绕过版本或 standalone。
审核回调 — QIM_WEBHOOK_FAIL_POLICY(fail-open/closed),MODERATION_SYNC_TIMEOUT_MS = 300。归属:qim-common::webhook。约束:发送后异步通知是唯一允许 try_send 的有界可丢队列,计 qim_webhook_after_dropped_total。
11. 术语层面的已知缺陷¶
- Redis 实例映射
shard % 实例数/bucket % 实例数— 等价于「用物理节点数取模」,违反docs/PLAN.md禁止方案。实例数被约束为桶数约数(4→5 台启动拒绝),翻倍时恰好 50% 重映射,且无迁移工具;ADR-0013/0014 把实例数说成「可调容量参数」的表述不准确。Scylla 线无此问题。正确方向:按稳定实例身份的会合哈希(HRW)+ per-shard 搬迁工具(ADR 待立)。 GroupMembership未分片 — Redis 线固定conns[0]。只影响群会话;Direct 授权纯计算。broker_to_enqueue名不副实 — 见 §9。read_next_raw死代码 —qim-commit-logtrait 方法,注释仍写「中心 consumer 用它把解码推给 worker」,该并行化已回滚。READ_SYNC (0x0406)契约已定、故意不实现 — 一期 user/device 合一,按 uid 顶号(同账号新登录把旧连接踢下线),此帧永远没有接收方;前置条件是多设备。