来自 #139 的架构级评审建议。不阻塞合入,仅供参考是否有更好的架构解法。
⚠️ [重要 · 性能] ScanRange 每行全量 JSON 反序列化在热路径上 storage/zstorage/memtable.go:163
问题根因:KVServer.Scan (service/fsm.go:177) 在回调中调用 pred.Eval(value),后者对每条命中的 value 执行 json.Unmarshal(value, &obj) 全量反序列化。在扫描窗口大(如最近 10 秒数千帧)且谓词命中率高时,每条都做 JSON 解析,而整个扫描路径已持 MemTable 读锁,JSON 解析延迟会放大锁持有时间,降低写入并发。
为什么低级解法不够:将谓词改为先解析 JSON 再比较、或在回调外批量解析,都不改变「每行都做全量 JSON 反序列化」这一结构性开销。通用评审者可能会建议「缓存解析结果」,但在 MemTable 不做持久化、写入时不对 value 做 JSON 校验的设计下,缓存命中率极低。
架构级方案:在 storage 层引入可选字段索引 / 投影物化:若某设备常按 az 查询,可在写入时预提取 az 字段值存入固定的元数据槽(per-entry 的 sidecar float64),ScanRange 扫描时先比较 sidecar 再决定是否需要完整 JSON 反序列化。或者,在 Predicate.Eval 中采用惰性 JSON 解析:先尝试用 json.RawMessage 扫描目标字段(json.Decoder 的 Token API 可跳过非目标字段)而非 Unmarshal 整个对象,大幅降低 O(n) 开销。
代价/收益:代价:sidecar 方案增加每 entry 约 8 字节(float64)的存储开销 + 写入路径的额外提取逻辑;惰性解析方案引入稍复杂的 JSON 解析代码。收益:扫描热路径上 JSON 开销从 O(fields) 降到 O(1)(sidecar)或降到仅扫描目标字段(惰性 Token API),锁持有时间缩短,写入吞吐不受扫描拖累。
storage/zstorage/memtable.go:163问题根因:KVServer.Scan (service/fsm.go:177) 在回调中调用
pred.Eval(value),后者对每条命中的 value 执行json.Unmarshal(value, &obj)全量反序列化。在扫描窗口大(如最近 10 秒数千帧)且谓词命中率高时,每条都做 JSON 解析,而整个扫描路径已持 MemTable 读锁,JSON 解析延迟会放大锁持有时间,降低写入并发。为什么低级解法不够:将谓词改为先解析 JSON 再比较、或在回调外批量解析,都不改变「每行都做全量 JSON 反序列化」这一结构性开销。通用评审者可能会建议「缓存解析结果」,但在 MemTable 不做持久化、写入时不对 value 做 JSON 校验的设计下,缓存命中率极低。
架构级方案:在 storage 层引入可选字段索引 / 投影物化:若某设备常按 az 查询,可在写入时预提取
az字段值存入固定的元数据槽(per-entry 的 sidecar float64),ScanRange扫描时先比较 sidecar 再决定是否需要完整 JSON 反序列化。或者,在Predicate.Eval中采用惰性 JSON 解析:先尝试用json.RawMessage扫描目标字段(json.Decoder的 Token API 可跳过非目标字段)而非Unmarshal整个对象,大幅降低 O(n) 开销。代价/收益:代价:sidecar 方案增加每 entry 约 8 字节(float64)的存储开销 + 写入路径的额外提取逻辑;惰性解析方案引入稍复杂的 JSON 解析代码。收益:扫描热路径上 JSON 开销从 O(fields) 降到 O(1)(sidecar)或降到仅扫描目标字段(惰性 Token API),锁持有时间缩短,写入吞吐不受扫描拖累。