溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

程序員必知的限流方案有哪些

發(fā)布時(shí)間:2021-10-19 16:25:46 來(lái)源:億速云 閱讀:106 作者:iii 欄目:編程語(yǔ)言

這篇文章主要介紹“程序員必知的限流方案有哪些”,在日常操作中,相信很多人在程序員必知的限流方案有哪些問(wèn)題上存在疑惑,小編查閱了各式資料,整理出簡(jiǎn)單好用的操作方法,希望對(duì)大家解答”程序員必知的限流方案有哪些”的疑惑有所幫助!接下來(lái),請(qǐng)跟著小編一起來(lái)學(xué)習(xí)吧!

限流簡(jiǎn)介

現(xiàn)在說(shuō)到高可用系統(tǒng),都會(huì)說(shuō)到高可用的保護(hù)手段:緩存、降級(jí)和限流,本博文就主要說(shuō)說(shuō)限流。限流是流量限速(Rate Limit)的簡(jiǎn)稱,是指只允許指定的事件進(jìn)入系統(tǒng),超過(guò)的部分將被拒絕服務(wù)、排隊(duì)或等待、降級(jí)等處理。

對(duì)于server服務(wù)而言,限流為了保證一部分的請(qǐng)求流量可以得到正常的響應(yīng),總好過(guò)全部的請(qǐng)求都不能得到響應(yīng),甚至導(dǎo)致系統(tǒng)雪崩。限流與熔斷經(jīng)常被人弄混,博主認(rèn)為它們最大的區(qū)別在于限流主要在server實(shí)現(xiàn),而熔斷主要在client實(shí)現(xiàn),當(dāng)然了,一個(gè)服務(wù)既可以充當(dāng)server也可以充當(dāng)client,這也是讓限流與熔斷同時(shí)存在一個(gè)服務(wù)中,這兩個(gè)概念才容易被混淆,關(guān)注公眾號(hào)碼猿技術(shù)專欄獲取更多面試資源。

那為什么需要限流呢?很多人第一反應(yīng)就是服務(wù)扛不住了所以需要限流。這是不全面的說(shuō)法,博主認(rèn)為限流是因?yàn)橘Y源的稀缺或出于安全防范的目的,采取的自我保護(hù)的措施。限流可以保證使用有限的資源提供最大化的服務(wù)能力,按照預(yù)期流量提供服務(wù),超過(guò)的部分將會(huì)拒絕服務(wù)、排隊(duì)或等待、降級(jí)等處理。

現(xiàn)在的系統(tǒng)對(duì)限流的支持各有不同,但是存在一些標(biāo)準(zhǔn)。在HTTP RFC 6585標(biāo)準(zhǔn)中規(guī)定了『429 Too Many Requests 』,429狀態(tài)碼表示用戶在給定時(shí)間內(nèi)發(fā)送了太多的請(qǐng)求,需要進(jìn)行限流(“速率限制”),同時(shí)包含一個(gè) Retry-After 響應(yīng)頭用于告訴客戶端多長(zhǎng)時(shí)間后可以再次請(qǐng)求服務(wù),關(guān)注公眾號(hào)碼猿技術(shù)專欄獲取更多面試資源。

HTTP/1.1 429 Too Many Requests
Content-Type: text/html
Retry-After: 3600


  
     <title>Too Many Requests</title>  
  
     <h2>Too Many Requests</h2>     <p>I only allow 50 requests per hour to this Web site per
        logged in user.  Try again soon.</p>

很多應(yīng)用框架同樣集成了,限流功能并且在返回的Header中給出明確的限流標(biāo)識(shí)。

  • X-Rate-Limit-Limit:同一個(gè)時(shí)間段所允許的請(qǐng)求的最大數(shù)目;

  • X-Rate-Limit-Remaining:在當(dāng)前時(shí)間段內(nèi)剩余的請(qǐng)求的數(shù)量;

  • X-Rate-Limit-Reset:為了得到最大請(qǐng)求數(shù)所等待的秒數(shù)。

這是通過(guò)響應(yīng)頭告訴調(diào)用方服務(wù)端的限流頻次是怎樣的,保證后端的接口訪問(wèn)上限,客戶端也可以根據(jù)響應(yīng)的Header調(diào)整請(qǐng)求。

限流分類

限流,拆分來(lái)看,就兩個(gè)字限和流,限就是動(dòng)詞限制,很好理解。但是流在不同的場(chǎng)景之下就是不同資源或指標(biāo),多樣性就在流中體現(xiàn)。在網(wǎng)絡(luò)流量中可以是字節(jié)流,在數(shù)據(jù)庫(kù)中可以是TPS,在API中可以是QPS亦可以是并發(fā)請(qǐng)求數(shù),在商品中還可以是庫(kù)存數(shù)... ...但是不管是哪一種『流』,這個(gè)流必須可以被量化,可以被度量,可以被觀察到、可以統(tǒng)計(jì)出來(lái)。我們把限流的分類基于不同的方式分為不同的類別,關(guān)注公眾號(hào)碼猿技術(shù)專欄獲取更多面試資源,如下圖。

