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_previousCAS 连续时,才把 W 推进到该 record 的observed_log_offset;失败或缺口不推进,W 永不跨越未完整物化的 record; HLEN == 3的严格字段数校验不变;- 空 lane 仍必须全零填充,presence 只能是 0 或 1,
非空 lane 仍要求
W == W_recomputed >= W_floor。
压缩只影响存储表示,不影响任何水位语义。