一文聊透限流這件事兒
限流簡介
現在說到高可用系統,都會說到高可用的保護手段:緩存、降級和限流,本博文就主要說說限流。限流是流量限速(Rate Limit)的簡稱,是指只允許指定的事件進入系統,超過的部分將被拒絕服務、排隊或等待、降級等處理。對於 server 服務而言,限流爲了保證一部分的請求流量可以得到正常的響應,總好過全部的請求都不能得到響應,甚至導致系統雪崩。限流與熔斷經常被人弄混,博主認爲它們最大的區別在於限流主要在 server 實現,而熔斷主要在 client 實現,當然了,一個服務既可以充當 server 也可以充當 client,這也是讓限流與熔斷同時存在一個服務中,這兩個概念才容易被混淆。
那爲什麼需要限流呢?很多人第一反應就是服務扛不住了所以需要限流。這是不全面的說法,博主認爲限流是因爲資源的稀缺或出於安全防範的目的,採取的自我保護的措施。限流可以保證使用有限的資源提供最大化的服務能力,按照預期流量提供服務,超過的部分將會拒絕服務、排隊或等待、降級等處理。
現在的系統對限流的支持各有不同,但是存在一些標準。在 HTTP RFC 6585 標準中規定了『429 Too Many Requests 』,429 狀態碼錶示用戶在給定時間內發送了太多的請求,需要進行限流(“速率限制”),同時包含一個 Retry-After 響應頭用於告訴客戶端多長時間後可以再次請求服務。
HTTP/1.1 429 Too Many Requests
Content-Type: text/html
Retry-After: 3600
<title>Too Many Requests</title>
<h1>Too Many Requests</h1>
<p>I only allow 50 requests per hour to this Web site per
logged in user. Try again soon.</p>
很多應用框架同樣集成了,限流功能並且在返回的 Header 中給出明確的限流標識。
-
X-Rate-Limit-Limit:同一個時間段所允許的請求的最大數目;
-
X-Rate-Limit-Remaining:在當前時間段內剩餘的請求的數量;
-
X-Rate-Limit-Reset:爲了得到最大請求數所等待的秒數。
這是通過響應頭告訴調用方服務端的限流頻次是怎樣的,保證後端的接口訪問上限,客戶端也可以根據響應的 Header 調整請求。
限流分類
限流
,拆分來看,就兩個字限
和流
,限
就是動詞限制,很好理解。但是流
在不同的場景之下就是不同資源或指標,多樣性就在流
中體現。在網絡流量中可以是字節流,在數據庫中可以是 TPS,在 API 中可以是 QPS 亦可以是併發請求數,在商品中還可以是庫存數... ... 但是不管是哪一種『流』,這個流必須可以被量化,可以被度量,可以被觀察到、可以統計出來。我們把限流的分類基於不同的方式分爲不同的類別,如下圖。
限流分類
因爲篇幅有限,本文只會挑選幾個常見的類型分類進行說明。
限流粒度分類
根據限流的粒度分類:
-
單機限流
-
分佈式限流
現狀的系統基本上都是分佈式架構,單機的模式已經很少了,這裏說的單機限流更加準確一點的說法是單服務節點限流。單機限流是指請求進入到某一個服務節點後超過了限流閾值,服務節點採取了一種限流保護措施。
單機限流示意圖
分佈式限流狹義的說法是在接入層實現多節點合併限流,比如 NGINX+redis,分佈式網關等,廣義的分佈式限流是多個節點(可以爲不同服務節點)有機整合,形成整體的限流服務。
分佈式限流示意圖
單機限流防止流量壓垮服務節點,缺乏對整體流量的感知。分佈式限流適合做細粒度不同的限流控制,可以根據場景不同匹配不同的限流規則。與單機限流最大的區別,分佈式限流需要中心化存儲,常見的使用 redis 實現。引入了中心化存儲,就需要解決以下問題:
-
數據一致性
在限流模式中理想的模式爲時間點一致性。時間點一致性的定義中要求所有數據組件的數據在任意時刻都是完全一致的,但是一般來說信息傳播的速度最大是光速,其實並不能達到任意時刻一致,總有一定的時間不一致,對於我們 CAP 中的一致性來說只要達到讀取到最新數據即可,達到這種情況並不需要嚴格的任意時間一致。這隻能是理論當中的一致性模型,可以在限流中達到線性一致性即可。
-
時間一致性
這裏的時間一致性與上述的時間點一致性不一樣,這裏就是指各個服務節點的時間一致性。一個集羣有 3 臺機器,但是在某一個 A/B 機器的時間爲
Tue Dec 3 16:29:28 CST 2019
,C 爲Tue Dec 3 16:29:28 CST 2019
,那麼它們的時間就不一致。那麼使用 ntpdate 進行同步也會存在一定的誤差,對於時間窗口敏感的算法就是誤差點。 -
超時
在分佈式系統中就需要網絡進行通信,會存在網絡抖動問題,或者分佈式限流中間件壓力過大導致響應變慢,甚至是超時時間閾值設置不合理,導致應用服務節點超時了,此時是放行流量還是拒絕流量?
-
性能與可靠性
分佈式限流中間件的資源總是有限的,甚至可能是單點的(寫入單點),性能存在上限。如果分佈式限流中間件不可用時候如何退化爲單機限流模式也是一個很好的降級方案。
限流對象類型分類
按照對象類型分類:
-
基於請求限流
-
基於資源限流
基於請求限流,一般的實現方式有限制總量和限制 QPS。限制總量就是限制某個指標的上限,比如搶購某一個商品,放量是 10w,那麼最多隻能賣出 10w 件。微信的搶紅包,羣裏發一個紅包拆分爲 10 個,那麼最多隻能有 10 人可以搶到,第十一個人打開就會顯示『手慢了,紅包派完了』。
紅包搶完了
限制 QPS,也是我們常說的限流方式,只要在接口層級進行,某一個接口只允許 1 秒只能訪問 100 次,那麼它的峯值 QPS 只能爲 100。限制 QPS 的方式最難的點就是如何預估閾值,如何定位閾值,下文中會說到。
基於資源限流是基於服務資源的使用情況進行限制,需要定位到服務的關鍵資源有哪些,並對其進行限制,如限制 TCP 連接數、線程數、內存使用量等。限制資源更能有效地反映出服務當前地清理,但與限制 QPS 類似,面臨着如何確認資源的閾值爲多少。這個閾值需要不斷地調優,不停地實踐纔可以得到一個較爲滿意地值。
限流算法分類
不論是按照什麼維度,基於什麼方式的分類,其限流的底層均是需要算法來實現。下面介紹實現常見的限流算法:
-
計數器
-
令牌桶算法
-
漏桶算法
計數器
固定窗口計數器
計數限流是最爲簡單的限流算法,日常開發中,我們說的限流很多都是說固定窗口計數限流算法,比如某一個接口或服務 1s 最多隻能接收 1000 個請求,那麼我們就會設置其限流爲 1000QPS。該算法的實現思路非常簡單,維護一個固定單位時間內的計數器,如果檢測到單位時間已經過去就重置計數器爲零。
固定窗口計數器原理
其操作步驟:
-
時間線劃分爲多個獨立且固定大小窗口;
-
落在每一個時間窗口內的請求就將計數器加 1;
-
如果計數器超過了限流閾值,則後續落在該窗口的請求都會被拒絕。但時間達到下一個時間窗口時,計數器會被重置爲 0。
下面實現一個簡單的代碼。
package limit
import (
"sync/atomic"
"time"
)
type Counter struct {
Count uint64 // 初始計數器
Limit uint64 // 單位時間窗口最大請求頻次
Interval int64 // 單位ms
RefreshTime int64 // 時間窗口
}
func NewCounter(count, limit uint64, interval, rt int64) *Counter {
return &Counter{
Count: count,
Limit: limit,
Interval: interval,
RefreshTime: rt,
}
}
func (c *Counter) RateLimit() bool {
now := time.Now().UnixNano() / 1e6
if now < (c.RefreshTime + c.Interval) {
atomic.AddUint64(&c.Count, 1)
return c.Count <= c.Limit
} else {
c.RefreshTime = now
atomic.AddUint64(&c.Count, -c.Count)
return true
}
}
測試代碼:
package limit
import (
"fmt"
"testing"
"time"
)
func Test_Counter(t *testing.T) {
counter := NewCounter(0, 5, 100, time.Now().Unix())
for i := 0; i < 10; i++ {
go func(i int) {
for k := 0; k <= 10; k++ {
fmt.Println(counter.RateLimit())
if k%3 == 0 {
time.Sleep(102 * time.Millisecond)
}
}
}(i)
}
time.Sleep(10 * time.Second)
}
看了上面的邏輯,有沒有覺得固定窗口計數器很簡單,對,就是這麼簡單,這就是它的一個優點實現簡單。同時也存在兩個比較嚴重缺陷。試想一下,固定時間窗口 1s 限流閾值爲 100,但是前 100ms,已經請求來了 99 個,那麼後續的 900ms 只能通過一個了,就是一個缺陷,基本上沒有應對突發流量的能力。第二個缺陷,在 00:00:00 這個時間窗口的後 500ms,請求通過了 100 個,在 00:00:01 這個時間窗口的前 500ms 還有 100 個請求通過,對於服務來說相當於 1 秒內請求量達到了限流閾值的 2 倍。
滑動窗口計數器
滑動時間窗口算法是對固定時間窗口算法的一種改進,這詞被大衆所知實在 TCP 的流量控制中。固定窗口計數器可以說是滑動窗口計數器的一種特例,滑動窗口的操作步驟:
-
將單位時間劃分爲多個區間,一般都是均分爲多個小的時間段;
-
每一個區間內都有一個計數器,有一個請求落在該區間內,則該區間內的計數器就會加一;
-
每過一個時間段,時間窗口就會往右滑動一格,拋棄最老的一個區間,並納入新的一個區間;
-
計算整個時間窗口內的請求總數時會累加所有的時間片段內的計數器,計數總和超過了限制數量,則本窗口內所有的請求都被丟棄。
時間窗口劃分的越細,並且按照時間 "滑動",這種算法避免了固定窗口計數器出現的上述兩個問題。缺點是時間區間的精度越高,算法所需的空間容量就越大。
常見的實現方式主要有基於 redis zset 的方式和循環隊列實現。基於 redis zset 可將 Key 爲限流標識 ID,Value 保持唯一,可以用 UUID 生成,Score 也記爲同一時間戳,最好是納秒級的。使用 redis 提供的 ZADD、EXPIRE、ZCOUNT 和 zremrangebyscore 來實現,並同時注意開啓 Pipeline 來儘可能提升性能。實現很簡單,但是缺點就是 zset 的數據結構會越來越大。
漏桶算法
漏桶算法是水先進入到漏桶裏,漏桶再以一定的速率出水,當流入水的數量大於流出水時,多餘的水直接溢出。把水換成請求來看,漏桶相當於服務器隊列,但請求量大於限流閾值時,多出來的請求就會被拒絕服務。漏桶算法使用隊列實現,可以以固定的速率控制流量的訪問速度,可以做到流量的 “平整化” 處理。
大家可以通過網上最流行的一張圖來理解。
漏桶算法原理
漏桶算法實現步驟:
-
將每個請求放入固定大小的隊列進行存儲;
-
隊列以固定速率向外流出請求,如果隊列爲空則停止流出;
-
如隊列滿了則多餘的請求會被直接拒絕 ·
漏桶算法有一個明顯的缺陷:當短時間內有大量的突發請求時,即使服務器負載不高,每個請求也都得在隊列中等待一段時間才能被響應。
令牌桶算法
令牌桶算法的原理是系統會以一個恆定的速率往桶裏放入令牌,而如果請求需要被處理,則需要先從桶裏獲取一個令牌,當桶裏沒有令牌可取時,則拒絕服務。從原理上看,令牌桶算法和漏桶算法是相反的,前者爲 “進”,後者爲“出”。漏桶算法與令牌桶算法除了“方向” 上的不同還有一個更加主要的區別:令牌桶算法限制的是平均流入速率(允許突發請求,只要有足夠的令牌,支持一次拿多個令牌),並允許一定程度突發流量;
令牌桶算法的實現步驟:
-
令牌以固定速率生成並放入到令牌桶中;
-
如果令牌桶滿了則多餘的令牌會直接丟棄,當請求到達時,會嘗試從令牌桶中取令牌,取到了令牌的請求可以執行;
-
如果桶空了,則拒絕該請求。
令牌桶算法原理
四種策略該如何選擇?
-
固定窗口:實現簡單,但是過於粗暴,除非情況緊急,爲了能快速止損眼前的問題可以作爲臨時應急的方案。
-
滑動窗口:限流算法簡單易實現,可以應對有少量突增流量場景。
-
漏桶:對於流量絕對均勻有很強的要求,資源的利用率上不是極致,但其寬進嚴出模式,保護系統的同時還留有部分餘量,是一個通用性方案。
-
令牌桶:系統經常有突增流量,並儘可能的壓榨服務的性能。
怎麼做限流?
不論使用上述的哪一種分類或者實現方式,系統都會面臨一個共同的問題:如何確認限流閾值。有人團隊根據經驗先設定一個小的閾值,後續慢慢進行調整;有的團隊是通過進行壓力測試後總結出來。這種方式的問題在於壓測模型與線上環境不一定一致,接口的單壓不能反饋整個系統的狀態,全鏈路壓測又難以真實反應實際流量場景流量比例。再換一個思路是通過壓測 + 各應用監控數據。根據系統峯值的 QPS 與系統資源使用情況,進行等水位放大預估限流閾值,問題在於系統性能拐點未知,單純的預測不一定準確甚至極大偏離真實場景。正如《Overload Control for Scaling WeChat Microservices》所說,在具有複雜依賴關係的系統中,對特定服務的進行過載控制可能對整個系統有害或者服務的實現有缺陷。希望後續可以出現一個更加 AI 的運行反饋自動設置限流閾值的系統,可以根據當前 QPS、資源狀態、RT 情況等多種關聯數據動態地進行過載保護。
不論是哪一種方式給出的限流閾值,系統都應該關注以下幾點:
-
運行指標狀態,比如當前服務的 QPS、機器資源使用情況、數據庫的連接數、線程的併發數等;
-
資源間的調用關係,外部鏈路請求、內部服務之間的關聯、服務之間的強弱依賴等;
-
控制方式,達到限流後對後續的請求直接拒絕、快速失敗、排隊等待等處理方式
go 限流類庫使用
限流的類庫有很多,不同語言的有不同的類庫,如大 Java 的有 concurrency-limits、Sentinel、Guava 等,這些類庫都有很多的分析和使用方式了,本文主要介紹 Golang 的限流類庫就是 Golang 的擴展庫:https://github.com/golang/time/rate 。可以進去語言類庫的代碼都值得去研讀一番,學習過 Java 的同學是否對 AQS 的設計之精妙而感嘆呢!time/rate
也有其精妙的部分,下面開始進入類庫學習階段。
github.com/golang/time/rate
進行源碼分析前的,最應該做的是瞭解類庫的使用方式、使用場景和 API。對業務有了初步的瞭解,閱讀代碼就可以事半功倍。因爲篇幅有限後續的博文在對多個限流類庫源碼做分析。類庫的 API 文檔:https://godoc.org/golang.org/x/time/rate。time/rate 類庫是基於令牌桶算法實現的限流功能。前面說令牌桶算法的原理是系統會以一個恆定的速率往桶裏放入令牌,那麼桶就有一個固定的大小,往桶中放入令牌的速率也是恆定的,並且允許突發流量。查看文檔發現一個函數:
func NewLimiter(r Limit, b int) *Limiter
newLimiter 返回一個新的限制器,它允許事件的速率達到 r,並允許最多突發 b 個令牌。也就是說 Limter 限制時間的發生頻率,但這個桶一開始容量就爲 b,並且裝滿 b 個令牌(令牌池中最多有 b 個令牌,所以一次最多隻能允許 b 個事件發生,一個事件花費掉一個令牌),然後每一個單位時間間隔(默認 1s)往桶裏放入 r 個令牌。
limter := rate.NewLimiter(10, 5)
上面的例子表示,令牌桶的容量爲 5,並且每一秒中就往桶裏放入 10 個令牌。細心的讀者都會發現函數 NewLimiter 第一個參數是 Limit 類型,可以看源碼就會發現 Limit 實際上就是 float64 的別名。
// Limit defines the maximum frequency of some events.
// Limit is represented as number of events per second.
// A zero Limit allows no events.
type Limit float64
限流器還可以指定往桶裏放入令牌的時間間隔,實現方式如下:
limter := rate.NewLimiter(rate.Every(100*time.Millisecond), 5)
這兩個例子的效果是一樣的,使用第一種方式不會出現在每一秒間隔一下子放入 10 個令牌,也是均勻分散在 100ms 的間隔放入令牌。rate.Limiter 提供了三類方法用來限速:
-
Allow/AllowN
-
Wait/WaitN
-
Reserve/ReserveN
下面對比這三類限流方式的使用方式和適用場景。先看第一類方法:
func (lim *Limiter) Allow() bool
func (lim *Limiter) AllowN(now time.Time, n int) bool
Allow 是 AllowN(time.Now(), 1) 的簡化方法。那麼重點就在方法 AllowN 上了,API 的解釋有點抽象,說得雲裏霧裏的,可以看看下面的 API 文檔解釋:
AllowN reports whether n events may happen at time now.
Use this method if you intend to drop / skip events that exceed the rate limit.
Otherwise use Reserve or Wait.
實際上就是爲了說,方法 AllowN 在指定的時間時是否可以從令牌桶中取出 N 個令牌。也就意味着可以限定 N 個事件是否可以在指定的時間同時發生。這個兩個方法是無阻塞,也就是說一旦不滿足,就會跳過,不會等待令牌數量足夠才執行。也就是文檔中的第二行解釋,如果打算丟失或跳過超出速率限制的時間,那麼久請使用該方法。比如使用之前實例化好的限流器,在某一個時刻,服務器同時收到超過了 8 個請求,如果令牌桶內令牌小於 8 個,那麼這 8 個請求就會被丟棄。一個小示例:
func AllowDemo() {
limter := rate.NewLimiter(rate.Every(200*time.Millisecond), 5)
i := 0
for {
i++
if limter.Allow() {
fmt.Println(i, "====Allow======", time.Now())
} else {
fmt.Println(i, "====Disallow======", time.Now())
}
time.Sleep(80 * time.Millisecond)
if i == 15 {
return
}
}
}
執行結果:
1 ====Allow====== 2019-12-14 15:54:09.9852178 +0800 CST m=+0.005998001
2 ====Allow====== 2019-12-14 15:54:10.1012231 +0800 CST m=+0.122003301
3 ====Allow====== 2019-12-14 15:54:10.1823056 +0800 CST m=+0.203085801
4 ====Allow====== 2019-12-14 15:54:10.263238 +0800 CST m=+0.284018201
5 ====Allow====== 2019-12-14 15:54:10.344224 +0800 CST m=+0.365004201
6 ====Allow====== 2019-12-14 15:54:10.4242458 +0800 CST m=+0.445026001
7 ====Allow====== 2019-12-14 15:54:10.5043101 +0800 CST m=+0.525090301
8 ====Allow====== 2019-12-14 15:54:10.5852232 +0800 CST m=+0.606003401
9 ====Disallow====== 2019-12-14 15:54:10.6662181 +0800 CST m=+0.686998301
10 ====Disallow====== 2019-12-14 15:54:10.7462189 +0800 CST m=+0.766999101
11 ====Allow====== 2019-12-14 15:54:10.8272182 +0800 CST m=+0.847998401
12 ====Disallow====== 2019-12-14 15:54:10.9072192 +0800 CST m=+0.927999401
13 ====Allow====== 2019-12-14 15:54:10.9872224 +0800 CST m=+1.008002601
14 ====Disallow====== 2019-12-14 15:54:11.0672253 +0800 CST m=+1.088005501
15 ====Disallow====== 2019-12-14 15:54:11.1472946 +0800 CST m=+1.168074801
第二類方法:因爲 ReserveN 比較複雜,第二類先說 WaitN。
func (lim *Limiter) Wait(ctx context.Context) (err error)
func (lim *Limiter) WaitN(ctx context.Context, n int) (err error)
類似 Wait 是 WaitN(ctx, 1) 的簡化方法。與 AllowN 不同的是 WaitN 會阻塞,如果令牌桶內的令牌數不足 N 個,WaitN 會阻塞一段時間,阻塞時間的時長可以用第一個參數 ctx 進行設置,把 context 實例爲 context.WithDeadline 或 context.WithTimeout 進行制定阻塞的時長。
func WaitNDemo() {
limter := rate.NewLimiter(10, 5)
i := 0
for {
i++
ctx, canle := context.WithTimeout(context.Background(), 400*time.Millisecond)
if i == 6 {
// 取消執行
canle()
}
err := limter.WaitN(ctx, 4)
if err != nil {
fmt.Println(err)
continue
}
fmt.Println(i, ",執行:", time.Now())
if i == 10 {
return
}
}
}
執行結果:
1 ,執行:2019-12-14 15:45:15.538539 +0800 CST m=+0.011023401
2 ,執行:2019-12-14 15:45:15.8395195 +0800 CST m=+0.312003901
3 ,執行:2019-12-14 15:45:16.2396051 +0800 CST m=+0.712089501
4 ,執行:2019-12-14 15:45:16.6395169 +0800 CST m=+1.112001301
5 ,執行:2019-12-14 15:45:17.0385893 +0800 CST m=+1.511073701
context canceled
7 ,執行:2019-12-14 15:45:17.440514 +0800 CST m=+1.912998401
8 ,執行:2019-12-14 15:45:17.8405152 +0800 CST m=+2.312999601
9 ,執行:2019-12-14 15:45:18.2405402 +0800 CST m=+2.713024601
10 ,執行:2019-12-14 15:45:18.6405179 +0800 CST m=+3.113002301
適用於允許阻塞等待的場景,比如消費消息隊列的消息,可以限定最大的消費速率,過大了就會被限流避免消費者負載過高。
第三類方法:
func (lim *Limiter) Reserve() *Reservation
func (lim *Limiter) ReserveN(now time.Time, n int) *Reservation
與之前的兩類方法不同的是 Reserve/ReserveN 返回了 Reservation 實例。Reservation 在 API 文檔中有 5 個方法:
func (r *Reservation) Cancel() // 相當於CancelAt(time.Now())
func (r *Reservation) CancelAt(now time.Time)
func (r *Reservation) Delay() time.Duration // 相當於DelayFrom(time.Now())
func (r *Reservation) DelayFrom(now time.Time) time.Duration
func (r *Reservation) OK() bool
通過這 5 個方法可以讓開發者根據業務場景進行操作,相比前兩類的自動化,這樣的操作顯得複雜多了。通過一個小示例來學習 Reserve/ReserveN:
func ReserveNDemo() {
limter := rate.NewLimiter(10, 5)
i := 0
for {
i++
reserve := limter.ReserveN(time.Now(), 4)
// 如果爲flase說明拿不到指定數量的令牌,比如需要的令牌數大於令牌桶容量的場景
if !reserve.OK() {
return
}
ts := reserve.Delay()
time.Sleep(ts)
fmt.Println("執行:", time.Now(),ts)
if i == 10 {
return
}
}
}
執行結果:
執行:2019-12-14 16:22:26.6446468 +0800 CST m=+0.008000201 0s
執行:2019-12-14 16:22:26.9466454 +0800 CST m=+0.309998801 247.999299ms
執行:2019-12-14 16:22:27.3446473 +0800 CST m=+0.708000701 398.001399ms
執行:2019-12-14 16:22:27.7456488 +0800 CST m=+1.109002201 399.999499ms
執行:2019-12-14 16:22:28.1456465 +0800 CST m=+1.508999901 398.997999ms
執行:2019-12-14 16:22:28.5456457 +0800 CST m=+1.908999101 399.0003ms
執行:2019-12-14 16:22:28.9446482 +0800 CST m=+2.308001601 399.001099ms
執行:2019-12-14 16:22:29.3446524 +0800 CST m=+2.708005801 399.998599ms
執行:2019-12-14 16:22:29.7446514 +0800 CST m=+3.108004801 399.9944ms
執行:2019-12-14 16:22:30.1446475 +0800 CST m=+3.508000901 399.9954ms
如果在執行Delay()
之前操作Cancel()
那麼返回的時間間隔就會爲 0,意味着可以立即執行操作,不進行限流。
func ReserveNDemo2() {
limter := rate.NewLimiter(5, 5)
i := 0
for {
i++
reserve := limter.ReserveN(time.Now(), 4)
// 如果爲flase說明拿不到指定數量的令牌,比如需要的令牌數大於令牌桶容量的場景
if !reserve.OK() {
return
}
if i == 6 || i == 5 {
reserve.Cancel()
}
ts := reserve.Delay()
time.Sleep(ts)
fmt.Println(i, "執行:", time.Now(), ts)
if i == 10 {
return
}
}
}
執行結果:
1 執行:2019-12-14 16:25:45.7974857 +0800 CST m=+0.007005901 0s
2 執行:2019-12-14 16:25:46.3985135 +0800 CST m=+0.608033701 552.0048ms
3 執行:2019-12-14 16:25:47.1984796 +0800 CST m=+1.407999801 798.9722ms
4 執行:2019-12-14 16:25:47.9975269 +0800 CST m=+2.207047101 799.0061ms
5 執行:2019-12-14 16:25:48.7994803 +0800 CST m=+3.009000501 799.9588ms
6 執行:2019-12-14 16:25:48.7994803 +0800 CST m=+3.009000501 0s
7 執行:2019-12-14 16:25:48.7994803 +0800 CST m=+3.009000501 0s
8 執行:2019-12-14 16:25:49.5984782 +0800 CST m=+3.807998401 798.0054ms
9 執行:2019-12-14 16:25:50.3984779 +0800 CST m=+4.607998101 799.0075ms
10 執行:2019-12-14 16:25:51.1995131 +0800 CST m=+5.409033301 799.0078ms
看到這裏 time/rate 的限流方式已經完成,除了上述的三類限流方式,time/rate 還提供了動態調整限流器參數的功能。相關 API 如下:
func (lim *Limiter) SetBurst(newBurst int) // 相當於SetBurstAt(time.Now(), newBurst).
func (lim *Limiter) SetBurstAt(now time.Time, newBurst int)// 重設令牌桶的容量
func (lim *Limiter) SetLimit(newLimit Limit) // 相當於SetLimitAt(time.Now(), newLimit)
func (lim *Limiter) SetLimitAt(now time.Time, newLimit Limit)// 重設放入令牌的速率
這四個方法可以讓程序根據自身的狀態動態的調整令牌桶速率和令牌桶容量。
結尾
通過上述一系列講解,相信大家對各個限流的應用場景和優缺點也有了大致的掌握,希望在日常開發中有所幫助。限流僅僅是整個服務治理中的一個小環節,需要與多種技術結合使用,纔可以更好的提升服務的穩定性的同時提高用戶體驗。
附錄
https://github.com/uber-go/ratelimit https://en.wikipedia.org/wiki/Token_bucket https://www.cs.columbia.edu/~ruigu/papers/socc18-final100.pdf https://github.com/alibaba/Sentinel https://tools.ietf.org/html/rfc6585 https://www.yiichina.com/doc/guide/2.0/rest-rate-limiting https://github.com/RussellLuo/slidingwindow http://zim.logdown.com/posts/300977-distributed-rate-limiter https://www.yuque.com/clip/dev-wiki/axo1wb?language=en-us
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/icXd55HR2GiVr95d44V34w