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

前言

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

  1. updateOffset 就是作 bucket 更新,以及肯定當前時間落在哪一個 bucket 上【超過桶個數直接返回桶個數】,將其以前的 bucket 重置

    • 肯定當前時間相對於 bucket interval的跨度【超過桶個數直接返回桶個數】
    • 將跨度內的 bucket 都清空數據。reset
    • 更新 offset,也是即將要寫入數據的 bucket
    • 更新執行時間 lastTime,也給下一次移動作一個標誌
  2. 由上一次更新的 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 支持咱們!

微信交流羣

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

go-zero 系列文章見『微服務實踐』公衆號
相關文章
相關標籤/搜索