程序員必知的限流方案有哪些

限流分類

因?yàn)槠邢?,本文只?huì)挑選幾個(gè)常見(jiàn)的類型分類進(jìn)行說(shuō)明。

限流粒度分類

根據(jù)限流的粒度分類:

  • 單機(jī)限流

  • 分布式限流

現(xiàn)狀的系統(tǒng)基本上都是分布式架構(gòu),單機(jī)的模式已經(jīng)很少了,這里說(shuō)的單機(jī)限流更加準(zhǔn)確一點(diǎn)的說(shuō)法是單服務(wù)節(jié)點(diǎn)限流。單機(jī)限流是指請(qǐng)求進(jìn)入到某一個(gè)服務(wù)節(jié)點(diǎn)后超過(guò)了限流閾值,服務(wù)節(jié)點(diǎn)采取了一種限流保護(hù)措施。

程序員必知的限流方案有哪些

單機(jī)限流示意圖

分布式限流狹義的說(shuō)法是在接入層實(shí)現(xiàn)多節(jié)點(diǎn)合并限流,比如NGINX+redis,分布式網(wǎng)關(guān)等,廣義的分布式限流是多個(gè)節(jié)點(diǎn)(可以為不同服務(wù)節(jié)點(diǎn))有機(jī)整合,形成整體的限流服務(wù)。

程序員必知的限流方案有哪些

分布式限流示意圖

單機(jī)限流防止流量壓垮服務(wù)節(jié)點(diǎn),缺乏對(duì)整體流量的感知。分布式限流適合做細(xì)粒度不同的限流控制,可以根據(jù)場(chǎng)景不同匹配不同的限流規(guī)則。與單機(jī)限流最大的區(qū)別,分布式限流需要中心化存儲(chǔ),常見(jiàn)的使用redis實(shí)現(xiàn)。引入了中心化存儲(chǔ),就需要解決以下問(wèn)題:

  • 數(shù)據(jù)一致性在限流模式中理想的模式為時(shí)間點(diǎn)一致性。時(shí)間點(diǎn)一致性的定義中要求所有數(shù)據(jù)組件的數(shù)據(jù)在任意時(shí)刻都是完全一致的,但是一般來(lái)說(shuō)信息傳播的速度最大是光速,其實(shí)并不能達(dá)到任意時(shí)刻一致,總有一定的時(shí)間不一致,對(duì)于我們CAP中的一致性來(lái)說(shuō)只要達(dá)到讀取到最新數(shù)據(jù)即可,達(dá)到這種情況并不需要嚴(yán)格的任意時(shí)間一致。這只能是理論當(dāng)中的一致性模型,可以在限流中達(dá)到線性一致性即可。

  • 時(shí)間一致性這里的時(shí)間一致性與上述的時(shí)間點(diǎn)一致性不一樣,這里就是指各個(gè)服務(wù)節(jié)點(diǎn)的時(shí)間一致性。一個(gè)集群有3臺(tái)機(jī)器,但是在某一個(gè)A/B機(jī)器的時(shí)間為Tue Dec 3 16:29:28 CST 2019,C為Tue Dec 3 16:29:28 CST 2019,那么它們的時(shí)間就不一致。那么使用ntpdate進(jìn)行同步也會(huì)存在一定的誤差,對(duì)于時(shí)間窗口敏感的算法就是誤差點(diǎn)。

  • 超時(shí)在分布式系統(tǒng)中就需要網(wǎng)絡(luò)進(jìn)行通信,會(huì)存在網(wǎng)絡(luò)抖動(dòng)問(wèn)題,或者分布式限流中間件壓力過(guò)大導(dǎo)致響應(yīng)變慢,甚至是超時(shí)時(shí)間閾值設(shè)置不合理,導(dǎo)致應(yīng)用服務(wù)節(jié)點(diǎn)超時(shí)了,此時(shí)是放行流量還是拒絕流量?

  • 性能與可靠性分布式限流中間件的資源總是有限的,甚至可能是單點(diǎn)的(寫(xiě)入單點(diǎn)),性能存在上限。如果分布式限流中間件不可用時(shí)候如何退化為單機(jī)限流模式也是一個(gè)很好的降級(jí)方案。

限流對(duì)象類型分類

按照對(duì)象類型分類:

  • 基于請(qǐng)求限流

  • 基于資源限流

