限流,永遠都不是一件簡單的事!

背景

隨着微服務的流行,服務之間的穩定性變得愈加重要,每每咱們會花不少經歷在維護服務的穩定性上,限流和熔斷降級是咱們最經常使用的兩個手段。前段時間在羣裏有些小夥伴對限流的使用些疑問,再加上最近公司大促也作了限流相關的事,因此在這裏總結一下寫寫本身對限流的一些見解。前端

剛纔說了限流是咱們保證服務穩定性的手段之一,可是他並非全部場景的穩定性都能保證,和他名字同樣他只能在大流量或者突發流量的場景下才能發揮出本身的做用。好比咱們的系統最高支持100QPS,可是忽然有1000QPS請求打了進來,可能這個時候系統就會直接掛掉,致使後面一個請求都處理不了,可是若是咱們有限流的手段,不管他有多大的QPS,咱們都只處理100QPS的請求,其餘請求都直接拒絕掉,雖然有900的QPS的請求咱們拒絕掉了,可是咱們的系統沒有掛掉,咱們系統仍然能夠不斷的處理後續的請求,這個是咱們所指望的。有同窗可能會說,如今都上的雲了,服務的動態伸縮應該是特別簡單的吧,若是咱們發現流量特別大的時候,自動擴容機器到能夠支撐目標QPS那不就不須要限流了嗎?其實有這個想法的同窗應該還挺多的,有些同窗可能被一些吹牛的文章給唬到了,因此纔會這麼想,這個想法在特別理想化的時候是能夠實現的,可是在現實中其實有下面幾個問題:java

  • 擴容是須要時間。擴容簡單來講就是搞一個新的機器,而後從新發布代碼,作java的同窗應該是知道發佈成功一個代碼的時間通常不是以秒級計算,而是以分鐘級別計算,有時候你擴容完成,說不定流量尖峯都過去了。web

  • 擴容到多少是個特別複雜的問題。擴容幾臺機器這個是比較複雜的,須要大量的壓測計算,以及整條鏈路上的一個擴容,若是擴容了你這邊的機器以後,其餘團隊的機器沒有擴容可能最後仍是有瓶頸這個也是一個問題。redis

因此單純的擴容是解決不了這個問題的,限流仍然是咱們必須掌握的技能!算法

基本原理

想要掌握好限流,就須要先掌握他的一些基本算法,限流的算法基本上分爲三種,計數器,漏斗,令牌桶,其餘的一些都是在這些基礎上進行演變而來。數據庫

計數器算法

首先咱們來講一下計數器算法,這個算法比較簡單粗暴,咱們只須要一個累加變量,而後每隔一秒鐘去刷新這個累加變量,而後再判斷這個累加變量是否大於咱們的最大QPS。編程

    int curQps = 0;
    long lastTime = System.currentTimeMillis();
    int maxQps = 100;
    Object lock = new Object();
    boolean check(){
        synchronized (lock){
            long now = System.currentTimeMillis();
            if (now - lastTime > 1000){
                lastTime = now;
                curQps = 0;
            }
            curQps++;
            if (curQps > maxQps){
                return false;
            }
        }
        return true;
    }

這個代碼比較簡單,咱們定義了當前的qps,以及上一次刷新累加變量的時間,還有咱們的最大qps和咱們的lock鎖,咱們每次檢查的時候,都須要判斷是否須要刷新,若是須要刷新那麼須要把時間和qps都進行重置,而後再進行qps的累加判斷。緩存

這個算法由於太簡單了因此帶來的問題也是特別明顯,若是咱們最大的qps是100,在0.99秒的時候來了100個請求,而後在1.01秒的時候又來了100個請求,這個是能夠經過咱們的程序的,可是咱們其實在0.03秒以內經過了200個請求,這個確定不符合咱們的預期,由於頗有可能這200個請求直接就會將咱們機器給打掛。tomcat

滑動窗口計數器

爲了解決上面的臨界的問題,咱們這裏可使用滑動窗口來解決這個問題:
服務器


如上圖所示,咱們將1s的普通計數器,分紅了5個200ms,咱們統計的當前qps都須要統計最近的5個窗口的全部qps,再回到剛纔的問題,0.99秒和1.01秒其實都在咱們的最近5個窗口以內,因此這裏不會出現剛纔的臨界的突刺問題。


