来自 #125 的架构级评审建议。不阻塞合入,仅供参考是否有更好的架构解法。
⚠️ [重要 · 存储] Flush 失败重试无退避可能引发活锁 storage/zstorage/memtable.go:207
问题根因:Flush() 失败时立即 m.StartFlush() 重试,而 FlushWorker 是单协程顺序消费 FlushChan。如果 WriteToSSTable 持续失败(如磁盘满、权限问题),Flush 会陷入「失败→立即重试→再失败→再重试」的紧密循环,每次都是忙循环(无任何间歇)。这会把整个 FlushWorker 钉死,导致:1) CPU 空转;2) 活跃的写操作被背压永久阻塞(因为信用永远不归还)。
为什么低级解法不够:通用评审者可能建议加 time.Sleep 或指数退避。这是一个合理的 patch,但不够完整——退避期间背压的写者是否该得到明确的错误反馈而非永久阻塞?
架构级方案:引入带退避的重试 + 错误传播给被阻塞的写者:
- 在
MemTable 上维护一个 flushFailCount(atomic),每次失败递增。
Flush 重试前按指数退避等待(如 min(100ms * 2^failCount, 10s))。
- 失败次数超过阈值(如 5 次连续失败)时,通过一个
flushError channel 向所有 acquireCredit 阻塞者广播错误,让它们返回错误给上层,而不是永久阻塞。
- 一次成功刷盘后重置失败计数。
额外好处:背压不再是「无限的、隐式的等待」,而是有界限的、可报告的错误路径。
代价/收益:代价:实现复杂度略增(需要 error channel 或 callback 通知等待者);收益:避免了在持久化路径故障时整个系统 hang 死,提供故障快速失败路径。
storage/zstorage/memtable.go:207问题根因:
Flush()失败时立即m.StartFlush()重试,而FlushWorker是单协程顺序消费FlushChan。如果WriteToSSTable持续失败(如磁盘满、权限问题),Flush会陷入「失败→立即重试→再失败→再重试」的紧密循环,每次都是忙循环(无任何间歇)。这会把整个 FlushWorker 钉死,导致:1) CPU 空转;2) 活跃的写操作被背压永久阻塞(因为信用永远不归还)。为什么低级解法不够:通用评审者可能建议加
time.Sleep或指数退避。这是一个合理的 patch,但不够完整——退避期间背压的写者是否该得到明确的错误反馈而非永久阻塞?架构级方案:引入带退避的重试 + 错误传播给被阻塞的写者:
MemTable上维护一个flushFailCount(atomic),每次失败递增。Flush重试前按指数退避等待(如min(100ms * 2^failCount, 10s))。flushErrorchannel 向所有acquireCredit阻塞者广播错误,让它们返回错误给上层,而不是永久阻塞。额外好处:背压不再是「无限的、隐式的等待」,而是有界限的、可报告的错误路径。
代价/收益:代价:实现复杂度略增(需要 error channel 或 callback 通知等待者);收益:避免了在持久化路径故障时整个系统 hang 死,提供故障快速失败路径。