基于請(qǐng)求限流,一般的實(shí)現(xiàn)方式有限制總量和限制QPS。限制總量就是限制某個(gè)指標(biāo)的上限,比如搶購(gòu)某一個(gè)商品,放量是10w,那么最多只能賣出10w件。微信的搶紅包,群里發(fā)一個(gè)紅包拆分為10個(gè),那么最多只能有10人可以搶到,第十一個(gè)人打開(kāi)就會(huì)顯示『手慢了,紅包派完了』。

程序員必知的限流方案有哪些

紅包搶完了

限制QPS,也是我們常說(shuō)的限流方式,只要在接口層級(jí)進(jìn)行,某一個(gè)接口只允許1秒只能訪問(wèn)100次,那么它的峰值QPS只能為100。限制QPS的方式最難的點(diǎn)就是如何預(yù)估閾值,如何定位閾值,下文中會(huì)說(shuō)到,關(guān)注公眾號(hào)碼猿技術(shù)專欄獲取更多面試資源。

基于資源限流是基于服務(wù)資源的使用情況進(jìn)行限制,需要定位到服務(wù)的關(guān)鍵資源有哪些,并對(duì)其進(jìn)行限制,如限制TCP連接數(shù)、線程數(shù)、內(nèi)存使用量等。限制資源更能有效地反映出服務(wù)當(dāng)前地清理,但與限制QPS類似,面臨著如何確認(rèn)資源的閾值為多少。這個(gè)閾值需要不斷地調(diào)優(yōu),不停地實(shí)踐才可以得到一個(gè)較為滿意地值。

限流算法分類

不論是按照什么維度,基于什么方式的分類,其限流的底層均是需要算法來(lái)實(shí)現(xiàn)。下面介紹實(shí)現(xiàn)常見(jiàn)的限流算法:

  • 計(jì)數(shù)器

  • 令牌桶算法

  • 漏桶算法

計(jì)數(shù)器

固定窗口計(jì)數(shù)器

計(jì)數(shù)限流是最為簡(jiǎn)單的限流算法,日常開(kāi)發(fā)中,我們說(shuō)的限流很多都是說(shuō)固定窗口計(jì)數(shù)限流算法,比如某一個(gè)接口或服務(wù)1s最多只能接收1000個(gè)請(qǐng)求,那么我們就會(huì)設(shè)置其限流為1000QPS。該算法的實(shí)現(xiàn)思路非常簡(jiǎn)單,維護(hù)一個(gè)固定單位時(shí)間內(nèi)的計(jì)數(shù)器,如果檢測(cè)到單位時(shí)間已經(jīng)過(guò)去就重置計(jì)數(shù)器為零。

程序員必知的限流方案有哪些

固定窗口計(jì)數(shù)器原理

其操作步驟:

  1. 時(shí)間線劃分為多個(gè)獨(dú)立且固定大小窗口;

  2. 落在每一個(gè)時(shí)間窗口內(nèi)的請(qǐng)求就將計(jì)數(shù)器加1;

  3. 如果計(jì)數(shù)器超過(guò)了限流閾值,則后續(xù)落在該窗口的請(qǐng)求都會(huì)被拒絕。但時(shí)間達(dá)到下一個(gè)時(shí)間窗口時(shí),計(jì)數(shù)器會(huì)被重置為0。

下面實(shí)現(xiàn)一個(gè)簡(jiǎn)單的代碼。

package limitimport (
   "sync/atomic"   "time")

type Counter struct {
   Count       uint64   // 初始計(jì)數(shù)器   Limit       uint64  // 單位時(shí)間窗口最大請(qǐng)求頻次   Interval    int64   // 單位ms   RefreshTime int64   // 時(shí)間窗口}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   }
}

測(cè)試代碼:

package limitimport (
   "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)
}

看了上面的邏輯,有沒(méi)有覺(jué)得固定窗口計(jì)數(shù)器很簡(jiǎn)單,對(duì),就是這么簡(jiǎn)單,這就是它的一個(gè)優(yōu)點(diǎn)實(shí)現(xiàn)簡(jiǎn)單。同時(shí)也存在兩個(gè)比較嚴(yán)重缺陷。試想一下,固定時(shí)間窗口1s限流閾值為100,但是前100ms,已經(jīng)請(qǐng)求來(lái)了99個(gè),那么后續(xù)的900ms只能通過(guò)一個(gè)了,就是一個(gè)缺陷,基本上沒(méi)有應(yīng)對(duì)突發(fā)流量的能力。第二個(gè)缺陷,在00:00:00這個(gè)時(shí)間窗口的后500ms,請(qǐng)求通過(guò)了100個(gè),在00:00:01這個(gè)時(shí)間窗口的前500ms還有100個(gè)請(qǐng)求通過(guò),對(duì)于服務(wù)來(lái)說(shuō)相當(dāng)于1秒內(nèi)請(qǐng)求量達(dá)到了限流閾值的2倍。

