<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          限流,傷害不大但效果極強(qiáng)

          共 20152字,需瀏覽 41分鐘

           ·

          2021-04-19 21:38

          作者 | qiangmzsx

          來源 http://r6d.cn/ac844

          限流簡介

          現(xiàn)在說到高可用系統(tǒng),都會說到高可用的保護(hù)手段:緩存、降級和限流,本博文就主要說說限流。限流是流量限速(Rate Limit)的簡稱,是指只允許指定的事件進(jìn)入系統(tǒng),超過的部分將被拒絕服務(wù)、排隊(duì)或等待、降級等處理。對于server服務(wù)而言,限流為了保證一部分的請求流量可以得到正常的響應(yī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è)概念才容易被混淆。

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

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

          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>
            

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

          • X-Rate-Limit-Limit:同一個(gè)時(shí)間段所允許的請求的最大數(shù)目;
          • X-Rate-Limit-Remaining:在當(dāng)前時(shí)間段內(nèi)剩余的請求的數(shù)量;
          • X-Rate-Limit-Reset:為了得到最大請求數(shù)所等待的秒數(shù)。

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

          限流分類

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

          限流分類

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

          限流粒度分類

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

          • 單機(jī)限流
          • 分布式限流

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

          單機(jī)限流示意圖

          分布式限流狹義的說法是在接入層實(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),缺乏對整體流量的感知。分布式限流適合做細(xì)粒度不同的限流控制,可以根據(jù)場景不同匹配不同的限流規(guī)則。與單機(jī)限流最大的區(qū)別,分布式限流需要中心化存儲,常見的使用redis實(shí)現(xiàn)。引入了中心化存儲,就需要解決以下問題:

          • 數(shù)據(jù)一致性

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

          • 時(shí)間一致性

            這里的時(shí)間一致性與上述的時(shí)間點(diǎn)一致性不一樣,這里就是指各個(gè)服務(wù)節(jié)點(diǎn)的時(shí)間一致性。一個(gè)集群有3臺機(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)行同步也會存在一定的誤差,對于時(shí)間窗口敏感的算法就是誤差點(diǎn)。

          • 超時(shí)

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

          • 性能與可靠性

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

          限流對象類型分類

          按照對象類型分類:

          • 基于請求限流
          • 基于資源限流

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

          紅包搶完了

          限制QPS,也是我們常說的限流方式,只要在接口層級進(jìn)行,某一個(gè)接口只允許1秒只能訪問100次,那么它的峰值QPS只能為100。限制QPS的方式最難的點(diǎn)就是如何預(yù)估閾值,如何定位閾值,下文中會說到。

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

          限流算法分類

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

          • 計(jì)數(shù)器
          • 令牌桶算法
          • 漏桶算法

          計(jì)數(shù)器

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

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

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

          其操作步驟:

          1. 時(shí)間線劃分為多個(gè)獨(dú)立且固定大小窗口;
          2. 落在每一個(gè)時(shí)間窗口內(nèi)的請求就將計(jì)數(shù)器加1;
          3. 如果計(jì)數(shù)器超過了限流閾值,則后續(xù)落在該窗口的請求都會被拒絕。但時(shí)間達(dá)到下一個(gè)時(shí)間窗口時(shí),計(jì)數(shù)器會被重置為0。

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

          package limit

          import (
             "sync/atomic"
             "time"
          )

          type Counter struct {
             Count       uint64   // 初始計(jì)數(shù)器
             Limit       uint64  // 單位時(shí)間窗口最大請求頻次
             Interval    int64   // 單位ms
             RefreshTime int64   // 時(shí)間窗口
          }

          func NewCounter(count, limit uint64, interval, rt int64) *Counter {
             return &amp;Counter{
                Count:       count,
                Limit:       limit,
                Interval:    interval,
                RefreshTime: rt,
             }
          }

          func (c *Counter) RateLimit() bool {
             now := time.Now().UnixNano() / 1e6
             if now &lt; (c.RefreshTime + c.Interval) {
                atomic.AddUint64(&amp;c.Count, 1)
                return c.Count &lt;= c.Limit
             } else {
                c.RefreshTime = now
                atomic.AddUint64(&amp;c.Count, -c.Count)
                return true
             }
          }

          測試代碼:

          package limit

          import (
             "fmt"
             "testing"
             "time"
          )

          func Test_Counter(t *testing.T) {
             counter := NewCounter(05100, time.Now().Unix())
             for i := 0; i &lt; 10; i++ {
                go func(i int) {
                   for k := 0; k &lt;= 10; k++ {
                      fmt.Println(counter.RateLimit())
                      if k%3 == 0 {
                         time.Sleep(102 * time.Millisecond)
                      }
                   }
                }(i)
             }
             time.Sleep(10 * time.Second)
          }

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

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

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

          1. 將單位時(shí)間劃分為多個(gè)區(qū)間,一般都是均分為多個(gè)小的時(shí)間段;
          2. 每一個(gè)區(qū)間內(nèi)都有一個(gè)計(jì)數(shù)器,有一個(gè)請求落在該區(qū)間內(nèi),則該區(qū)間內(nèi)的計(jì)數(shù)器就會加一;
          3. 每過一個(gè)時(shí)間段,時(shí)間窗口就會往右滑動(dòng)一格,拋棄最老的一個(gè)區(qū)間,并納入新的一個(gè)區(qū)間;
          4. 計(jì)算整個(gè)時(shí)間窗口內(nèi)的請求總數(shù)時(shí)會累加所有的時(shí)間片段內(nèi)的計(jì)數(shù)器,計(jì)數(shù)總和超過了限制數(shù)量,則本窗口內(nèi)所有的請求都被丟棄。

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

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

          漏桶算法

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

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

          漏桶算法原理

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

          1. 將每個(gè)請求放入固定大小的隊(duì)列進(jìn)行存儲;
          2. 隊(duì)列以固定速率向外流出請求,如果隊(duì)列為空則停止流出;
          3. 如隊(duì)列滿了則多余的請求會被直接拒絕·

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

          令牌桶算法

          令牌桶算法的原理是系統(tǒng)會以一個(gè)恒定的速率往桶里放入令牌,而如果請求需要被處理,則需要先從桶里獲取一個(gè)令牌,當(dāng)桶里沒有令牌可取時(shí),則拒絕服務(wù)。從原理上看,令牌桶算法和漏桶算法是相反的,前者為“進(jìn)”,后者為“出”。漏桶算法與令牌桶算法除了“方向”上的不同還有一個(gè)更加主要的區(qū)別:令牌桶算法限制的是平均流入速率(允許突發(fā)請求,只要有足夠的令牌,支持一次拿多個(gè)令牌),并允許一定程度突發(fā)流量;

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

          1. 令牌以固定速率生成并放入到令牌桶中;
          2. 如果令牌桶滿了則多余的令牌會直接丟棄,當(dāng)請求到達(dá)時(shí),會嘗試從令牌桶中取令牌,取到了令牌的請求可以執(zhí)行;
          3. 如果桶空了,則拒絕該請求。
          令牌桶算法原理

          四種策略該如何選擇?

          • 固定窗口:實(shí)現(xiàn)簡單,但是過于粗暴,除非情況緊急,為了能快速止損眼前的問題可以作為臨時(shí)應(yīng)急的方案。
          • 滑動(dòng)窗口:限流算法簡單易實(shí)現(xiàn),可以應(yīng)對有少量突增流量場景。
          • 漏桶:對于流量絕對均勻有很強(qiáng)的要求,資源的利用率上不是極致,但其寬進(jìn)嚴(yán)出模式,保護(hù)系統(tǒng)的同時(shí)還留有部分余量,是一個(gè)通用性方案。
          • 令牌桶:系統(tǒng)經(jīng)常有突增流量,并盡可能的壓榨服務(wù)的性能。

          怎么做限流?

          不論使用上述的哪一種分類或者實(shí)現(xiàn)方式,系統(tǒng)都會面臨一個(gè)共同的問題:如何確認(rèn)限流閾值。有人團(tuán)隊(duì)根據(jù)經(jīng)驗(yàn)先設(shè)定一個(gè)小的閾值,后續(xù)慢慢進(jìn)行調(diào)整;有的團(tuán)隊(duì)是通過進(jìn)行壓力測試后總結(jié)出來。這種方式的問題在于壓測模型與線上環(huán)境不一定一致,接口的單壓不能反饋整個(gè)系統(tǒng)的狀態(tài),全鏈路壓測又難以真實(shí)反應(yīng)實(shí)際流量場景流量比例。再換一個(gè)思路是通過壓測+各應(yīng)用監(jiān)控?cái)?shù)據(jù)。根據(jù)系統(tǒng)峰值的QPS與系統(tǒng)資源使用情況,進(jìn)行等水位放大預(yù)估限流閾值,問題在于系統(tǒng)性能拐點(diǎn)未知,單純的預(yù)測不一定準(zhǔn)確甚至極大偏離真實(shí)場景。正如《Overload Control for Scaling WeChat Microservices》所說,在具有復(fù)雜依賴關(guān)系的系統(tǒng)中,對特定服務(wù)的進(jìn)行過載控制可能對整個(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)行過載保護(hù)。

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

          1. 運(yùn)行指標(biāo)狀態(tài),比如當(dāng)前服務(wù)的QPS、機(jī)器資源使用情況、數(shù)據(jù)庫的連接數(shù)、線程的并發(fā)數(shù)等;
          2. 資源間的調(diào)用關(guān)系,外部鏈路請求、內(nèi)部服務(wù)之間的關(guān)聯(lián)、服務(wù)之間的強(qiáng)弱依賴等;
          3. 控制方式,達(dá)到限流后對后續(xù)的請求直接拒絕、快速失敗、排隊(duì)等待等處理方式

          go限流類庫使用

          限流的類庫有很多,不同語言的有不同的類庫,如大Java的有concurrency-limits、Sentinel、Guava 等,這些類庫都有很多的分析和使用方式了,本文主要介紹Golang的限流類庫就是Golang的擴(kuò)展庫:https://github.com/golang/time/rate 。可以進(jìn)去語言類庫的代碼都值得去研讀一番,學(xué)習(xí)過Java的同學(xué)是否對AQS的設(shè)計(jì)之精妙而感嘆呢!time/rate 也有其精妙的部分,下面開始進(jìn)入類庫學(xué)習(xí)階段。

          github.com/golang/time/rate

          進(jìn)行源碼分析前的,最應(yīng)該做的是了解類庫的使用方式、使用場景和API。對業(yè)務(wù)有了初步的了解,閱讀代碼就可以事半功倍。因?yàn)槠邢藓罄m(xù)的博文在對多個(gè)限流類庫源碼做分析。類庫的API文檔:https://godoc.org/golang.org/x/time/rate。time/rate類庫是基于令牌桶算法實(shí)現(xiàn)的限流功能。前面說令牌桶算法的原理是系統(tǒng)會以一個(gè)恒定的速率往桶里放入令牌,那么桶就有一個(gè)固定的大小,往桶中放入令牌的速率也是恒定的,并且允許突發(fā)流量。查看文檔發(fā)現(xiàn)一個(gè)函數(shù):

          func NewLimiter(r Limit, b int) *Limiter

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

          limter := rate.NewLimiter(105)

          上面的例子表示,令牌桶的容量為5,并且每一秒中就往桶里放入10個(gè)令牌。細(xì)心的讀者都會發(fā)現(xiàn)函數(shù)NewLimiter第一個(gè)參數(shù)是Limit類型,可以看源碼就會發(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è)例子的效果是一樣的,使用第一種方式不會出現(xiàn)在每一秒間隔一下子放入10個(gè)令牌,也是均勻分散在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)的簡化方法。那么重點(diǎn)就在方法 AllowN上了,API的解釋有點(diǎn)抽象,說得云里霧里的,可以看看下面的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í)際上就是為了說,方法 AllowN在指定的時(shí)間時(shí)是否可以從令牌桶中取出N個(gè)令牌。也就意味著可以限定N個(gè)事件是否可以在指定的時(shí)間同時(shí)發(fā)生。這個(gè)兩個(gè)方法是無阻塞,也就是說一旦不滿足,就會跳過,不會等待令牌數(shù)量足夠才執(zhí)行。也就是文檔中的第二行解釋,如果打算丟失或跳過超出速率限制的時(shí)間,那么久請使用該方法。比如使用之前實(shí)例化好的限流器,在某一個(gè)時(shí)刻,服務(wù)器同時(shí)收到超過了8個(gè)請求,如果令牌桶內(nèi)令牌小于8個(gè),那么這8個(gè)請求就會被丟棄。一個(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.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

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

          func WaitNDemo() {
             limter := rate.NewLimiter(105)
             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.011023401
          2 ,執(zhí)行:2019-12-14 15:45:15.8395195 +0800 CST m=+0.312003901
          3 ,執(zhí)行:2019-12-14 15:45:16.2396051 +0800 CST m=+0.712089501
          4 ,執(zhí)行:2019-12-14 15:45:16.6395169 +0800 CST m=+1.112001301
          5 ,執(zhí)行:2019-12-14 15:45:17.0385893 +0800 CST m=+1.511073701
          context canceled
          7 ,執(zhí)行:2019-12-14 15:45:17.440514 +0800 CST m=+1.912998401
          8 ,執(zhí)行:2019-12-14 15:45:17.8405152 +0800 CST m=+2.312999601
          9 ,執(zhí)行:2019-12-14 15:45:18.2405402 +0800 CST m=+2.713024601
          10 ,執(zhí)行:2019-12-14 15:45:18.6405179 +0800 CST m=+3.113002301

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

          第三類方法:

          func (lim *Limiter) Reserve() *Reservation
          func (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.Duration
          func (r *Reservation) OK() bool

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

          func ReserveNDemo() {
             limter := rate.NewLimiter(105)
             i := 0
             for {
                i++
                reserve := limter.ReserveN(time.Now(), 4)
                // 如果為flase說明拿不到指定數(shù)量的令牌,比如需要的令牌數(shù)大于令牌桶容量的場景
                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í)間間隔就會為0,意味著可以立即執(zhí)行操作,不進(jìn)行限流。

          func ReserveNDemo2() {
             limter := rate.NewLimiter(55)
             i := 0
             for {
                i++
                reserve := limter.ReserveN(time.Now(), 4)
                // 如果為flase說明拿不到指定數(shù)量的令牌,比如需要的令牌數(shù)大于令牌桶容量的場景
                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 0s
          2 執(zhí)行:2019-12-14 16:25:46.3985135 +0800 CST m=+0.608033701 552.0048ms
          3 執(zhí)行:2019-12-14 16:25:47.1984796 +0800 CST m=+1.407999801 798.9722ms
          4 執(zhí)行:2019-12-14 16:25:47.9975269 +0800 CST m=+2.207047101 799.0061ms
          5 執(zhí)行:2019-12-14 16:25:48.7994803 +0800 CST m=+3.009000501 799.9588ms
          6 執(zhí)行:2019-12-14 16:25:48.7994803 +0800 CST m=+3.009000501 0s
          7 執(zhí)行:2019-12-14 16:25:48.7994803 +0800 CST m=+3.009000501 0s
          8 執(zhí)行:2019-12-14 16:25:49.5984782 +0800 CST m=+3.807998401 798.0054ms
          9 執(zhí)行:2019-12-14 16:25:50.3984779 +0800 CST m=+4.607998101 799.0075ms
          10 執(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)整令牌桶速率和令牌桶容量。

          結(jié)尾

          通過上述一系列講解,相信大家對各個(gè)限流的應(yīng)用場景和優(yōu)缺點(diǎn)也有了大致的掌握,希望在日常開發(fā)中有所幫助。限流僅僅是整個(gè)服務(wù)治理中的一個(gè)小環(huán)節(jié),需要與多種技術(shù)結(jié)合使用,才可以更好的提升服務(wù)的穩(wěn)定性的同時(shí)提高用戶體驗(yàn)。

          附錄

          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

          瀏覽 44
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  免费看按摩AAAAAA片 | 俺去也亚洲地区 | 中国四川一级大毛片a一 | 在线一级二级免费观看视频 | 最新国产成人小电影 |