Golang 限流器的使用和實現

限流器是服務中很是重要的一個組件,在網關設計、微服務、以及普通的後臺應用中都比較常見。它能夠限制訪問服務的頻次和速率,防止服務過載,被刷爆。git

限流器的算法比較多,常見的好比令牌桶算法、漏斗算法、信號量等。本文主要介紹基於漏斗算法的一個限流器的實現。文本也提供了其餘幾種開源的實現方法。github

基於令牌桶的限流器實現

在golang 的官方擴展包 time 中(github/go/time),提供了一個基於令牌桶算法的限流器的實現。golang

原理

令牌桶限流器,有兩個概念:web

  • 令牌:每次都須要拿到令牌後,才能夠訪問
  • 桶:有必定大小的桶,桶中最多能夠放必定數量的令牌
  • 放入頻率:按照必定的頻率向通裏面放入令牌,可是令牌數量不能超過桶的容量

所以,一個令牌桶的限流器,能夠限制一個時間間隔內,最多能夠承載桶容量的訪問頻次。下面咱們看看官方的實現。算法

實現

限流器的定義

下面是對一個限流器的定義:shell

type Limiter struct {
  limit Limit // 放入桶的頻率   (Limit 爲 float64類型)
  burst int   // 桶的大小

  mu     sync.Mutex
  tokens float64 // 當前桶內剩餘令牌個數
  last time.Time  // 最近取走token的時間
  lastEvent time.Time // 最近限流事件的時間
}

其中,核心參數是 limit,burst。 burst 表明了桶的大小,從實際意義上來說,能夠理解爲服務能夠承載的併發量大小;limit 表明了 放入桶的頻率,能夠理解爲正常狀況下,1s內咱們的服務能夠處理的請求個數。網絡

在令牌發放後,會被保留在Reservation 對象中,定義以下:session

type Reservation struct {
  ok        bool  // 是否知足條件分配到了tokens
  lim       *Limiter // 發送令牌的限流器
  tokens    int   // tokens 的數量
  timeToAct time.Time  //  知足令牌發放的時間
  limit Limit  // 令牌發放速度
}

Reservation 對象,描述了一個在達到 timeToAct 時間後,能夠獲取到的令牌的數量tokens。 (由於有些需求會作預留的功能,因此timeToAct 並不必定就是當前的時間。併發

限流器如何限流

官方提供的限流器有阻塞等待式的,也有直接判斷方式的,還有提供了本身維護預留式的,但核心的實現都是下面的reserveN 方法。微服務

// 在 now 時間須要拿到n個令牌,最多能夠等待的時間爲maxFutureResrve
// 結果將返回一個預留令牌的對象
func (lim *Limiter) reserveN(now time.Time, n int, maxFutureReserve time.Duration) Reservation {
  lim.mu.Lock()

  // 首先判斷是否放入頻次是否爲無窮大,若是爲無窮大,說明暫時不限流
  if lim.limit == Inf {
    // ...
  }

  // 拿到截至now 時間時,能夠獲取的令牌tokens數量,上一次拿走令牌的時間last
  now, last, tokens := lim.advance(now)

  // 而後更新 tokens 的數量,把須要拿走的去掉
  tokens -= float64(n)

  // 若是tokens 爲負數,說明須要等待,計算等待的時間
  var waitDuration time.Duration
  if tokens < 0 {
    waitDuration = lim.limit.durationFromTokens(-tokens)
  }

  // 計算是否知足分配條件
  // ① 須要分配的大小不超過桶容量
  // ② 等待時間不超過設定的等待時常
  ok := n <= lim.burst && waitDuration <= maxFutureReserve

  // 最後構造一個Reservation對象
  r := Reservation{
    ok:    ok,
    lim:   lim,
    limit: lim.limit,
  }
  if ok {
    r.tokens = n
    r.timeToAct = now.Add(waitDuration)
  }

  // 並更新當前limiter 的值
  if ok {
    lim.last = now
    lim.tokens = tokens
    lim.lastEvent = r.timeToAct
  } else {
    lim.last = last
  }

  lim.mu.Unlock()
  return r
}

從實現上看,limiter 並非每隔一段時間更新當前桶中令牌的數量,而是記錄了上次訪問時間和當前桶中令牌的數量。當再次訪問時,經過上次訪問時間計算出當前桶中的令牌的數量,決定是否能夠發放令牌。

使用

下面咱們經過一個簡單的例子,學習上面介紹的限流器的使用。

limiter := rate.NewLimiter(rate.Every(100*time.Millisecond), 10)
  http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    if limiter.Allow() {// do something
      log.Println("say hello")
    }
  })
  _ = http.ListenAndServe(":13100", nil)

上面,每100 ms 放入令牌桶中1個令牌,因此當批量訪問該接口時,能夠看到以下結果:

2020/06/26 14:34:16 say hello  有18 條記錄
2020/06/26 14:34:17 say hello  有10 條記錄
2020/06/26 14:34:18 say hello  有10 條記錄
  ...

一開始漏斗滿着,能夠緩解部分突發的流量。當漏斗未空時,訪問的頻次和令牌放入的頻次變爲一致。

其餘限流器的實現

  1. uber 開源庫中基於漏斗算法實現了一個限流器。漏斗算法能夠限制流量的請求速度,並起到削峯填谷的做用。 https://github.com/uber-go/ratelimit
  2. 滴滴開源實現了一個對http請求的限流器中間件。能夠基於如下模式限流。

    • 基於IP,路徑,方法,header,受權用戶等限流
    • 經過自定義方法限流
    • 還支持基於 http header 設置限流數據
    • 實現方式是基於 github/go/time 實現的,不一樣類別的數據都存儲在一個帶超時時間的數據池中。
    • 代碼地址 https://github.com/didip/tollbooth
  3. golang 網絡包中還有基於信號量實現的限流器。 https://github.com/golang/net/blob/master/netutil/listen.go 也值得咱們去學習下。

總結

令牌桶實現的限流器算法,相較於漏斗算法能夠在必定程度上容許突發的流量進入咱們的應用中,因此在web應用中最爲普遍。

在實際使用時,通常不會作全局的限流,而是針對某些特徵去作精細化的限流。例如:經過header、x-forward-for 等限制爬蟲的訪問,經過對 ip,session 等用戶信息限制單個用戶的訪問等。

相關文章
相關標籤/搜索