滑動(dòng)窗口計(jì)數(shù)器

滑動(dòng)時(shí)間窗口算法是對(duì)固定時(shí)間窗口算法的一種改進(jìn),這詞被大眾所知實(shí)在TCP的流量控制中。固定窗口計(jì)數(shù)器可以說(shuō)是滑動(dòng)窗口計(jì)數(shù)器的一種特例,滑動(dòng)窗口的操作步驟:

  1. 將單位時(shí)間劃分為多個(gè)區(qū)間,一般都是均分為多個(gè)小的時(shí)間段;

  2. 每一個(gè)區(qū)間內(nèi)都有一個(gè)計(jì)數(shù)器,有一個(gè)請(qǐng)求落在該區(qū)間內(nèi),則該區(qū)間內(nèi)的計(jì)數(shù)器就會(huì)加一;

  3. 每過(guò)一個(gè)時(shí)間段,時(shí)間窗口就會(huì)往右滑動(dòng)一格,拋棄最老的一個(gè)區(qū)間,并納入新的一個(gè)區(qū)間;

  4. 計(jì)算整個(gè)時(shí)間窗口內(nèi)的請(qǐng)求總數(shù)時(shí)會(huì)累加所有的時(shí)間片段內(nèi)的計(jì)數(shù)器,計(jì)數(shù)總和超過(guò)了限制數(shù)量,則本窗口內(nèi)所有的請(qǐng)求都被丟棄。

時(shí)間窗口劃分的越細(xì),并且按照時(shí)間"滑動(dòng)",這種算法避免了固定窗口計(jì)數(shù)器出現(xiàn)的上述兩個(gè)問(wèn)題。缺點(diǎn)是時(shí)間區(qū)間的精度越高,算法所需的空間容量就越大。

常見(jiàn)的實(shí)現(xiàn)方式主要有基于redis zset的方式和循環(huán)隊(duì)列實(shí)現(xiàn)?;趓edis zset可將Key為限流標(biāo)識(shí)ID,Value保持唯一,可以用UUID生成,Score 也記為同一時(shí)間戳,最好是納秒級(jí)的。使用redis提供的 ZADD、EXPIRE、ZCOUNT 和 zremrangebyscore 來(lái)實(shí)現(xiàn),并同時(shí)注意開(kāi)啟 Pipeline 來(lái)盡可能提升性能。實(shí)現(xiàn)很簡(jiǎn)單,但是缺點(diǎn)就是zset的數(shù)據(jù)結(jié)構(gòu)會(huì)越來(lái)越大。

漏桶算法

漏桶算法是水先進(jìn)入到漏桶里,漏桶再以一定的速率出水,當(dāng)流入水的數(shù)量大于流出水時(shí),多余的水直接溢出。把水換成請(qǐng)求來(lái)看,漏桶相當(dāng)于服務(wù)器隊(duì)列,但請(qǐng)求量大于限流閾值時(shí),多出來(lái)的請(qǐng)求就會(huì)被拒絕服務(wù)。漏桶算法使用隊(duì)列實(shí)現(xiàn),可以以固定的速率控制流量的訪問(wèn)速度,可以做到流量的“平整化”處理。

大家可以通過(guò)網(wǎng)上最流行的一張圖來(lái)理解。

程序員必知的限流方案有哪些

漏桶算法原理

漏桶算法實(shí)現(xiàn)步驟:

  1. 將每個(gè)請(qǐng)求放入固定大小的隊(duì)列進(jìn)行存儲(chǔ);

  2. 隊(duì)列以固定速率向外流出請(qǐng)求,如果隊(duì)列為空則停止流出;

  3. 如隊(duì)列滿了則多余的請(qǐng)求會(huì)被直接拒絕·

漏桶算法有一個(gè)明顯的缺陷:當(dāng)短時(shí)間內(nèi)有大量的突發(fā)請(qǐng)求時(shí),即使服務(wù)器負(fù)載不高,每個(gè)請(qǐng)求也都得在隊(duì)列中等待一段時(shí)間才能被響應(yīng)。

令牌桶算法

令牌桶算法的原理是系統(tǒng)會(huì)以一個(gè)恒定的速率往桶里放入令牌,而如果請(qǐng)求需要被處理,則需要先從桶里獲取一個(gè)令牌,當(dāng)桶里沒(méi)有令牌可取時(shí),則拒絕服務(wù)。從原理上看,令牌桶算法和漏桶算法是相反的,前者為“進(jìn)”,后者為“出”。

漏桶算法與令牌桶算法除了“方向”上的不同還有一個(gè)更加主要的區(qū)別:令牌桶算法限制的是平均流入速率(允許突發(fā)請(qǐng)求,只要有足夠的令牌,支持一次拿多個(gè)令牌),并允許一定程度突發(fā)流量;

