<ul id="qxxfc"><fieldset id="qxxfc"><tr id="qxxfc"></tr></fieldset></ul>


      前言

      分布式環(huán)境下應(yīng)對高并發(fā)保證服務(wù)穩(wěn)定幾招,按照個人理解,優(yōu)先級從高到低分別為緩存、限流、降級、熔斷,每招都有它的作用,本文重點就講講限流這部分。

      坦白講,其實上面的說法也不準確,因為服務(wù)降級、熔斷本身也是限流的一種
      ,因為它們本質(zhì)上也是阻斷了流量進來,但是本文希望大家可以把限流當做一個單純的名詞來理解,看一下對請求做流控的幾種算法及具體實現(xiàn)方式。

      ?

      為什么要限流

      其實很好理解的一個問題,為什么要限流,自然就流量過大了唄,一個對外服務(wù)有很多場景都會流量增大:

      * 業(yè)務(wù)用戶量不斷攀升
      * 各種促銷
      * 網(wǎng)絡(luò)爬蟲
      * 惡意刷單

      注意這個"大",1000QPS大嗎?5000QPS大嗎?10000QPS大么?沒有答案,因為沒有標準,因此,"大"一定是和正常流量相比的大。流量一大,服務(wù)器扛不住,扛不住就掛了,掛了沒法提供對外服務(wù)導(dǎo)致業(yè)務(wù)直接熔斷。怎么辦,最直接的辦法就是從源頭把流量限制下來,例如服務(wù)器只有支撐1000QPS的處理能力,那就每秒放1000個請求,自然保證了服務(wù)器的穩(wěn)定,這就是限流。

      下面看一下常見的兩種限流算法。

      ?

      漏桶算法

      漏桶算法的原理比較簡單,水(請求)先進入到漏桶里,人為設(shè)置一個最大出水速率,漏桶以<=出水速率的速度出水,當水流入速度過大會直接溢出(拒絕服務(wù)):



      因此,這個算法的核心為:

      * 存下請求
      * 勻速處理
      * 多于丟棄
      因此這是一種強行限制請求速率的方式,但是缺點非常明顯,主要有兩點:

      * 無法面對突發(fā)的大流量----
      比如請求處理速率為1000,容量為5000,來了一波2000/s的請求持續(xù)10s,那么后5s的請求將全部直接被丟棄,服務(wù)器拒絕服務(wù),但是實際上網(wǎng)絡(luò)中突發(fā)一波大流量尤其是短時間的大流量是非常正常的,超過容量就拒絕,非常簡單粗暴
      *
      無法有效利用網(wǎng)絡(luò)資源----比如雖然服務(wù)器的處理能力是1000/s,但這不是絕對的,這個1000只是一個宏觀服務(wù)器處理能力的數(shù)字,實際上一共5秒,每秒請求量分別為1200、1300、1200、500、800,平均下來qps也是1000/s,但是這個量對服務(wù)器來說完全是可以接受的,但是因為限制了速率是1000/s,因此前面的三秒,每秒只能處理掉1000個請求而一共打回了700個請求,白白浪費了服務(wù)器資源
      所以,通常來說利用漏桶算法來限流,實際場景下用得不多。

      ?

      令牌桶算法

      令牌桶算法是網(wǎng)絡(luò)流量整形(Traffic Shaping)和限流(Rate
      Limiting)中最常使用的一種算法,它可用于控制發(fā)送到網(wǎng)絡(luò)上數(shù)據(jù)的數(shù)量并允許突發(fā)數(shù)據(jù)的發(fā)送。

      從某種意義上來說,令牌桶算法是對漏桶算法的一種改進,主要在于令牌桶算法能夠在限制調(diào)用的平均速率的同時還允許一定程度的突發(fā)調(diào)用,來看下令牌桶算法的實現(xiàn)原理:



      整個的過程是這樣的:

      * 系統(tǒng)以恒定的速率產(chǎn)生令牌,然后將令牌放入令牌桶中
      * 令牌桶有一個容量,當令牌桶滿了的時候,再向其中放入的令牌就會被丟棄
      * 每次一個請求過來,需要從令牌桶中獲取一個令牌,假設(shè)有令牌,那么提供服務(wù);假設(shè)沒有令牌,那么拒絕服務(wù)

      那么,我們再看一下,為什么令牌桶算法可以防止一定程度的突發(fā)流量呢?可以這么理解,假設(shè)我們想要的速率是1000QPS,那么往桶中放令牌的速度就是1000個/s,假設(shè)第1秒只有800個請求,那意味著第2秒可以容許1200個請求,這就是
      一定程度突發(fā)流量的意思,反之我們看漏桶算法,第一秒只有800個請求,那么全部放過,第二秒這1200個請求將會被打回200個。


      注意上面多次提到一定程度這四個字,這也是我認為令牌桶算法最需要注意的一個點。假設(shè)還是1000QPS的速率,那么5秒鐘放1000個令牌,第1秒鐘800個請求過來,第2~4秒沒有請求,那么按照令牌桶算法,第5秒鐘可以接受4200個請求,但是實際上這已經(jīng)遠遠超出了系統(tǒng)的承載能力,因此使用令牌桶算法特別注意
      設(shè)置桶中令牌的上限即可。

      總而言之,作為對漏桶算法的改進,令牌桶算法在限流場景下被使用更加廣泛。

      ?

      RateLimiter使用

      上面說了令牌桶算法在限流場景下被使用更加廣泛,接下來我們看一下代碼示例,模擬一下每秒最多過五個請求:
      public class RateLimiterTest { private static final SimpleDateFormat FORMATTER
      =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); private static final int
      THREAD_COUNT = 25; @Test public void testRateLimiter1() { RateLimiter
      rateLimiter= RateLimiter.create(5); Thread[] ts = new Thread[THREAD_COUNT]; for
      (int i = 0; i < THREAD_COUNT; i++) { ts[i] = new Thread(new
      RateLimiterThread(rateLimiter), "RateLimiterThread-" + i); } for (int i = 0; i
      < THREAD_COUNT; i++) { ts[i].start(); } for (;;); } public class
      RateLimiterThreadimplements Runnable { private RateLimiter rateLimiter; public
      RateLimiterThread(RateLimiter rateLimiter) {this.rateLimiter = rateLimiter; }
      @Overridepublic void run() { rateLimiter.acquire(1);
      System.out.println(Thread.currentThread().getName()+ "獲取到了令牌,時間 = " +
      FORMATTER.format(new Date())); } } }

      利用RateLimiter.create這個構(gòu)造方法可以指定每秒向桶中放幾個令牌,比方說上面的代碼create(5),那么每秒放置5個令牌,即200ms會向令牌桶中放置一個令牌。這邊代碼寫了一條線程模擬實際場景,拿到令牌那么就能執(zhí)行下面邏輯,看一下代碼執(zhí)行結(jié)果:
      RateLimiterThread-0獲取到了令牌,時間 = 2019-08-25 20:58:53 RateLimiterThread
      -23獲取到了令牌,時間 = 2019-08-25 20:58:54 RateLimiterThread-21獲取到了令牌,時間 = 2019-08-25
      20:58:54 RateLimiterThread-19獲取到了令牌,時間 = 2019-08-25 20:58:54 RateLimiterThread
      -17獲取到了令牌,時間 = 2019-08-25 20:58:54 RateLimiterThread-13獲取到了令牌,時間 = 2019-08-25
      20:58:54 RateLimiterThread-9獲取到了令牌,時間 = 2019-08-25 20:58:55 RateLimiterThread
      -15獲取到了令牌,時間 = 2019-08-25 20:58:55 RateLimiterThread-5獲取到了令牌,時間 = 2019-08-25
      20:58:55 RateLimiterThread-1獲取到了令牌,時間 = 2019-08-25 20:58:55 RateLimiterThread
      -11獲取到了令牌,時間 = 2019-08-25 20:58:55 RateLimiterThread-7獲取到了令牌,時間 = 2019-08-25
      20:58:56 RateLimiterThread-3獲取到了令牌,時間 = 2019-08-25 20:58:56 RateLimiterThread
      -4獲取到了令牌,時間 = 2019-08-25 20:58:56 RateLimiterThread-8獲取到了令牌,時間 = 2019-08-25
      20:58:56 RateLimiterThread-12獲取到了令牌,時間 = 2019-08-25 20:58:56 RateLimiterThread
      -16獲取到了令牌,時間 = 2019-08-25 20:58:57 RateLimiterThread-20獲取到了令牌,時間 = 2019-08-25
      20:58:57 RateLimiterThread-24獲取到了令牌,時間 = 2019-08-25 20:58:57 RateLimiterThread
      -2獲取到了令牌,時間 = 2019-08-25 20:58:57 RateLimiterThread-6獲取到了令牌,時間 = 2019-08-25
      20:58:57 RateLimiterThread-10獲取到了令牌,時間 = 2019-08-25 20:58:58 RateLimiterThread
      -14獲取到了令牌,時間 = 2019-08-25 20:58:58 RateLimiterThread-18獲取到了令牌,時間 = 2019-08-25
      20:58:58 RateLimiterThread-22獲取到了令牌,時間 = 2019-08-25 20:58:58
      看到,非常標準,在每次消耗一個令牌的情況下,RateLimiter可以保證每一秒內(nèi)最多只有5個線程獲取到令牌
      ,使用這種方式可以很好的做單機對請求的QPS數(shù)控制。

      至于為什么2019-08-25
      20:58:53這個時間點只有1條線程獲取到了令牌而不是有5條線程獲取到令牌,因為RateLimiter是按照秒計數(shù)的,可能第一個線程是2019-08-25
      20:58:53.999秒來的,算在2019-08-25 20:58:53這一秒內(nèi);下一個線程2019-08-25
      20:58:54.001秒來,自然就算到2019-08-25 20:58:54這一秒去了。

      上面的寫法是RateLimiter最常用的寫法,注意:

      * acquire是阻塞的且會一直等待到獲取令牌為止,它有一個返回值為double型,意思是從阻塞開始到獲取到令牌的等待時間,單位為秒
      *
      tryAcquire是另外一個方法,它可以指定超時時間,返回值為boolean型,即假設(shè)線程等待了指定時間后仍然沒有獲取到令牌,那么就會返回給客戶端false,客戶端根據(jù)自身情況是打回給前臺錯誤還是定時重試
      ?

      RateLimiter預(yù)消費

      處理請求,每次來一個請求就acquire一把是RateLimiter最常見的用法,但是我們看acquire還有個acquire(int
      permits)的重載方法,即允許每次獲取多個令牌數(shù)。這也是有可能的,請求數(shù)是一個大維度每次扣減1,有可能服務(wù)器按照字節(jié)數(shù)來進行限流,例如每秒最多處理10000字節(jié)的數(shù)據(jù),那每次扣減的就不止1了。

      接著我們再看一段代碼示例:
      @Test public void testRateLimiter2() { RateLimiter rateLimiter =
      RateLimiter.create(1); System.out.println("獲取1個令牌開始,時間為" + FORMATTER.format(new
      Date()));double cost = rateLimiter.acquire(1); System.out.println(
      "獲取1個令牌結(jié)束,時間為" + FORMATTER.format(new Date()) + ", 耗時" + cost + "ms");
      System.out.println("獲取5個令牌開始,時間為" + FORMATTER.format(new Date())); cost =
      rateLimiter.acquire(5); System.out.println("獲取5個令牌結(jié)束,時間為" + FORMATTER.format(new
      Date()) + ", 耗時" + cost + "ms"); System.out.println("獲取3個令牌開始,時間為" +
      FORMATTER.format(new Date())); cost = rateLimiter.acquire(3);
      System.out.println("獲取3個令牌結(jié)束,時間為" + FORMATTER.format(new Date()) + ", 耗時" +
      cost + "ms"); }
      代碼運行結(jié)果為:
      獲取1個令牌開始,時間為2019-08-25 21:21:09.973 獲取1個令牌結(jié)束,時間為2019-08-25 21:21:09.976,
      耗時0.0ms 獲取5個令牌開始,時間為2019-08-25 21:21:09.976 獲取5個令牌結(jié)束,時間為2019-08-25 21:21:10.974
      , 耗時0.997237ms 獲取3個令牌開始,時間為2019-08-25 21:21:10.976 獲取3個令牌結(jié)束,時間為2019-08-25
      21:21:15.974, 耗時4.996529ms


      看到這就是標題所說的預(yù)消費能力,也是RateLimiter中允許一定程度突發(fā)流量的實現(xiàn)方式。第二次需要獲取5個令牌,指定的是每秒放1個令牌到桶中,我們發(fā)現(xiàn)實際上并沒有等5秒鐘等桶中積累了5個令牌才能讓第二次acquire成功,而是直接等了1秒鐘就成功了。我們可以捋一捋這個邏輯:

      * 第一次請求過來需要獲取1個令牌,直接拿到
      * RateLimiter在1秒鐘后放一個令牌,第一次請求預(yù)支的1個令牌還上了
      * 1秒鐘之后第二次請求過來需要獲得5個令牌,直接拿到
      * RateLimiter在花了5秒鐘放了5個令牌,還上了第二次請求預(yù)支的5個令牌
      * 第三個請求在5秒鐘之后拿到3個令牌
      也就是說,前面的請求如果流量大于每秒放置令牌的數(shù)量,那么允許處理,但是帶來的結(jié)果就是后面的請求延后處理,從而在整體上達到一個平衡整體處理速率的效果。


      突發(fā)流量的處理,在令牌桶算法中有兩種方式,一種是有足夠的令牌才能消費,一種是先消費后還令牌。后者就像我們0首付買車似的,30萬的車很少有等攢到30萬才全款買的,先簽了相關(guān)合同把車子給你,然后貸款慢慢還,這樣就爽了。RateLimiter也是同樣的道理,先讓請求得到處理,再慢慢還上預(yù)支的令牌,客戶端同樣也爽了,否則我假設(shè)預(yù)支60個令牌,1分鐘之后才能處理我的請求,不合理也不人性化。

      ?

      RateLimiter的限制

      特別注意RateLimiter是單機的,也就是說它無法跨JVM使用,設(shè)置的1000QPS,那也在單機中保證平均1000QPS的流量。

      假設(shè)集群中部署了10臺服務(wù)器,想要保證集群1000QPS的接口調(diào)用量,那么RateLimiter就不適用了,集群流控最常見的方法是使用強大的Redis:

      * 一種是固定窗口的計數(shù),例如當前是2019/8/26 20:05:00,就往這個"2019/8/26
      20:05:00"這個key進行incr,當前是2019/8/26 20:05:01,就往"2019/8/26
      20:05:01"這個key進行incr,incr后的結(jié)果只要大于我們設(shè)定的值,那么就打回去,小于就相當于獲取到了執(zhí)行權(quán)限
      * 一種是結(jié)合lua腳本,實現(xiàn)分布式的令牌桶算法,網(wǎng)上實現(xiàn)還是比較多的,可以參考
      https://blog.csdn.net/sunlihuo/article/details/79700225
      <https://blog.csdn.net/sunlihuo/article/details/79700225>這篇文章
      總得來說,集群限流的實現(xiàn)也比較簡單。

      ?

      總結(jié)

      本文主要寫了常見的兩種限流算法漏桶算法與令牌桶算法,并且演示了Guava中RateLimiter的實現(xiàn),相信看到這里的朋友一定都懂了,恭喜你們!

      令牌桶算法是最常用的限流算法,它最大的特點就是容許一定程度的突發(fā)流量。

      漏桶算法同樣也有自己的應(yīng)用之處,例如Nginx的限流模塊就是基于漏桶算法的,它最大的特點就是強行限制流量按照指定的比例下發(fā)
      ,適合那種對流量有絕對要求的場景,就是流量可以容許在我指定的值之下,可以被多次打回,但是無論如何決不能超過指定的。

      雖然令牌桶算法相對更好,但是還是我經(jīng)常說的,使用哪種完全就看大家各自的場景,適合的才是最好的。

      ?

      友情鏈接
      ioDraw流程圖
      API參考文檔
      OK工具箱
      云服務(wù)器優(yōu)惠
      阿里云優(yōu)惠券
      騰訊云優(yōu)惠券
      京東云優(yōu)惠券
      站點信息
      問題反饋
      郵箱:[email protected]
      QQ群:637538335
      關(guān)注微信

        <ul id="qxxfc"><fieldset id="qxxfc"><tr id="qxxfc"></tr></fieldset></ul>
          乱伦一级黄色视频 | 欧美成人在线视频 | 日本高清黄页免费网站大全 | 久久综合五月天婷婷丁香 | 爆操jk| 天天草天天日天天干 | 亚洲欧美suv精品8888日 | 免费 无码 无套内谢软件 | 公交车上荫蒂添的好舒服的电影 | 公交车后挺进老师 |