跳转至

ADR-0015:packed watermark v2——全同 lane 压缩存储

  • 状态:已接受
  • 日期:2026-08-26
  • 相关:ADR-0011(邮箱热路径表示)、ADR-0014(实例数是容量参数)

背景

ADR-0014 用 perf 对 mailbox Redis 主进程采样(10,000 msg/s、31,486 个采样、 符号已解析)后确认:Lua 解释器占 Redis CPU 的 34.50%,其中 luaS_newlstr(Lua 建串 + 驻留哈希)单项 6.59%,是 Lua 内部最大的 可优化项。

它的来源是确定的。wm:{shard} 的 wm_blob 固定 1216 字节 (64 lane × 19 字节)。同一 partition/shard 的 record 顺序物化,且 record 的 全部 lane/chunk/entries durable、各 lane 的 expected_previous CAS 连续后,64 lane 才推进到该 record 的 observed_log_offset。稳态下这使 64 lane 逐字节相同;未完成 chunk、CAS 缺口或物化失败时则分叉或不推进。为了利用这个性质,ADR-0011 引入了运行时判定:

local function uniform_blob(blob)
    return string.sub(blob, LANE_BYTES + 1) == string.sub(blob, 1, -(LANE_BYTES + 1))
end

这已经是同类写法里最快的(对照实测:两次 sub 比较 5.48µs, rep 版 11.77µs,逐字节版 203µs),但它仍要构造两个约 1197 字节的新串, 每个都经 luaS_newlstr 建串并驻留。写回时的 string.rep(advanced_record, 64) 再造一个 1216 字节串, 最后 HSET 写出 1216 字节。

存储格式在为一个稳态下永远不需要的自由度付费。

决策

引入 packed-v2:wm_blob 的长度本身编码「是否全同」。

wm:{shard} 仍恰好 3 个字段:

字段 值
wm_layout packed-v2
wm_epoch 十进制 epoch(不变)
wm_blob 19 字节(全同:64 个 lane 均等于该条记录)或 1216 字节(分叉:逐 lane)

合法长度只有 {19, 1216} 两个值,其余一律判损坏并失败闭合。

由此:

  • uniform_blob(blob) 退化为 #blob == LANE_BYTES——O(1),零建串;
  • 全同时写回 19 字节而非 1216 字节;
  • string.rep(record, 64) 在全同路径上完全消失。

预估收益(基于 ADR-0014 的隔离微基准,单次脚本 227µs): uniform_blob 两处调用点约 10.4µs + string.rep 7.6µs + 写入体量差 22.7µs ≈ 40µs,约 18%。

实测(10,000 msg/s、1,000 客户端、60 秒、4 个 mailbox Redis 实例)

指标 v1 + 写侧削减 packed-v2
evalsha 225.35µs 186.51µs(−17.2%)
mailbox Redis CPU 99.7% 85.8%
端到端 P50 91.3ms 78.8ms
端到端 P99 10,657.4ms 131.8ms
SEND_ACK P99 31.4ms 45.6ms
窗口内投递 99.3% 100%

预估 18%、实测 17.2%。这是本轮唯一一次事前预估与实测吻合的优化—— ADR-0014 记录的四次失败推断都发生在用聚合指标做减法时, 而本次的依据是 perf 直接采样加上隔离微基准的单项计时。

容量含义:4 个实例的 mailbox Redis CPU 从 99.7% 降到 85.8%, 脱离 ρ≈1。同一速率此前需要 8 个实例才能让端到端 P99 进入百毫秒级 (ADR-0014:8+4 为 113.2ms),现在 4 个实例即可(131.8ms)。

压缩后的容量阶梯(同机、1,000 客户端、60 秒):

mailbox + core 实例 速率 结果
4 + 4 10,000 端到端 P99 131.8ms、100% 投递、mailbox CPU 85.8%
4 + 8 12,000 mailbox CPU 99.6% 饱和,投递 94.8%、P99 19,085ms
8 + 8 15,000 committed 12,989/s(提交侧封顶),mailbox CPU 仅 67.3%

即:4 个 mailbox 实例可支撑 10,000 msg/s,但不足以支撑 12,000; 而 8 个实例下 mailbox 侧已有明显余量,约束完全转移到提交侧 (core Redis)。ADR-0014 记录的「实例数按 ρ ≤ 0.65 反推」方法不变, 只是同一 ρ 现在对应更高的速率。

仍为 60 秒短压,不构成容量结论;发布判据不变。

兼容与迁移

新代码读旧数据:接受 packed-v1(1216 字节),并在下一次写入时 自然转换为 packed-v2。不需要单独的迁移作业。

旧代码读新数据:两条独立的失败闭合路径—— ① wm_layout 检查为 ~= 'packed-v1' 即返回损坏; ② #blob ~= 1216 即返回损坏。 任何情况下都不会静默误读,这是本设计可接受的前提。

因此不支持同一 shard 内的滚动升级:一旦新 owner 写出 v2, 旧 owner 立即失败闭合。升级必须先 fence 该 shard 的旧 owner(停掉持有该 shard 的 mailbox 实例)再启动新实例。mailbox 按静态分区指派、每 shard 单一 owner,这本就是既有的部署模型,与 legacy 192-field → packed-v1 的迁移纪律一致。

降级:任何 v2 写入之后回退到旧代码,需要离线把 19 字节展开为 1216 字节 并把 wm_layout 改回 packed-v1。不提供在线降级。

影响面

Lua(5 个脚本,全部需同步改):

  • materialize_fresh_single_recipient.lua(热路径,读 + 写)
  • advance_log_watermark.lua(读 + 写)
  • advance_log_watermarks.lua(读 + 写)
  • read_log_watermarks.lua(只读)
  • gc_completed_dispatch.lua(只读)

Rust:redis/watermark.rs::decode_lane_watermarks 需接受两种长度; 调用点只有 redis/store.rs 一处。

不变的部分

  • lane 记录本身仍是 19 字节 presence + W + W_floor + W_recomputed, offset 为 6 字节 big-endian u48,与 MailboxSeq 低 48 位一致;
  • 连续水位语义不变:对同一 dispatch partition/shard 顺序消费,只有 record 的全部 lane/chunk/entries durable 且 expected_previous CAS 连续时,才把 W 推进到该 record 的 observed_log_offset;失败或缺口不推进,W 永不跨越未完整物化的 record;
  • HLEN == 3 的严格字段数校验不变;
  • 空 lane 仍必须全零填充,presence 只能是 0 或 1, 非空 lane 仍要求 W == W_recomputed >= W_floor。

压缩只影响存储表示,不影响任何水位语义。