07 可靠性、安全与运维¶
状态:可实施;以下 fencing 的实现缺口为发布阻断 更新日期:2026-09-01 上游契约:
docs/PLAN.md§8、§19、§20、§24 实施决策:ADR-0006(Rust + Redpanda,受 ADR-0012 覆盖)、ADR-0007(一期简化形态)
0. 文档边界¶
本文定义:fencing 实现、检查点参数、鉴权与令牌生命周期、限流实现、 运维手册与告警响应。
本文不得重新定义:错误码含义(附录 A.6)、一致性边界(§19.1)、 到达层级(§11.2)、SLO 目标值(§2.4)。
1. fencing 实现(§19.2)¶
1.1 两个校验点,缺一不可¶
前置约束 Writer 自我 fencing:续租超过 self_fence_deadline 后停止写入;
它只缩小故障窗口,不能替代下列两个校验点。
校验点 1 控制面:ConversationHead 的条件更新
IF fencing_epoch < ? OR (fencing_epoch = ? AND head_version < ?)
校验点 2 分发数据面:GroupDispatch.fencing_epoch 的消费侧单调过滤
MailboxNode 按 (tenant, conversation) 维护已见最大 fencing_epoch
小于该值的记录整条丢弃,计入 stale_epoch_dispatch_dropped_total
当前发布阻断:目标 GroupDispatch 必须携带并逐字节编解码 fencing_epoch,但当前
qim_common::commit::GroupDispatch 没有该字段,MailboxNode 亦未实现对应过滤。此处保留目标
要求并将缺口暴露为发布阻断;不得删除校验点 2、把它降级为内存 best-effort,或用 broker
producer fencing 替代。
1.2 Fanout 日志语义与实现漂移¶
ADR-0012 已取消 FanoutCoordinator 的 Kafka 事务。日志只承担可重放、幂等生产及 durable delivery; 它不承担 stale-owner fencing。正确顺序是:
产出本批全部 GroupDispatch,并等待全部 durable delivery(acks=all)
-> 才持久化/确认该批 Outbox source 进度
第 1 步成功、第 2 步失败 -> 重放;由 dispatch_id + MailboxStore 持久去重收敛。
禁止 source 进度先于 dispatch durable 被确认,否则会永久跳过消息。
transactional_id、InitProducerId 和 ProducerFenced 均不是本设计的 fencing 前提;不得把
fencing_epoch 编入任何 broker producer 标识来试图代替 GroupDispatch 过滤。
当前命名/配置漂移:RdkafkaCommitLog::publish_dispatches_and_ack_batch 已落实“全部 dispatch
durable 后才 Sync commit source”的 ADR-0012 顺序。代码中的 TransactionalFanout /
FanoutBatchTransaction / metrics 是历史名称;QIM_FANOUT_TRANSACTIONAL_ID 虽未参与该语义仍被
强制读取。发布前必须移除无用 env 门禁并清理名称,但不得把它们误判为仍在运行 Kafka transaction;
本节的故障注入证明必须持续保留。
1.3 参数与不等式¶
shard_lease_ttl 15 s
lease_renew_interval 5 s 失败后转 1 s 快速重试直至 self_fence_deadline
writer_self_fence_deadline 12 s
writer_failover_wait 25 s
必须成立:writer_failover_wait > shard_lease_ttl > writer_self_fence_deadline
接管必须等租约自然过期,不能靠心跳判定。SIGSTOP、cgroup CPU 限流、 VM 挂起都会造成秒级不响应而进程仍活着;Rust 无 GC 不改变这一点。
2. 检查点与恢复(§19.3)¶
2.1 一期的检查点内容(ADR-0007 简化)¶
一期(Redis 7.0.0+ standalone,AOF + noeviction)与阶段一(ScyllaDB):权威数据在节点外
检查点只需 W[lane] + W_floor[lane] + DispatchProgress 快照
不做邮箱索引检查点
邮箱由 Redpanda 分发日志物化:日桶保留 30 天;DispatchProgress 默认不 GC,
只有部署方显式注入经审计的日志 replay safety window,并证明全部 lane 的
W/W_floor 越过最大 observed offset 且最后一次重复记录已超过该窗口后才删除。
writer→mailbox TCP dispatch 不属于当前链路。
阶段二(自研 LSM):才需要完整索引检查点到对象存储
内容 = 邮箱索引 + 投影 + UserBadgeState + shard_epoch + log_offset
+ 各 lane 水位 + EpochBoundary / ShardSplitBoundary
2.2 恢复时间不等式¶
T_recover = 检查点加载时间 + 重放追平时间
= checkpoint_load_time_budget + checkpoint_interval × 分片事件速率 / replay_rate
log_retention_days >= (checkpoint_interval + T_recover) × 2
×2 覆盖:重放期间日志仍在追加、恢复本身失败并需重来一次
目标:checkpoint_interval 15 min,shard_rto_target 10 min
由 §26 用例 26.6.3 的实测保证,不是推导结果
2.3 备份与时间点恢复¶
全量快照 + 增量 -> 独立 bucket(WORM)
RPO <= 15 min
每季度恢复演练,演练结果回填 checkpoint_load_time_budget(附录 B.7)
3. 鉴权与令牌¶
3.1 三种令牌¶
| 令牌 | 签发方 | 生命周期 | 校验点 |
|---|---|---|---|
access_token |
Auth/Session | 1 h | 每次 AUTH |
route_token(REDIRECT) |
Auth/Session | ≤ 60 s,单次使用 | 重定向落点 |
游标令牌(MailboxCursor) |
ConnectionNode | 随分片归属 | 每次 PULL_MAILBOX |
3.2 游标令牌:签名部分与明文部分(§6.8)¶
令牌承载不可伪造部分(HMAC 签名):
mailbox_shard_id、lane_id、shard_epoch、user_id、device_id、签发时间
last_applied_mailbox_seq 在帧中**明文传输**:
服务端校验 acked_seq <= 该用户 lane 当前 W[lane]
越界返回 ERROR{CURSOR_INVALID}
这样客户端可以本地推进游标而不必每次换发令牌,同时无法越权读取其他分片。
3.3 长连接内的令牌续期(§15.3)¶
连接可能活 7 天而 access_token 只有 1 h:
客户端在过期前用 refresh_token 换新,经控制帧刷新,**不断连**
刷新失败 -> ERROR{TOKEN_EXPIRED} -> 客户端静默重连
吊销(远程登出 / 封禁 / 设备丢失):
吊销事件经 PresenceDirectory 的 compacted topic 传播
ConnectionNode 收到后立即发 KICKED{token_revoked} 并关闭连接
同步删除该设备的 device_token 与 DevicePreKeyBundle
4. 限流实现(§8.2、§20.4)¶
4.1 三层准入¶
L1 发送者维度 per_sender_in_conversation_rate(1 msg/3s)
ConnectionNode 本地令牌桶 + ConversationWriter 复核
L2 会话维度 per_conversation_msg_rate(按 N 分档 20/5/2 msg/s)
L3 租户维度 tenant_fanout_quota(entry/s 令牌桶)
超限 -> ERROR{RATE_LIMITED} 或 FANOUT_QUOTA_EXCEEDED
**行为是发送侧拒绝或排队,绝不静默丢邮箱引用**
4.2 执行位置的确定性¶
用户与会话都已固定到分片(§5.2),因此每用户 / 每会话令牌桶可以
**在单节点内存中维护**,不需要跨节点一致的分布式限流器,
也不需要把 Redis 放进每条消息路径(§3 禁令 9)。
每租户 fanout 配额是全局量:中心配额分片下发 + 本地令牌桶,
中心每 quota_refill_interval 按节点在途负载重新分配份额。
4.3 其他限流维度(§20.4)¶
per_ip_connect_rate L4 / ConnectionNode,直接拒握手不返回应用层帧(避免放大)
unauth_connection_timeout 10 s,slowloris 防护
takeover_admit_rate 5%/s,接管分批放行
per_user_reaction_rate 5/s,与消息速率独立计量
history_pull_rate 每用户历史拉取速率
5. 多租户隔离(§20.5)¶
隔离档位
共享集群 逻辑隔离,tenant_id 贯穿全部主键与缓存键
独立分片 物理隔离到指定 MailboxShard 与 ConnectionShard(超大租户)
独立集群 私有化部署
噪声邻居防护
§5.2 的均匀打散 + 分片级连续水位曾构成"单租户慢任务阻塞同分片其他租户"的
级联路径 —— §6.5.1 的 lane 向量水位把阻塞域从整分片缩小到 1/64
超大租户应启用"独立分片"档位彻底隔离
缓存键必须包含 tenant_id(防跨租户命中)
数据驻留:region 绑定,跨境传输受 §21.5 约束
6. 可观测性¶
6.1 恒零指标(非零即 P1)¶
mailbox_seq_regression_count mailbox_seq 回退
cursor_advanced_by_push_total 游标被 PUSH_EVENTS 越位推进(违反 §6.8)
event_id_nondeterminism_total 重放/主备物化结果比对不一致
孤儿 dispatch 计数 有 MessageRecord 无 dispatch,或反之
covered_through_seq 跨越未物化区间 客户端上报
这五个指标是正确性的最后防线。它们非零不是"性能退化", 是"某条不变量已被破坏",必须停止发布并定位。
6.2 关键 SLI 与告警¶
| 指标 | 目标 | 告警 |
|---|---|---|
online_ready_latency_p99{backlog} |
无积压 ≤ 2 s;1 万条 ≤ 10 s | 超目标 5 min → P2;超 2× → P1 |
send_ack_latency_p99 |
≤ 150 ms(同区) | > 500 ms → P1 |
lane_watermark_stall_ms{shard,lane} |
< lane_stall_alert(30 s) |
> 30 s → P2;> 120 s → 自动接管 |
projection_lag_seq |
p99 < 5 min | > 保留窗口 1/3 → P1 |
unread_drift_ratio |
≈ 0 | > 0.1% → P3 |
fanout_entries_per_sec{tenant,shard} |
≤ 预算 50% | > 70% → P2;> 90% → P1 |
mailbox_store_shadow_mismatch |
0 | > 0 → P1 且阻断切换 |
device_token_digest_lag |
< 1 min | > 5 min → P3 |
conversation_active_member_ratio{size_bucket} |
无目标值 | 无告警(是 §10.2.3 判据的输入量) |
6.3 追踪标识¶
贯穿提交、分发、邮箱、推送日志:
trace_id, tenant_id, message_id/event_id, conversation_id, dispatch_id,
mailbox_shard, lane_id, mailbox_seq, connection_shard,
connection_node_id, mailbox_node_id, shard_epoch, membership_version, device_id
日志与追踪禁止输出:access_token、refresh_token、游标签名、
DEK 与任何密钥、明文消息正文。
7. 告警响应手册¶
7.1 mailbox_seq_regression_count > 0(P1)¶
含义:某分片的 mailbox_seq 出现回退 —— 复合序号的单调性被破坏
可能原因:shard_epoch 未在日志重建/跨集群切换时递增
立即动作:
1. 停止该分片的一切写入(qimctl shard drain)
2. 比对 EpochBoundary 与实际 log_offset
3. **不要**试图"修正"已发放的游标 —— 递增 shard_epoch 后走 CURSOR_REBASED
禁止:降低水位或重置游标,那会静默丢消息
7.2 lane_watermark_stall_ms > lane_stall_failover(P2→自动接管)¶
含义:某 lane 的子任务卡住,该 lane 用户的可见性冻结
排查顺序:
1. DispatchProgress 中该 lane 是否有未完成块
2. 该块对应的 dispatch 是否在慢速重试队列
3. MailboxStore 是否有写入错误(standalone Redis AOF/内存策略异常 / Scylla 超时)
自动动作:lane_stall_failover(120s) 触发租约漂移接管
人工兜底:若接管后仍停滞,说明是数据问题而非节点问题,转 7.1 流程
7.3 projection_lag_seq 逼近 dispatch recovery window(P1)¶
含义:qsession durable checkpoint 滞后逼近 dispatch 日志恢复窗口——再滞后将无法靠日志重放投影
立即动作:
1. 扩容 qsession 消费/Redis 写入能力并定位慢 partition
2. 暂停尽力低延迟 Projection 补充,把资源让给 durable dispatch catch-up
3. 若仍不收敛,延长 dispatch retention(治标)并准备 UserConversationState 权威重建
禁止:checkpoint 落到 broker low watermark 之前后仍以残缺 Redis 投影对外 ready
7.4 mailbox_store_shadow_mismatch > 0(P1)¶
含义:两套 MailboxStore 实现对空洞、边界或幂等的理解不一致
立即动作:阻断切换、回切到原实现、保留差异样本
这是**实现缺陷**而非环境问题,必须定位到具体条目差异
8. 混沌场景清单(§26.8)¶
节点 kill -9(gateway / writer / mailbox 各一轮)
SIGSTOP 30 秒后 SIGCONT(模拟运行时停顿、cgroup 限流、VM 挂起)
网络分区(region 间、节点与 etcd 间、节点与 Redpanda 间)
磁盘满(Redis / Scylla)
时钟回拨(超过 clock_regression_reject_ms 与未超过各一次)
日志分区不可用 10 min
登录风暴(百万连接 60 秒内重连)
大群消息风暴 / 聊天室洪峰
Redis Cluster 三节点配置启动拒绝(相关服务不得 ready)
每个场景的通过判据统一为:§6.1 的五个恒零指标保持为零, 且持久消息丢失数 == 0(以邮箱层三元组集合比对)。
9. 验收¶
R-1【发布阻断】fencing 两个校验点
SIGSTOP 旧 Writer 30 s 后恢复:ConversationHead 条件写 100% 被拒。
新 epoch dispatch 已消费后注入旧 epoch dispatch:MailboxNode 100% 拒收、
新增 UserMailboxEntry == 0、stale_epoch_dispatch_dropped_total 增量 == 注入数。
GroupDispatch 字段缺失、编解码未保留 epoch、或关闭任一校验点时,本用例必须失败。
R-2【发布阻断】Fanout 无事务提交顺序
断言 RdkafkaCommitLog 先完成全部 dispatch durable,再 Sync commit source offset;
启动/运行不得要求 QIM_FANOUT_TRANSACTIONAL_ID(无该 env 时正常通过 readiness 与 fanout)。
在“全部 dispatch durable、source checkpoint 前”与“部分 dispatch 失败”两点分别 kill -9:
前者恢复后允许重复但不得缺失;后者 source checkpoint 前进次数 == 0。
R-3 令牌续期不断连
连接保持 2 小时(access_token 1 h):刷新成功且断连次数 == 0
R-4 吊销即时生效
远程登出:目标设备在 1 分钟内收到 KICKED{token_revoked} 并断开
该设备后续 AUTH 一律失败
R-5 限流不静默丢消息
注入超 tenant_fanout_quota 的负载:
返回 FANOUT_QUOTA_EXCEEDED 的次数 > 0
静默丢弃邮箱引用次数 == 0
R-6 恢复演练
从检查点 + 日志恢复单分片:RTO <= shard_rto_target(10 min)
恢复后五个恒零指标为零,邮箱三元组集合与故障前一致
10. 待办¶
[ ] quota_refill_interval 的默认值与中心配额分配算法
[ ] history_pull_rate 的默认值(本文引用,附录 B 尚未收录)
[ ] APNs JWT 与 FCM OAuth 凭证轮换流程
[ ] qimctl 危险操作的二人复核实现