背景和⽬的
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
爬⾍頻控,臨時通過redis固定窗⼝控制單個IP的訪問頻率
短信頻控(24⼩時最多發送N條),記錄每個⽤⼾的發送記錄,滑動窗⼝判斷是否滿⾜條件
秒殺、活動等常⻅突發流量,進⾏令牌桶控制
穩定性保障重試導致有可能短期流量放⼤,防⽌雪崩,進⾏熔斷限流
業務限流,流量打散,⽐如秒殺場景前端進⾏倒計時限制,提前預約保證流量可預知,也能⼤幅減少⽆效流量。
7. TCP流速控制滑動窗⼝,Nginx限流leaky_bucket,linuxtc流量控制token_bucket
歡迎添加筆者weixin:mingyuan_2018
參考鏈接
https://github.com/uber-go/ratelimitleaky_bucket