令牌桶算法的實(shí)現(xiàn)步驟:

  1. 令牌以固定速率生成并放入到令牌桶中;

  2. 如果令牌桶滿了則多余的令牌會(huì)直接丟棄,當(dāng)請(qǐng)求到達(dá)時(shí),會(huì)嘗試從令牌桶中取令牌,取到了令牌的請(qǐng)求可以執(zhí)行;

  3. 如果桶空了,則拒絕該請(qǐng)求。

程序員必知的限流方案有哪些

令牌桶算法原理

四種策略該如何選擇?

  • 固定窗口:實(shí)現(xiàn)簡(jiǎn)單,但是過(guò)于粗暴,除非情況緊急,為了能快速止損眼前的問(wèn)題可以作為臨時(shí)應(yīng)急的方案。

  • 滑動(dòng)窗口:限流算法簡(jiǎn)單易實(shí)現(xiàn),可以應(yīng)對(duì)有少量突增流量場(chǎng)景。

  • 漏桶:對(duì)于流量絕對(duì)均勻有很強(qiáng)的要求,資源的利用率上不是極致,但其寬進(jìn)嚴(yán)出模式,保護(hù)系統(tǒng)的同時(shí)還留有部分余量,是一個(gè)通用性方案。

  • 令牌桶:系統(tǒng)經(jīng)常有突增流量,并盡可能的壓榨服務(wù)的性能。

怎么做限流?

不論使用上述的哪一種分類或者實(shí)現(xiàn)方式,系統(tǒng)都會(huì)面臨一個(gè)共同的問(wèn)題:如何確認(rèn)限流閾值。有人團(tuán)隊(duì)根據(jù)經(jīng)驗(yàn)先設(shè)定一個(gè)小的閾值,后續(xù)慢慢進(jìn)行調(diào)整;有的團(tuán)隊(duì)是通過(guò)進(jìn)行壓力測(cè)試后總結(jié)出來(lái)。這種方式的問(wèn)題在于壓測(cè)模型與線上環(huán)境不一定一致,接口的單壓不能反饋整個(gè)系統(tǒng)的狀態(tài),全鏈路壓測(cè)又難以真實(shí)反應(yīng)實(shí)際流量場(chǎng)景流量比例。

再換一個(gè)思路是通過(guò)壓測(cè)+各應(yīng)用監(jiān)控?cái)?shù)據(jù)。根據(jù)系統(tǒng)峰值的QPS與系統(tǒng)資源使用情況,進(jìn)行等水位放大預(yù)估限流閾值,問(wèn)題在于系統(tǒng)性能拐點(diǎn)未知,單純的預(yù)測(cè)不一定準(zhǔn)確甚至極大偏離真實(shí)場(chǎng)景。正如《Overload Control for Scaling WeChat Microservices》所說(shuō),在具有復(fù)雜依賴關(guān)系的系統(tǒng)中,對(duì)特定服務(wù)的進(jìn)行過(guò)載控制可能對(duì)整個(gè)系統(tǒng)有害或者服務(wù)的實(shí)現(xiàn)有缺陷。

希望后續(xù)可以出現(xiàn)一個(gè)更加AI的運(yùn)行反饋?zhàn)詣?dòng)設(shè)置限流閾值的系統(tǒng),可以根據(jù)當(dāng)前QPS、資源狀態(tài)、RT情況等多種關(guān)聯(lián)數(shù)據(jù)動(dòng)態(tài)地進(jìn)行過(guò)載保護(hù)。

不論是哪一種方式給出的限流閾值,系統(tǒng)都應(yīng)該關(guān)注以下幾點(diǎn):

  1. 運(yùn)行指標(biāo)狀態(tài),比如當(dāng)前服務(wù)的QPS、機(jī)器資源使用情況、數(shù)據(jù)庫(kù)的連接數(shù)、線程的并發(fā)數(shù)等;

  2. 資源間的調(diào)用關(guān)系,外部鏈路請(qǐng)求、內(nèi)部服務(wù)之間的關(guān)聯(lián)、服務(wù)之間的強(qiáng)弱依賴等;

  3. 控制方式,達(dá)到限流后對(duì)后續(xù)的請(qǐng)求直接拒絕、快速失敗、排隊(duì)等待等處理方式

go限流類庫(kù)使用

限流的類庫(kù)有很多,不同語(yǔ)言的有不同的類庫(kù),如大Java的有concurrency-limits、Sentinel、Guava 等,這些類庫(kù)都有很多的分析和使用方式了,本文主要介紹Golang的限流類庫(kù)就是Golang的擴(kuò)展庫(kù):

https://github.com/golang/time/rate

