前言
go-zero 群里常常有同学问:
服务监控是通过什么算法实现的?
滑动窗口是怎么工作的?是否讲讲这块的原理?
熔断算法是怎么设计的?为啥没有半开半闭状态呢?
本篇文章,来剖析一下 go-zero
中指标统计背地的实现算法和逻辑。
指标怎么统计
这个咱们间接看 breaker
:
type googleBreaker struct {
k float64
stat *collection.RollingWindow
proba *mathx.Proba
}
go-zero
中默认的breaker
是以 google SRE 做为实现底本。
当 breaker
在拦挡申请过程中,会记录以后这类申请的胜利 / 失败率:
func (b *googleBreaker) doReq(req func() error, fallback func(err error) error, acceptable Acceptable) error {
...
// 执行理论申请函数
err := req()
if acceptable(err) {// 理论执行:b.stat.Add(1)
// 也就是说:外部指标统计胜利 +1
b.markSuccess()} else {
// 原理同上
b.markFailure()}
return err
}
所以其实底层说白了就是:申请执行结束,会依据谬误产生次数,外部的统计数据结构会相应地加上统计值(可正可负)。同时随着工夫迁徙,统计值也须要随工夫进化。
简略来说:工夫序列内存数据库【也没数据库这么猛,就是一个存储,只是一个内存版的】
上面就来说说这个工夫序列用什么数据结构组织的。
滑动窗口
咱们来看看 rollingwindow
定义数据结构:
type RollingWindow struct {
lock sync.RWMutex
size int
win *window
interval time.Duration
offset int
ignoreCurrent bool
lastTime time.Duration
}
上述构造定义中,window
就存储指标记录属性。
在一个 rollingwindow
蕴含若干个桶(这个看开发者本人定义):
每一个桶存储了:Sum
胜利总数,Count
申请总数。所以在最初 breaker
做计算的时候,会将 Sum 累计加和为 accepts
,Count 累计加和为 total
,从而能够统计出以后的错误率。
滑动是怎么产生的
首先对于 breaker
它是须要统计单位工夫(比方 1s)内的申请状态,对应到下面的 bucket
咱们只须要将单位工夫的指标数据记录在这个 bucket
即可。
那咱们怎么保障在工夫后退过程中,指定的 Bucket
存储的就是单位工夫内的数据?
第一个想到的形式:后盾开一个定时器,每隔单位工夫就创立一个 bucket
,而后当申请时以后的工夫戳落在 bucket
中,记录以后的申请状态。周期性创立桶会存在临界条件,数据来了,桶还没建好的矛盾。
第二个形式是:惰性创立 bucket
,当遇到一个数据再去查看并创立 bucket
。这样就有时有桶有时没桶,而且会大量创立 bucket
,咱们是否能够复用呢?
go-zero 的形式是:rollingwindow
间接事后创立,申请的以后工夫通过一个算法确定到bucket
,并记录申请状态。
上面看看 breaker
调用 b.stat.Add(1)
的过程:
func (rw *RollingWindow) Add(v float64) {rw.lock.Lock()
defer rw.lock.Unlock()
// 滑动的动作产生在此
rw.updateOffset()
rw.win.add(rw.offset, v)
}
func (rw *RollingWindow) updateOffset() {span := rw.span()
if span <= 0 {return}
offset := rw.offset
// 重置过期的 bucket
for i := 0; i < span; i++ {rw.win.resetBucket((offset + i + 1) % rw.size)
}
rw.offset = (offset + span) % rw.size
now := timex.Now()
// 更新工夫
rw.lastTime = now - (now-rw.lastTime)%rw.interval
}
func (w *window) add(offset int, v float64) {
// 往执行的 bucket 退出指定的指标
w.buckets[offset%w.size].add(v)
}
上图就是在 Add(delta)
过程中产生的 bucket
产生的窗口变动。解释一下:
-
updateOffset
就是做bucket
更新,以及确定以后工夫落在哪个bucket
上【超过桶个数间接返回桶个数】,将其之前的bucket
重置- 确定以后工夫绝对于
bucket interval
的跨度【超过桶个数间接返回桶个数】 - 将跨度内的
bucket
都清空数据。reset
- 更新
offset
,也是行将要写入数据的bucket
- 更新执行工夫
lastTime
,也给下一次挪动做一个标记
- 确定以后工夫绝对于
- 由上一次更新的
offset
,向对应的bucket
写入数据
而在这个过程中,如何确定确定 bucket
过期点,以及更新工夫。滑动窗口最重要的就是工夫更新,上面用图来解释这个过程:
而 bucket
过期点,说白就是 lastTime
即上一个更新工夫逾越了几个 bucket
:timex.Since(rw.lastTime) / rw.interval
这样,在 Add()
的过程中,通过 lastTime
和 nowTime
的标注,通过一直重置来实现窗口滑动,新的数据一直补上,从而实现窗口计算。
总结
本文剖析了 go-zero
框架中的指标统计的根底封装、滑动窗口的实现 rollingWindow
。当然,除此之外,store/redis
也存在指标统计,这个外面的就不须要滑动窗口计数了,因为自身只须要计算命中率,命中则对 hit +1,不命中则对 miss +1 即可,分指标计数,最初统计一下就晓得命中率。
滑动窗口实用于流控中对指标进行计算,同时也能够做到控流。
对于 go-zero
更多的设计和实现文章,能够关注『微服务实际』公众号。
我的项目地址
https://github.com/tal-tech/go-zero
欢送应用 go-zero 并 star 反对咱们!
微信交换群
关注『微服务实际 』公众号并点击 交换群 获取社区群二维码。
go-zero 系列文章见『微服务实际』公众号