其實換個角度想,咱們普通的計數器其實就是窗口數量爲1的滑動窗口計數器,只要咱們分的窗口越多,咱們使用計數器方案的時候統計就會越精確,可是相對來講維護的窗口的成本就會增長,等會咱們介紹sentinel的時候會詳細介紹他是怎麼實現滑動窗口計數的。

漏斗算法

解決計數器中臨界的突刺問題也能夠經過漏斗算法來實現,以下圖所示:

在漏斗算法中咱們須要關注漏桶和勻速流出,不論流量有多大都會先到漏桶中,而後以均勻的速度流出。如何在代碼中實現這個勻速呢?好比咱們想讓勻速爲100q/s,那麼咱們能夠獲得每流出一個流量須要消耗10ms,相似一個隊列,每隔10ms從隊列頭部取出流量進行放行,而咱們的隊列也就是漏桶,當流量大於隊列的長度的時候,咱們就能夠拒絕超出的部分。

漏斗算法一樣的也有必定的缺點:沒法應對突發流量(和上面的臨界突刺不同,不要混淆)。好比一瞬間來了100個請求,在漏桶算法中只能一個一個的過去,當最後一個請求流出的時候時間已通過了一秒了,因此漏斗算法比較適合請求到達比較均勻,須要嚴格控制請求速率的場景。

令牌桶算法

爲了解決突發流量狀況,咱們可使用令牌桶算法,以下圖所示:


這個圖上須要關注三個階段:


  • 生產令牌:咱們在這裏一樣的仍是假設最大qps是100,那麼咱們從漏斗的每10ms過一個流量轉化成每10ms生產一個令牌,直到達到最大令牌。

  • 消耗令牌:咱們每個流量都會消耗令牌桶,這裏的消耗的規則能夠多變,既能夠是簡單的每一個流量消耗一個令牌,又能夠是根據不一樣的流量數據包大小或者流量類型來進行不一樣的消耗規則,好比查詢的流量消耗1個令牌,寫入的流量消耗2個令牌。

  • 判斷是否經過:若是令牌桶足夠那麼咱們就容許流量經過,若是不足夠能夠等待或者直接拒絕,這個就能夠採用漏斗那種用隊列來控制。

單機限流

上面咱們已經介紹了限流的一些基本算法,咱們把這些算法應用到咱們的分佈式服務中又能夠分爲兩種,一個是單機限流,一個是集羣限流。單機限流指的是每臺機器各自作本身的限流,互不影響。咱們接下來看看單機限流怎麼去實現呢?

guava

guava是谷歌開源的java核心工具庫,裏面包括集合,緩存,併發等好用的工具,固然也提供了咱們這裏所須要的的限流的工具,核心類就是RateLimiter。

//  RateLimiter rateLimiter = RateLimiter.create(100, 500, TimeUnit.MILLISECONDS); 預熱的rateLimit
    RateLimiter rateLimiter = RateLimiter.create(100); // 簡單的rateLimit
    boolean limitResult = rateLimiter.tryAcquire();

使用方式比較簡單,如上面代碼所示,咱們只須要構建一個RateLimiter,而後再調用tryAcquire方法,若是返回爲true表明咱們此時流量經過,相反則被限流。在guava中RateLimiter也分爲兩種,一個是普通的令牌桶算法的實現,還有一個是帶有預熱的RateLimiter,可讓咱們令牌桶的釋放速度逐步增長直到最大,這個帶有預熱的在sentinel也有,這個能夠在一些冷系統中好比數據庫鏈接池沒有徹底填滿,還在不斷初始化的場景下使用。

在這裏只簡單的介紹一下guava的令牌桶怎麼去實現的

普通的令牌桶建立了一個SmoothBursty的類,這個類也就是咱們實現限流的關鍵,具體怎麼作限流的在咱們的tryAcquire中:


這裏分爲四步:


  • Step1: 加上一個同步鎖,須要注意一下這裏在sentinel中並無加鎖這個環節,在guava中是有這個的,後續也會將sentinel的一些問題。

  • Step2: 判斷是否能申請令牌桶,若是桶內沒有足夠的令牌而且等待時間超過咱們的timeout,這裏咱們就不進行申請了。

  • Step3: 申請令牌並獲取等待時間,在咱們tryAcquire中的timeout參數就是就是咱們的最大等待時間,若是咱們只是調用tryAcquire(),不會出現等待,第二步的時候已經快速失敗了。

  • Step4: sleep等待的時間。

