来自 #153 的架构级评审建议。不阻塞合入,仅供参考是否有更好的架构解法。
⚠️ [重要 · 性能] LoadSSTableMetaList 热路径埋了三个异步 goroutine 启动 storage/zstorage/SSTable.go:108
问题根因:LoadSSTableMetaList 在加载每个 SSTable 时都启动 go meta.EnsureMeta()、go ss.getBlockIndex(...)、go ss.getBloom(...) 三个 goroutine。其中 EnsureMeta 已经被本 PR 的修复绕过了(MaxKeyLoaded=true 的 SSTable 不应执行有效工作),但代码仍然会启动 goroutine。更重要的是:这里由于 MaxKeyLoaded 已置 true,EnsureMeta 内部应该会跳过自身——但每次加载依然会产生 goroutine 调度开销。更严重的是,getBlockIndex 和 getBloom 的异步调用与主流程无协调:后续的读取路径可能在缓存尚未填充时就到达,导致多次并发从磁盘加载同一文件的块索引/布隆——这不是一次而是每次启动都会出现。
为什么低级解法不够:把三个 goroutine 改成同步调用可以消除负载峰值下的 goroutine 风暴,但完全牺牲了启动并行度。热路径(ReadFromSSTable)中对 getBlockIndex/getBloom 有 fallback 加载逻辑,理论上能容忍异步未填满,但会导致大量并发重复读盘。
架构级方案:三层优化:(1) 将 MaxKeyLoaded=true 的 SSTable 完全跳过 EnsureMeta 调用(不仅是内部跳过)。(2) 为 getBlockIndex/getBloom 使用 sync.Once 或 sync.SingleFlight 模式:在加载函数内部做去重,而不是在 getBlockIndex/getBloom 的读路径用 RWMutex 做缓存 miss 时竞争——现在的实现是:每次 miss 都去读盘、然后写锁更新缓存,N 个并发 miss 会导致 N-1 次浪费。(3) 如果预期启动的 SSTable 文件数量大(数百个),考虑用 worker pool 限制并发读盘数,避免同时打开大量文件描述符。
代价/收益:代价:SingleFlight 引入额外依赖或少量抽象;worker pool 需要配置。收益:消除启动时的 goroutine 调度风暴,消除文件 I/O 的重复读盘,降低启动时的内存和 fd 峰值。
storage/zstorage/SSTable.go:108问题根因:
LoadSSTableMetaList在加载每个 SSTable 时都启动go meta.EnsureMeta()、go ss.getBlockIndex(...)、go ss.getBloom(...)三个 goroutine。其中EnsureMeta已经被本 PR 的修复绕过了(MaxKeyLoaded=true的 SSTable 不应执行有效工作),但代码仍然会启动 goroutine。更重要的是:这里由于MaxKeyLoaded已置 true,EnsureMeta内部应该会跳过自身——但每次加载依然会产生 goroutine 调度开销。更严重的是,getBlockIndex和getBloom的异步调用与主流程无协调:后续的读取路径可能在缓存尚未填充时就到达,导致多次并发从磁盘加载同一文件的块索引/布隆——这不是一次而是每次启动都会出现。为什么低级解法不够:把三个 goroutine 改成同步调用可以消除负载峰值下的 goroutine 风暴,但完全牺牲了启动并行度。热路径(
ReadFromSSTable)中对getBlockIndex/getBloom有 fallback 加载逻辑,理论上能容忍异步未填满,但会导致大量并发重复读盘。架构级方案:三层优化:(1) 将
MaxKeyLoaded=true的 SSTable 完全跳过EnsureMeta调用(不仅是内部跳过)。(2) 为getBlockIndex/getBloom使用sync.Once或sync.SingleFlight模式:在加载函数内部做去重,而不是在getBlockIndex/getBloom的读路径用 RWMutex 做缓存 miss 时竞争——现在的实现是:每次 miss 都去读盘、然后写锁更新缓存,N 个并发 miss 会导致 N-1 次浪费。(3) 如果预期启动的 SSTable 文件数量大(数百个),考虑用 worker pool 限制并发读盘数,避免同时打开大量文件描述符。代价/收益:代价:SingleFlight 引入额外依赖或少量抽象;worker pool 需要配置。收益:消除启动时的 goroutine 调度风暴,消除文件 I/O 的重复读盘,降低启动时的内存和 fd 峰值。