Skip to content

🐯 [性能] Release 对 cond.Broadcast 的唤醒风暴 · credit.go #129

@github-actions

Description

@github-actions

来自 #125 的架构级评审建议。不阻塞合入,仅供参考是否有更好的架构解法。

💡 [建议 · 性能] Release 对 cond.Broadcast 的唤醒风暴 pkg/credit/credit.go:31

问题根因Release 每次都调用 cond.Broadcast(),而 AcquireWait() 等待。在高并发写压下,每次 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,所以 SignalBroadcast 更高效且语义正确。

代价/收益:用 Signal 代替 Broadcast:代价极低(改一行),在每次只释放少量信用且只有一个等待者能解除阻塞时效率更高。在释放大量信用、多个等待者可同时解除的场景下,Signal 会导致需要更多的 Release 次数来逐个唤醒——但 Flush 完成时的 Release 通常是释放整个 dirty 表的全部信用(大量),此时一次唤醒一个可能需要多次 Release 才能解除所有等待者。更精细的做法是保持 Broadcast 但通过分层(per-shard Pool)减少竞争。

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions