阿里內網一位p7大佬關於“限流”的分享(僅限小範圍傳播) 背景和⽬的 常⻅算法 常⻅場景使⽤ 參考鏈接

背景和⽬的

Rate limiting is used to control the amount of incoming and outgoing traffic to or from a network。

限流需要解決的問題本質:

1. 未知和已知的⽭盾。互聯⽹流量有⼀定的突發和未知性,系統⾃⼰的處理能⼒是已知的。

2. 需求和資源的⽭盾。需求可能是集中發⽣的,資源⼀般情況下是穩定的。

3. 公平和安全的⽭盾。流量處理⽅式是公平對待的,但其中部分流量有可能是惡意(或者不友好)的,爲了安全和效率考慮是需要限制的。

4 交付和全局的⽭盾。分佈式環境下,服務拓撲⽐較複雜,上游的最⼤努⼒交付和全局穩定性考慮是需要平衡的。

常⻅算法

1. 固定窗⼝(FixedWindow)

1.1 基本原理:通過時間段來定義窗⼝,在該時間段中的請求進⾏add操作,超限直接拒絕。

1.2 現實舉例:

▪ 旅遊景點國慶限流,⼀個⾃然⽇允許多少遊客進⼊。

▪ 銀⾏密碼輸錯三次鎖定,得第⼆天進⾏嘗試。

1.3 優勢:符合⼈類思維,好理解。兩個窗⼝相互獨⽴,新的請求進⼊⼀個新的窗⼝⼤概率會滿⾜,不會形成飢餓效應,實現簡單,快速解決問題。

1.4 劣勢:窗⼝臨界點可能會出現雙倍流量,規則⽐較簡單,容易被攻擊者利⽤。

1.5 實現⽅式:

type LocalWindow struct {
         // The start boundary (timestamp in nanoseconds) of the window.
         // [start, start + size)
       start int64
 
       // The total count of events happened in the window.
       count int64
}
 
func (l *FixedWindowLimiter) Acquire() bool {
      l.mu.Lock()
      defer l.mu.Unlock()
      now := time.Now()
      newCurrStart := now.Truncate(l.interval)
      if newCurrStart != l.window.Start() {
          l.window.Reset(newCurrStart, 0)
      }
      if l.window.Count()+1 > int64(l.limit) {
          return false
      }
      l.window.AddCount(1)
      return true
}

2. 滑動窗⼝(SlidingWidow)

2.1 基本原理:按請求時間計算出當前窗⼝和前⼀個窗⼝,該時間點滑動⼀個窗⼝⼤⼩得到統計開始的時間點,然後按⽐例計算請求總數後add操作,如果超限拒絕,否則當前窗⼝計數更新。

2.2 現實舉例:北京買房從現在起倒推60個⽉連續社保

2.3 優勢:⽐較好地解決固定窗⼝交界的雙倍流量問題,限流準確率和性能都不錯,也不太會有飢餓問題。

2.4 劣勢:惡劣情況下突發流量也是有問題的,精確性⼀般。如果要提⾼精確性就要記錄log或者維護很多個⼩窗⼝,這個成本會提⾼。

2.5 實現⽅式:

func (lim *Limiter) AllowN(now time.Time, n int64) bool {
      lim.mu.Lock()
      defer lim.mu.Unlock()

      lim.advance(now)

      elapsed := now.Sub(lim.curr.Start())
      weight := float64(lim.size-elapsed) / float64(lim.size)
      count := int64(weight*float64(lim.prev.Count())) + lim.curr.Count()

     // Trigger the possible sync behaviour.
     defer lim.curr.Sync(now)

     if count+n > lim.limit {
           return false
     }

     lim.curr.AddCount(n)
     return true
}
 // advance updates the current/previous windows resulting from the passage of
     time.
func (lim *Limiter) advance(now time.Time) {
     // Calculate the start boundary of the expected current-window.
     newCurrStart := now.Truncate(lim.size)

     diffSize := newCurrStart.Sub(lim.curr.Start()) / lim.size
     if diffSize >= 1 {
         // The current-window is at least one-window-size behind the expected one.
         newPrevCount := int64(0)
         if diffSize == 1 {
             // The new previous-window will overlap with the old current-window,
             // so it inherits the count.
             //
             // Note that the count here may be not accurate, since it is only a
             // SNAPSHOT of the current-window's count, which in itself tends to
             // be inaccurate due to the asynchronous nature of the sync behaviour.
             newPrevCount = lim.curr.Count()
         }
         lim.prev.Reset(newCurrStart.Add(-lim.size), newPrevCount)

         // The new current-window always has zero count.
         lim.curr.Reset(newCurrStart, 0)
     }
}

3. 漏桶(LeakyBucket)

3.1 基本原理:所有請求都進⼊⼀個特定容量的桶,桶的流出速度是恆定的,如果桶滿則拋棄,滿⾜FIFO特性。

3.2 現實舉例:旅遊景點檢票處效率恆定,如果檢不過來,⼤家都要排隊,假設排隊沒地⽅排了,那麼就只能放棄了。

3.3 優勢:輸出速率⼀定,能接受突發輸⼊流量,但需要排隊處理。桶的⼤⼩和速率是⼀定的,所以資源是可以充分利⽤的。

3.4 劣勢:容易出現飢餓問題,並且時效性沒有保證,突發流量沒法很快流出。

3.5 實現⽅式:

type limiter struct {
    sync.Mutex
    last         time.Time       //上⼀個請求流出時間
    sleepFor     time.Duration   // 需要等待的時⻓
    perRequest time.Duration     // 每個請求處理時⻓
    maxSlack     time.Duration   // 突發流量控制閾值
    clock        Clock
}
 // Take blocks to ensure that the time spent between multiple
 // Take calls is on average time.Second/rate.
func (t *limiter) Take() time.Time {
    t.Lock()
    defer t.Unlock()

    now := t.clock.Now()

    // If this is our first request, then we allow it.
    if t.last.IsZero() {
        t.last = now
        return t.last
    }

     // sleepFor calculates how much time we should sleep based on
     // the perRequest budget and how long the last request took.
     // Since the request may take longer than the budget, this number
     // can get negative, and is summed across requests.
     t.sleepFor += t.perRequest - now.Sub(t.last)

     // We shouldn't allow sleepFor to get too negative, since it would mean that
     // a service that slowed down a lot for a short period of time would get
     // a much higher RPS following that.
     if t.sleepFor < t.maxSlack {
         t.sleepFor = t.maxSlack
     }

     // If sleepFor is positive, then we should sleep now.
     if t.sleepFor > 0 {
         t.clock.Sleep(t.sleepFor)
         t.last = now.Add(t.sleepFor)
         t.sleepFor = 0
     } else {
         t.last = now
     }

     return t.last
}

4. 令牌桶(TokenBucket)

4.1 基本原理:特定速率往⼀個特定容量的桶⾥放⼊令牌,如果桶滿,令牌丟棄,所有請求進⼊桶中拿令牌,拿不到令牌丟棄。

4.2 現實舉例:旅遊景點不控制檢票速度(假設是光速),⽽控制放票速度,有票的⼈直接就可以進。

4.3 優勢:可以⽀持突發流量,靈活性⽐較好。

4.4 劣勢:實現稍顯複雜。

4.5 實現⽅式

  type qpsLimiter struct {
      limit       int32
      tokens      int32
      interval time.Duration
      once        int32
      ticker      *time.Ticker
}
 //   這⾥允許⼀些誤差,直接Store,可以換來更好的性能,也解決了⼤併發情況之下CAS不上的問題 by
      chengguozhu
func (l *qpsLimiter) updateToken() {
     var v int32
     v = atomic.LoadInt32(&l.tokens)
     if v < 0 {
         v = atomic.LoadInt32(&l.once)
     } else if v+atomic.LoadInt32(&l.once) > atomic.LoadInt32(&l.limit) {
         v = atomic.LoadInt32(&l.limit)
     } else {
         v = v + atomic.LoadInt32(&l.once)
     }
     atomic.StoreInt32(&l.tokens, v)
}

5. 分佈式限流

5.1 思路:⽤分佈式⽅式實現相關算法,redis替代本地內存,算法邏輯可通過lua來實現。

常⻅場景使⽤

1.下游流量保護,sleep⼤法,leaky_bucket

  1. 爬⾍頻控,臨時通過redis固定窗⼝控制單個IP的訪問頻率

  2. 短信頻控(24⼩時最多發送N條),記錄每個⽤⼾的發送記錄,滑動窗⼝判斷是否滿⾜條件

  3. 秒殺、活動等常⻅突發流量,進⾏令牌桶控制

  4. 穩定性保障重試導致有可能短期流量放⼤,防⽌雪崩,進⾏熔斷限流

  5. 業務限流,流量打散,⽐如秒殺場景前端進⾏倒計時限制,提前預約保證流量可預知,也能⼤幅減少⽆效流量。

7. TCP流速控制滑動窗⼝,Nginx限流leaky_bucket,linuxtc流量控制token_bucket

歡迎添加筆者weixin:mingyuan_2018

參考鏈接

https://github.com/uber-go/ratelimitleaky_bucket

https://github.com/RussellLuo/slidingwindowsliding_window

https://github.com/juju/ratelimittoken_bucket

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章