扣除令牌的方法具體在reserverEarliestAvailable方法中:

這裏雖然看起來過程比較多,可是若是咱們只是調用tryAcquire(),就只須要關注兩個紅框:

  • Step1: 根據當前最新時間發放token,在guava中沒有采用使用其餘線程異步發放token的方式,把token的更新放在了咱們每次調用限流方法中,這個設計能夠值得學習一下,不少時候不必定須要異步線程去執行也能夠達到咱們想要的目的,而且也沒有異步線程的複雜。

  • Step2: 扣除令牌,這裏咱們已經在canAcquire中校驗過了,令牌必定能扣除成功。

guava的限流目前就提供了這兩種方式的限流,不少中間件或者業務服務都把guava的限流做爲本身的工具,可是guava的方式比較侷限,動態改變限流,以及更多策略的限流都不支持,因此咱們接下來介紹一下sentinel。

sentinel

sentinel是阿里巴巴開源的分佈式服務框架的輕量級流量控制框架,承接了阿里巴巴近 10 年的雙十一大促流量的核心場景,他的核心是流量控制可是不侷限於流量控制,還支持熔斷降級,監控等等。

使用sentinel的限流稍微比guava複雜不少,下面寫了一個最簡單的代碼:

        String KEY = "test";
        // ============== 初始化規則 =========
        List<FlowRule> rules = new ArrayList<FlowRule>();
        FlowRule rule1 = new FlowRule();
        rule1.setResource(KEY);
        // set limit qps to 20
        rule1.setCount(20);
        rule1.setGrade(RuleConstant.FLOW_GRADE_QPS);
        rule1.setLimitApp("default");
        rules.add(rule1);
        rule1.setControlBehavior(CONTROL_BEHAVIOR_DEFAULT);
        FlowRuleManager.loadRules(rules);
        // ================ 限流斷定 ===========
        Entry entry = null;

        try {
            entry = SphU.entry(KEY);
            // do something

        } catch (BlockException e1) {
            // 限流會拋出BlockException 異常
        }finally {
            if (entry != null) {
                entry.exit();
            }
        }
  • Step1:在sentinel中比較強調Resource這個概念,咱們所保護的或者說所做用於都是基於Resource來講,因此咱們首先須要肯定咱們的Resource的key,這裏咱們簡單的設置爲test了。

  • Step2:而後咱們初始化咱們這個Resource的一個限流規則,咱們這裏選擇的是針對QPS限流而且策略選擇的是默認,這裏默認的話就是使用的滑動窗口版的計數器,而後加載到全局的規則管理器裏面,整個規則的設置和guava的差異比較大。

  • Step3: 在sentinel第二個比較重要的概念就是Entry,Entry表示一次資源操做,內部會保存當前invocation信息,在finally的時候須要對entry進行退出。咱們執行限流斷定的時候實際上也就是獲取Entry,SphU.entry也就是咱們執行咱們上面限流規則的關鍵,這裏和guava不同若是被限流了,就會拋出BlockException,咱們在進行限流的處理。

雖然sentinel的使用總體比guava複雜不少,可是算法的可選比guava的限流也多一點。

基於併發數(線程數)

咱們以前介紹的都是基於QPS的,在sentinel中提供了基於併發數的策略,效果相似於信號量隔離,當咱們須要讓業務線程池不被慢調用耗盡,咱們就可使用這種模式。

一般來講咱們同一個服務提供的http接口都是使用的一個線程池,好比咱們使用的tomcat-web服務器那麼咱們就會有個tomcat的業務線程池,若是在http中有兩個方法A和B,B的速度相對來講比較快,A的速度相對來講比較慢,若是大量的調用A這個方法,因爲A的速度太慢,線程得不到釋放,有可能致使線程池被耗盡,另外一個方法B就得不到線程。這個場景咱們以前有遇到過直接致使整個服務所接收的請求所有被拒絕。有的同窗說限制A的QPS不是就能夠了嗎,要注意的是QPS是每秒的,若是咱們這個A接口的耗時大於1s,那麼下一波A來了以後QPS是要從新計算的。

