自適應微服務治理背後的算法

前言

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 發生的窗口變化。解釋一下:

  1. updateOffset 就是做 bucket 更新,以及確定當前時間落在哪個 bucket 上【超過桶個數直接返回桶個數】,將其之前的 bucket 重置
  1. 由上一次更新的 offset,向對應的 bucket 寫入數據

而在這個過程中,如何確定確定 bucket 過期點,以及更新時間。滑動窗口最重要的就是時間更新,下面用圖來解釋這個過程:

而  bucket 過期點,說白就是 lastTime 即上一個更新時間跨越了幾個 buckettimex.Since(rw.lastTime) / rw.interval

這樣,在 Add() 的過程中,通過 lastTimenowTime 的標註,通過不斷重置來實現窗口滑動,新的數據不斷補上,從而實現窗口計算。

總結

本文分析了 go-zero 框架中的指標統計的基礎封裝、滑動窗口的實現 rollingWindow。當然,除此之外,store/redis 也存在指標統計,這個裏面的就不需要滑動窗口計數了,因爲本身只需要計算命中率,命中則對 hit +1,不命中則對 miss +1 即可,分指標計數,最後統計一下就知道命中率。

滑動窗口適用於流控中對指標進行計算,同時也可以做到控流。

關於 go-zero 更多的設計和實現文章,可以關注『微服務實踐』公衆號。

項目地址

https://github.com/tal-tech/go-zero

歡迎使用 go-zero 並 star 支持我們!

微信交流羣

關注『微服務實踐』公衆號並點擊 交流羣 獲取社區羣二維碼。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/OpF4JVWHA9W1v0yefeo56w