前言
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
更多的設計和實現文章,可以關注『微服務實踐』公眾號。
專案地址
歡迎使用 go-zero 並 star 支援我們!
微信交流群
關注『微服務實踐』公眾號並點選 交流群 獲取社群群二維碼。
本作品採用《CC 協議》,轉載必須註明作者和本文連結