基於這個就提供了基於併發數的限流,咱們設置Grade爲FLOW_GRADE_THREAD,就能夠實現這個限流模式。

基於QPS

基於QPS的限流sentinel也提供了4種策略:

  • 默認策略:設置Behavior爲CONTROL_BEHAVIOR_DEFAULT,這個模式是滑動窗口計數器模式。這種方式適用於對系統處理能力確切已知的狀況下,好比經過壓測肯定了系統的準確水位時。

  • Warm Up:設置爲Behavior爲CONTROL_BEHAVIOR_WARM_UP,相似以前guava中介紹的warmup。預熱啓動方式。當系統長期處於低水位的狀況下,當流量忽然增長時,直接把系統拉昇到高水位可能瞬間把系統壓垮。這個模式下QPS的曲線圖以下:

  • 勻速排隊:設置Behavior爲CONTROL_BEHAVIOR_RATE_LIMITER,這個模式其實就是漏斗算法,優缺點以前也講解過了

  • Warm Up + 勻速排隊:設置Behavior爲CONTROL_BEHAVIOR_WARM_UP_RATE_LIMITER,以前warm up到高水位以後使用的是滑動窗口的算法限流,這個模式下繼續使用勻速排隊的算法。

基於調用關係

sentinel提供了更爲複雜的一種限流,能夠基於調用關係去作更爲靈活的限流:

  • 根據調用方限流:調用方的限流使用比較複雜,須要調用ContextUtil.enter(resourceName, origin),origin就是咱們的調用方標識,而後在咱們的rule設置參數的時候,對limitApp進行設置就能夠進行對調用方的限流:

    • 設置爲default,默認對全部調用方都限流。

    • 設置爲{some_origin_name},表明對特定的調用者才限流。

    • 設置爲other,會對配置的一個referResource參數表明的調用者除外的進行限流。

  • 關聯流量控制:在sentinel中也支持,兩個有關聯的資源能夠互相影響流量控制,好比有兩個接口都使用的是同一個資源,一個接口比較重要,另一個接口不是那麼重要,咱們能夠設置一個規則當重要的接口大量訪問的時候,就能夠對另一個不重要接口進行限流,防止這個接口忽然出現流量影響重要的接口。

sentinel的一些問題

sentinel雖然提供了這麼多算法,可是也有一些問題:

  • 首先來講sentinel上手比較難,對比guava的兩行代碼來講,使用sentinel須要瞭解一些名詞,而後針對這些名詞再來使用,雖然sentinel提供了一些註解來幫助咱們簡化使用,可是總體來講仍是比guava要複雜。

  • sentinel有必定的運維成本,sentinel的使用每每須要搭建sentinel的server後臺,對比guava的開箱即用來講,有必定的運維成本。

  • sentinel的限流統計有必定的併發問題,在sentinel的源碼中是沒有加鎖的地方的,極端狀況下若是qps限制的是10,若是有100個同時過限流的邏輯,這個時候都會經過,而guava不會發生這樣的狀況。

這些問題基本上都是和guava的限流來比較的,畢竟sentinel的功能更多,付出的成本相對來講也會更多。

集羣限流

以前說的全部限流都是單機限流,可是咱們如今都是微服務集羣的架構模式,一般一個服務會有多臺機器,好比有一個訂單服務,這個服務有10臺機器,那麼咱們想作整個集羣限流到500QPS,咱們應該怎麼去作呢?這個很簡單,直接每臺機器都限流50就行了,50*10就是500,可是在現實環境中會出現負載不均衡的狀況,在微服務調用的時候負載均衡的算法多種多樣,好比同機房優先,輪訓,隨機等算法,這些算法都有可能致使咱們的負載不是特別的均衡,就會致使咱們整個集羣的QPS可能有沒有500,甚至在400的時候就被限流了,這個是咱們真實場景中所遇到過的。既然單機限流有問題,那麼咱們應該設計一個更加完善的集羣限流的方案

redis

這個方案不依賴限流的框架,咱們整個集羣使用同一個redis便可,須要本身封裝一下限流的邏輯,這裏咱們使用最簡單的計數器去設計,咱們將咱們的系統時間以秒爲單位做爲key,設置到redis裏面(能夠設置必定的過時時間用於空間清理),利用redis的int原子加法,每來一個請求都進行+1,而後再判斷當前值是否超過咱們限流的最大值。

