来自 #125 的架构级评审建议。不阻塞合入,仅供参考是否有更好的架构解法。
💡 [建议 · 性能] Release 对 cond.Broadcast 的唤醒风暴 pkg/credit/credit.go:31
问题根因:Release 每次都调用 cond.Broadcast(),而 Acquire 用 Wait() 等待。在高并发写压下,每次 Flush 完成 Release 后会唤醒所有等待的 goroutine(thundering herd),所有被唤醒者都会竞争锁,最终只有一个能成功 Acquire,其余重新 Wait——这引入了不必要的上下文切换和锁竞争。
为什么低级解法不够:通用评审者会建议换 Signal 而不是 Broadcast,但 Signal 只唤醒一个等待者,如果释放的信用足够让多个等待者通过,那一次 Signal 后只会有一个被唤醒,其他仍阻塞直到下次 Release——增加了延迟。
架构级方案:最佳方案:改用 sync.Cond 的语义+信用许可计数,改为类似 semaphore 的实现:
在 Release 中计算「本次 Release 能让多少个等待者解除阻塞」,然后精确唤醒那个数量:
func (p *Pool) Release(n int64) {
p.mu.Lock()
p.used -= n
if p.used < 0 {
p.used = 0
}
// 没有 cond.Broadcast,而是根据释放量精确唤醒
// 但 Sync.Cond 不支持选择性唤醒,所以简化为:
// 如果 used 从高位降到了可用水位,Signal 一次即可
p.cond.Signal() // Signal 代替 Broadcast 已能大大缓解
p.mu.Unlock()
}
但更好的架构方案是换用 channel-based semaphore(如带 struct{} 的 chan):每个 Acquire 是发送一次令牌,每个 Release 是消费令牌。核心改动是将 sync.Cond 替换为带有 count 的信号量实现。成本较大,当前用 Broadcast 是安全的,性能问题在非极端竞争下可接受。建议改为 Signal——Acquire 阻塞者被唤醒后检查 fits,仍然会循环不满足则 re-Wait,所以 Signal 比 Broadcast 更高效且语义正确。
代价/收益:用 Signal 代替 Broadcast:代价极低(改一行),在每次只释放少量信用且只有一个等待者能解除阻塞时效率更高。在释放大量信用、多个等待者可同时解除的场景下,Signal 会导致需要更多的 Release 次数来逐个唤醒——但 Flush 完成时的 Release 通常是释放整个 dirty 表的全部信用(大量),此时一次唤醒一个可能需要多次 Release 才能解除所有等待者。更精细的做法是保持 Broadcast 但通过分层(per-shard Pool)减少竞争。
💡 [建议 · 性能] Release 对 cond.Broadcast 的唤醒风暴
pkg/credit/credit.go:31问题根因:
Release每次都调用cond.Broadcast(),而Acquire用Wait()等待。在高并发写压下,每次 Flush 完成 Release 后会唤醒所有等待的 goroutine(thundering herd),所有被唤醒者都会竞争锁,最终只有一个能成功 Acquire,其余重新 Wait——这引入了不必要的上下文切换和锁竞争。为什么低级解法不够:通用评审者会建议换
Signal而不是Broadcast,但Signal只唤醒一个等待者,如果释放的信用足够让多个等待者通过,那一次Signal后只会有一个被唤醒,其他仍阻塞直到下次 Release——增加了延迟。架构级方案:最佳方案:改用
sync.Cond的语义+信用许可计数,改为类似 semaphore 的实现:在
Release中计算「本次 Release 能让多少个等待者解除阻塞」,然后精确唤醒那个数量:但更好的架构方案是换用 channel-based semaphore(如带
struct{}的chan):每个Acquire是发送一次令牌,每个Release是消费令牌。核心改动是将sync.Cond替换为带有 count 的信号量实现。成本较大,当前用Broadcast是安全的,性能问题在非极端竞争下可接受。建议改为Signal——Acquire阻塞者被唤醒后检查fits,仍然会循环不满足则 re-Wait,所以Signal比Broadcast更高效且语义正确。代价/收益:用
Signal代替Broadcast:代价极低(改一行),在每次只释放少量信用且只有一个等待者能解除阻塞时效率更高。在释放大量信用、多个等待者可同时解除的场景下,Signal会导致需要更多的 Release 次数来逐个唤醒——但 Flush 完成时的 Release 通常是释放整个 dirty 表的全部信用(大量),此时一次唤醒一个可能需要多次 Release 才能解除所有等待者。更精细的做法是保持Broadcast但通过分层(per-shard Pool)减少竞争。