go-zero 羣裏常常有同窗問:git
服務監控是經過什麼算法實現的?github
滑動窗口是怎麼工做的?可否講講這塊的原理?redis
熔斷算法是怎麼設計的?爲啥沒有半開半閉狀態呢?算法
本篇文章,來分析一下 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 系列文章見『微服務實踐』公衆號