redis的方案實現起來總體來講比較簡單,可是強依賴咱們的系統時間,若是不一樣機器之間的系統時間有誤差限流就有可能不許確。

sentinel

在sentinel中提供了集羣的解決方案,這個對比其餘的一些限流框架是比較有特點的。在sentinel中提供了兩種模式:

  • 獨立模式:限流服務做爲單獨的server進行部署,以下圖所示,全部的應用都向單獨部署的token-server進行獲取token,這種模式適用於跨服務之間的全侷限流,好比下面圖中,A和B都會去token-server去拿,這個場景通常來講比較少,更多的仍是服務內集羣的限流比較多。

  • 內嵌模式:在內嵌模式下,咱們會把server部署到咱們應用實例中,咱們也能夠經過接口轉換咱們的server-client身份,固然咱們能夠本身引入一些zk的一些邏輯設置讓咱們的leader去當server,機器掛了也能夠自動切換。這種比較適合同一個服務集羣之間的限流,靈活性比較好,可是要注意的是大量的token-server的訪問也有可能影響咱們本身的機器。

固然sentinel也有一些兜底的策略,若是token-server掛了咱們能夠退化成咱們單機限流的模式,不會影響咱們正常的服務。

實戰

咱們上面已經介紹了不少限流的工具,可是不少同窗對怎麼去限流仍然比較迷惑。咱們若是對一個場景或者一個資源作限流的話有下面幾個點須要確認一下:

  • 什麼地方去作限流

  • 限多少流

  • 怎麼去選擇工具

什麼地方去作限流

這個問題比較複雜,不少公司以及不少團隊的作法都不相同,在美團的時候搞了一波SOA,那個時候我記得全部的服務全部的接口都須要作限流,叫每一個團隊去給接口評估一個合理的QPS上限,這樣作理論上來講是對的,咱們每一個接口都應該給與一個上限,防止把總體系統拖垮,可是這樣作的成本是很是之高的,因此大部分公司仍是選擇性的去作限流。

首先咱們須要肯定一些核心的接口,好比電商系統中的下單,支付,若是流量過大那麼電商系統中的路徑就有問題,好比像對帳這種邊緣的接口(不影響核心路徑),咱們能夠不設置限流。

其次咱們不必定只在接口層才作限流,不少時候咱們直接在網關層把限流作了,防止流量進一步滲透到核心系統中。固然前端也能作限流,當前端捕獲到限流的錯誤碼以後,前端能夠提示等待信息,這個其實也算是限流的一部分。其實當限流越在下游觸發咱們的資源的浪費就越大,由於在下游限流以前上游已經作了不少工做了,若是這時候觸發限流那麼以前的工做就會白費,若是涉及到一些回滾的工做還會加大咱們的負擔,因此對於限流來講應該是越上層觸發越好。

限多少流

限多少流這個問題大部分的時候可能就是一個歷史經驗值,咱們能夠經過平常的qps監控圖,而後再在這個接觸上加一點冗餘的QPS可能這個就是咱們的限流了。可是有一個場景須要注意,那就是大促(這裏指的是電商系統裏面的場景,其餘系統類比流量較高的場景)的時候,咱們系統的流量就會突增,不再是咱們平常的QPS了,這種狀況下,每每須要咱們在大促以前給咱們系統進行全鏈路壓測,壓測出一個合理的上限,而後限流就基於這個上限去設置。

怎麼去選擇工具

通常來講大一點的互聯網公司都有本身的統一限流的工具這裏直接採用就好。對於其餘狀況的話,若是沒有集羣限流或者熔斷這些需求,我我的以爲選擇RateLimter是一個比較不錯的選擇,應該其使用比較簡單,基本沒有學習成本,若是有其餘的一些需求我我的以爲選擇sentinel,至於hytrix的話我我的不推薦使用,由於這個已經再也不維護了。

總結

限流雖然只有兩個字,可是真正要理解限流,作好限流,是一件很是不容易的事,對於我我的而已,這篇文章也只是一些淺薄的見識,若是你們有什麼更好的意見能夠關注個人公衆號留言進行討論。



推薦閱讀:


喜歡我能夠給我設爲星標哦

好文章,我 「在看」

本文分享自微信公衆號 - 漫話編程(mhcoding)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索