可以進(jìn)去語(yǔ)言類庫(kù)的代碼都值得去研讀一番,學(xué)習(xí)過(guò)Java的同學(xué)是否對(duì)AQS的設(shè)計(jì)之精妙而感嘆呢! time/rate 也有其精妙的部分,下面開(kāi)始進(jìn)入類庫(kù)學(xué)習(xí)階段。

github.com/golang/time/rate

進(jìn)行源碼分析前的,最應(yīng)該做的是了解類庫(kù)的使用方式、使用場(chǎng)景和API。對(duì)業(yè)務(wù)有了初步的了解,閱讀代碼就可以事半功倍。因?yàn)槠邢藓罄m(xù)的博文在對(duì)多個(gè)限流類庫(kù)源碼做分析。

類庫(kù)的API文檔:

https://godoc.org/golang.org/x/time/rate%E3%80%82

time/rate類庫(kù)是基于令牌桶算法實(shí)現(xiàn)的限流功能。前面說(shuō)令牌桶算法的原理是系統(tǒng)會(huì)以一個(gè)恒定的速率往桶里放入令牌,那么桶就有一個(gè)固定的大小,往桶中放入令牌的速率也是恒定的,并且允許突發(fā)流量。查看文檔發(fā)現(xiàn)一個(gè)函數(shù):

func NewLimiter(r Limit, b int) *Limiter

newLimiter返回一個(gè)新的限制器,它允許事件的速率達(dá)到r,并允許最多突發(fā)b個(gè)令牌。也就是說(shuō)Limter限制時(shí)間的發(fā)生頻率,但這個(gè)桶一開(kāi)始容量就為b,并且裝滿b個(gè)令牌(令牌池中最多有b個(gè)令牌,所以一次最多只能允許b個(gè)事件發(fā)生,一個(gè)事件花費(fèi)掉一個(gè)令牌),然后每一個(gè)單位時(shí)間間隔(默認(rèn)1s)往桶里放入r個(gè)令牌。

limter := rate.NewLimiter(10, 5)

上面的例子表示,令牌桶的容量為5,并且每一秒中就往桶里放入10個(gè)令牌。細(xì)心的讀者都會(huì)發(fā)現(xiàn)函數(shù)NewLimiter第一個(gè)參數(shù)是Limit類型,可以看源碼就會(huì)發(fā)現(xiàn)Limit實(shí)際上就是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

限流器還可以指定往桶里放入令牌的時(shí)間間隔,實(shí)現(xiàn)方式如下:

limter := rate.NewLimiter(rate.Every(100*time.Millisecond), 5)

這兩個(gè)例子的效果是一樣的,使用第一種方式不會(huì)出現(xiàn)在每一秒間隔一下子放入10個(gè)令牌,也是均勻分散在100ms的間隔放入令牌。rate.Limiter提供了三類方法用來(lái)限速:

  • Allow/AllowN

  • Wait/WaitN

  • Reserve/ReserveN

下面對(duì)比這三類限流方式的使用方式和適用場(chǎng)景。先看第一類方法:

func (lim *Limiter) Allow() boolfunc (lim *Limiter) AllowN(now time.Time, n int) bool

Allow 是AllowN(time.Now(), 1)的簡(jiǎn)化方法。那么重點(diǎn)就在方法 AllowN上了,API的解釋有點(diǎn)抽象,說(shuō)得云里霧里的,可以看看下面的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.

實(shí)際上就是為了說(shuō),方法 AllowN在指定的時(shí)間時(shí)是否可以從令牌桶中取出N個(gè)令牌。也就意味著可以限定N個(gè)事件是否可以在指定的時(shí)間同時(shí)發(fā)生。這個(gè)兩個(gè)方法是無(wú)阻塞,也就是說(shuō)一旦不滿足,就會(huì)跳過(guò),不會(huì)等待令牌數(shù)量足夠才執(zhí)行。

也就是文檔中的第二行解釋,如果打算丟失或跳過(guò)超出速率限制的時(shí)間,那么久請(qǐng)使用該方法。比如使用之前實(shí)例化好的限流器,在某一個(gè)時(shí)刻,服務(wù)器同時(shí)收到超過(guò)了8個(gè)請(qǐng)求,如果令牌桶內(nèi)令牌小于8個(gè),那么這8個(gè)請(qǐng)求就會(huì)被丟棄。一個(gè)小示例:

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      }
   }
}

執(zhí)行結(jié)果:

