代码
https://blog.csdn.net/jfwan/article/details/109328874
原理与代码分析
核心参数
Total – 向下游发送的总请求量
Accept – 下游能正常返回的请求量
在算法设计中,Total和Accept之间存在一个倍数,当total大于Accept乘以这个倍数,则表示下游服务出现异常,熔断功能打开。否则会将请求直接发往下游服务。这个倍数就是下面结构体中的参数k。
type googleBreaker struct {
k float64
stat *collection.RollingWindow
proba *mathx.Proba
}
如何判断是否往下游发放请求量由下面函数实现
func (b *googleBreaker) accept() error {
accepts, total := b.history()
weightedAccepts := b.k * float64(accepts)
// https://landing.google.com/sre/sre-book/chapters/handling-overload/#eq2101
dropRatio := math.Max(0, (float64(total-protection)-weightedAccepts)/float64(total+1))
if dropRatio <= 0 {
return nil
}
if b.proba.TrueOnProba(dropRatio) {
return ErrServiceUnavailable
}
return nil
}
Accepts和total分别是窗口期内正常返回的请求量和总请求量。Protection是一个常量值,它的作用是当窗口期内的请求数量较少时,我们默认不打开熔断器(因为对下游的压力很小),同时也为后续的流量熔断判断作为依据。当weightedAccepts远小于total时(下游请求大量异常的情况),dropRatio的值接近1;当weightedAccepts接近甚至大于total时,dropRatio的值接近0。我们再来看b.proba.TrueOnProba的实现
func (p *Proba) TrueOnProba(proba float64) (truth bool) {
p.lock.Lock()
truth = p.r.Float64() < proba
p.lock.Unlock()
return
}
p.r.Float64()计算结果是0到1的一个随机值,因此dropRatio的值接近1时请求大概率会被熔断器给拦截掉。
我们再来看一下accepts和total是如何计算出来的。它的核心思想就是将时间窗口分为若干个小的窗口,将每个小窗口的accepts和total取和。比如源代码实现的时间窗口为10s,细分了40份小的窗口,每个小窗口的时间跨度为250ms。我们统计最近10s内的下游响应情况,以此为依据判断是否打开熔断器,这比较好理解。那为何要划分40个窗口呢?原因是作者希望找到最近10s内的值,因此只用选取从当前时间点开始倒推40个小窗口值的和,细分的作用就是模拟取值窗口的平滑移动。这部分的细节代码在rollingwindow.go文件中。
判断熔断器是否允许向下游发送请求,由下面的函数进行实现
func (b *googleBreaker) allow() (internalPromise, error) {
if err := b.accept(); err != nil {
return nil, err
}
return googlePromise{
b: b,
}, nil
}
它返回了一个Promise结构的参数,这个结构有两个默认的方法
Promise interface {
// Accept tells the Breaker that the call is successful.
Accept()
// Reject tells the Breaker that the call is failed.
Reject(reason string)
}
当本次下游返回成功后会调用Accept方法,否则会调用Reject方法。这两个方法的调用会加入细分窗口的accepts和total的值,为之后熔断器判断作运算基础。