1 ====Allow====== 2019-12-14 15:54:09.9852178 +0800 CST m=+0.0059980012 ====Allow====== 2019-12-14 15:54:10.1012231 +0800 CST m=+0.1220033013 ====Allow====== 2019-12-14 15:54:10.1823056 +0800 CST m=+0.2030858014 ====Allow====== 2019-12-14 15:54:10.263238 +0800 CST m=+0.2840182015 ====Allow====== 2019-12-14 15:54:10.344224 +0800 CST m=+0.3650042016 ====Allow====== 2019-12-14 15:54:10.4242458 +0800 CST m=+0.4450260017 ====Allow====== 2019-12-14 15:54:10.5043101 +0800 CST m=+0.5250903018 ====Allow====== 2019-12-14 15:54:10.5852232 +0800 CST m=+0.6060034019 ====Disallow====== 2019-12-14 15:54:10.6662181 +0800 CST m=+0.68699830110 ====Disallow====== 2019-12-14 15:54:10.7462189 +0800 CST m=+0.76699910111 ====Allow====== 2019-12-14 15:54:10.8272182 +0800 CST m=+0.84799840112 ====Disallow====== 2019-12-14 15:54:10.9072192 +0800 CST m=+0.92799940113 ====Allow====== 2019-12-14 15:54:10.9872224 +0800 CST m=+1.00800260114 ====Disallow====== 2019-12-14 15:54:11.0672253 +0800 CST m=+1.08800550115 ====Disallow====== 2019-12-14 15:54:11.1472946 +0800 CST m=+1.168074801

第二類方法:因?yàn)镽eserveN比較復(fù)雜,第二類先說(shuō)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)的簡(jiǎn)化方法。與AllowN不同的是WaitN會(huì)阻塞,如果令牌桶內(nèi)的令牌數(shù)不足N個(gè),WaitN會(huì)阻塞一段時(shí)間,阻塞時(shí)間的時(shí)長(zhǎng)可以用第一個(gè)參數(shù)ctx進(jìn)行設(shè)置,把 context 實(shí)例為context.WithDeadline或context.WithTimeout進(jìn)行制定阻塞的時(shí)長(zhǎng)。

func WaitNDemo() {
   limter := rate.NewLimiter(10, 5)
   i := 0   for {
      i++
      ctx, canle := context.WithTimeout(context.Background(), 400*time.Millisecond)
      if i == 6 {
         // 取消執(zhí)行         canle()
      }
      err := limter.WaitN(ctx, 4)

      if err != nil {
         fmt.Println(err)
         continue      }
      fmt.Println(i, ",執(zhí)行:", time.Now())
      if i == 10 {
         return      }
   }
}

執(zhí)行結(jié)果:

1 ,執(zhí)行:2019-12-14 15:45:15.538539 +0800 CST m=+0.0110234012 ,執(zhí)行:2019-12-14 15:45:15.8395195 +0800 CST m=+0.3120039013 ,執(zhí)行:2019-12-14 15:45:16.2396051 +0800 CST m=+0.7120895014 ,執(zhí)行:2019-12-14 15:45:16.6395169 +0800 CST m=+1.1120013015 ,執(zhí)行:2019-12-14 15:45:17.0385893 +0800 CST m=+1.511073701context canceled7 ,執(zhí)行:2019-12-14 15:45:17.440514 +0800 CST m=+1.9129984018 ,執(zhí)行:2019-12-14 15:45:17.8405152 +0800 CST m=+2.3129996019 ,執(zhí)行:2019-12-14 15:45:18.2405402 +0800 CST m=+2.71302460110 ,執(zhí)行:2019-12-14 15:45:18.6405179 +0800 CST m=+3.113002301

適用于允許阻塞等待的場(chǎng)景,比如消費(fèi)消息隊(duì)列的消息,可以限定最大的消費(fèi)速率,過(guò)大了就會(huì)被限流避免消費(fèi)者負(fù)載過(guò)高。

第三類方法:

func (lim *Limiter) Reserve() *Reservationfunc (lim *Limiter) ReserveN(now time.Time, n int) *Reservation

與之前的兩類方法不同的是Reserve/ReserveN返回了Reservation實(shí)例。Reservation在API文檔中有5個(gè)方法:

func (r *Reservation) Cancel() // 相當(dāng)于CancelAt(time.Now())func (r *Reservation) CancelAt(now time.Time)func (r *Reservation) Delay() time.Duration // 相當(dāng)于DelayFrom(time.Now())func (r *Reservation) DelayFrom(now time.Time) time.Durationfunc (r *Reservation) OK() bool

通過(guò)這5個(gè)方法可以讓開(kāi)發(fā)者根據(jù)業(yè)務(wù)場(chǎng)景進(jìn)行操作,相比前兩類的自動(dòng)化,這樣的操作顯得復(fù)雜多了。通過(guò)一個(gè)小示例來(lái)學(xué)習(xí)Reserve/ReserveN:

func ReserveNDemo() {
   limter := rate.NewLimiter(10, 5)
   i := 0   for {
      i++
      reserve := limter.ReserveN(time.Now(), 4)
      // 如果為flase說(shuō)明拿不到指定數(shù)量的令牌,比如需要的令牌數(shù)大于令牌桶容量的場(chǎng)景      if !reserve.OK() {
         return      }
      ts := reserve.Delay()
      time.Sleep(ts)
      fmt.Println("執(zhí)行:", time.Now(),ts)
      if i == 10 {
         return      }
   }
}

執(zhí)行結(jié)果:

執(zhí)行:2019-12-14 16:22:26.6446468 +0800 CST m=+0.008000201 0s執(zhí)行:2019-12-14 16:22:26.9466454 +0800 CST m=+0.309998801 247.999299ms執(zhí)行:2019-12-14 16:22:27.3446473 +0800 CST m=+0.708000701 398.001399ms執(zhí)行:2019-12-14 16:22:27.7456488 +0800 CST m=+1.109002201 399.999499ms執(zhí)行:2019-12-14 16:22:28.1456465 +0800 CST m=+1.508999901 398.997999ms執(zhí)行:2019-12-14 16:22:28.5456457 +0800 CST m=+1.908999101 399.0003ms執(zhí)行:2019-12-14 16:22:28.9446482 +0800 CST m=+2.308001601 399.001099ms執(zhí)行:2019-12-14 16:22:29.3446524 +0800 CST m=+2.708005801 399.998599ms執(zhí)行:2019-12-14 16:22:29.7446514 +0800 CST m=+3.108004801 399.9944ms執(zhí)行:2019-12-14 16:22:30.1446475 +0800 CST m=+3.508000901 399.9954ms

如果在執(zhí)行Delay()之前操作Cancel()那么返回的時(shí)間間隔就會(huì)為0,意味著可以立即執(zhí)行操作,不進(jìn)行限流。

func ReserveNDemo2() {
   limter := rate.NewLimiter(5, 5)
   i := 0   for {
      i++
      reserve := limter.ReserveN(time.Now(), 4)
      // 如果為flase說(shuō)明拿不到指定數(shù)量的令牌,比如需要的令牌數(shù)大于令牌桶容量的場(chǎng)景      if !reserve.OK() {
         return      }

      if i == 6 || i == 5 {
         reserve.Cancel()
      }
      ts := reserve.Delay()
      time.Sleep(ts)
      fmt.Println(i, "執(zhí)行:", time.Now(), ts)
      if i == 10 {
         return      }
   }
}

執(zhí)行結(jié)果:

1 執(zhí)行:2019-12-14 16:25:45.7974857 +0800 CST m=+0.007005901 0s2 執(zhí)行:2019-12-14 16:25:46.3985135 +0800 CST m=+0.608033701 552.0048ms3 執(zhí)行:2019-12-14 16:25:47.1984796 +0800 CST m=+1.407999801 798.9722ms4 執(zhí)行:2019-12-14 16:25:47.9975269 +0800 CST m=+2.207047101 799.0061ms5 執(zhí)行:2019-12-14 16:25:48.7994803 +0800 CST m=+3.009000501 799.9588ms6 執(zhí)行:2019-12-14 16:25:48.7994803 +0800 CST m=+3.009000501 0s7 執(zhí)行:2019-12-14 16:25:48.7994803 +0800 CST m=+3.009000501 0s8 執(zhí)行:2019-12-14 16:25:49.5984782 +0800 CST m=+3.807998401 798.0054ms9 執(zhí)行:2019-12-14 16:25:50.3984779 +0800 CST m=+4.607998101 799.0075ms10 執(zhí)行:2019-12-14 16:25:51.1995131 +0800 CST m=+5.409033301 799.0078ms

看到這里time/rate的限流方式已經(jīng)完成,除了上述的三類限流方式,time/rate還提供了動(dòng)態(tài)調(diào)整限流器參數(shù)的功能。相關(guān)API如下:

func (lim *Limiter) SetBurst(newBurst int) // 相當(dāng)于SetBurstAt(time.Now(), newBurst).func (lim *Limiter) SetBurstAt(now time.Time, newBurst int)// 重設(shè)令牌桶的容量func (lim *Limiter) SetLimit(newLimit Limit) // 相當(dāng)于SetLimitAt(time.Now(), newLimit)func (lim *Limiter) SetLimitAt(now time.Time, newLimit Limit)// 重設(shè)放入令牌的速率

這四個(gè)方法可以讓程序根據(jù)自身的狀態(tài)動(dòng)態(tài)的調(diào)整令牌桶速率和令牌桶容量。

到此,關(guān)于“程序員必知的限流方案有哪些”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識(shí),請(qǐng)繼續(xù)關(guān)注億速云網(wǎng)站,小編會(huì)繼續(xù)努力為大家?guī)?lái)更多實(shí)用的文章!

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI