隨着微服務的流行,服務和服務之間的穩定性變得愈來愈重要。緩存、降級和限流是保護微服務系統運行穩定性的三大利器。前端
緩存的目的是提高系統訪問速度和增大系統能處理的容量,而降級是當服務出問題或者影響到核心流程的性能則須要暫時屏蔽掉,待高峯或者問題解決後再打開,而有些場景並不能用緩存和降級來解決,好比稀缺資源、數據庫的寫操做、頻繁的複雜查詢,所以需有一種手段來限制這些場景的請求量,即限流。java
好比當咱們設計了一個函數,準備上線,這時候這個函數會消耗一些資源,處理上限是1秒服務3000個QPS,但若是實際狀況遇到高於3000的QPS該如何解決呢?git
因此限流的目的應當是經過對併發訪問/請求進行限速或者一個時間窗口內的的請求進行限速來保護系統,一旦達到限制速率就能夠拒絕服務、等待、降級。github
學習如何去實現一個分佈式限流框架,首先,咱們須要去了解最基本的兩種限流算法。redis
漏桶算法思路很簡單,水(也就是請求)先進入到漏桶裏,漏桶以必定的速度出水,當水流入速度過大會直接溢出,而後就拒絕請求,能夠看出漏桶算法能強行限制數據的傳輸速率。示意圖(來源網絡)以下:算法
令牌桶算法和漏桶算法效果同樣但方向相反的算法,更加容易理解。隨着時間流逝,系統會按恆定1/QPS時間間隔(若是QPS=100,則間隔是10ms)往桶裏加入令牌(想象和漏洞漏水相反,有個水龍頭在不斷的加水),若是桶已經滿了就再也不加了。新請求來臨時,會各自拿走一個令牌,若是沒有令牌可拿了就阻塞或者拒絕服務。示意圖(來源網絡)以下:數據庫
漏桶算法與令牌桶算法的區別在於,漏桶算法可以強行限制數據的傳輸速率,令牌桶算法可以在限制數據的平均傳輸速率的同時還容許某種程度的突發狀況。令牌桶還有一個好處是能夠方便的改變速度。一旦須要提升速率,則按需提升放入桶中的令牌的速率。因此,限流框架的核心算法仍是以令牌桶算法爲主。數組
已知上面講述的令牌桶算法的原理,如何經過代碼實現?緩存
本地限流的實現能夠用Long長整型做爲令牌桶,爲了達到無鎖,建議使用Long的原子類型AtomicLong,使用AtomicLong的好處就是能夠很是方便的對其進行CAS加操做與CAS減操做(也就是令牌桶令牌的放入與拿取),以免線程的上下文切換的開銷,核心CAS算法以下:服務器
private boolean tryAcquireFailed() { long l = bucket.longValue(); while (l > 0) { if (bucket.compareAndSet(l, l - 1)) { return true; } l = bucket.longValue(); } return false; }
根據上述瞭解的令牌桶算法能夠得知,令牌桶須要一個ScheduledThread不斷的放入令牌,這部分的代碼以下:
ScheduledThreadExecutor.scheduleAtFixedRate(() -> bucket.set(rule.getLimit()), rule.getInitialDelay(), rule.getPeriod(), rule.getUnit() );
分佈式限流須要解決什麼問題呢?我想至少有下面幾個:
1.動態規則:好比限流的QPS咱們但願能夠動態修改,限流的功能能夠隨時開啓、關閉,限流的規則能夠跟隨業務進行動態變動等。
2.集羣限流:好比對Spring Cloud微服務架構中的某服務下的全部實例進行統一限流,以控制後續訪問數據庫的流量。
3.熔斷降級:好比在調用鏈路中某個資源出現不穩定狀態時(例如調用超時或異常比例升高),對這個資源的調用進行限制,讓請求快速失敗,避免影響到其它的資源而致使級聯錯誤。
可選的其它幾個功能,諸如實時監控數據、網關流控、熱點參數限流、系統自適應限流、黑白名單控制、註解支持等,這些功能其實能夠很是方便的進行擴展。
分佈式限流的思想我列舉下面三個方案:
這種方案是最簡單的一種集羣限流思想。在本地限流中,咱們使用Long的原子類做令牌桶,當實例數量超過1,咱們就考慮將Redis用做公共內存區域,進行讀寫。涉及到的併發控制,也可使用Redis實現分佈式鎖。
方案的缺點顯而易見,每取一次令牌都會進行一次網絡開銷,而網絡開銷起碼是毫秒級,因此這種方案支持的併發量是很是有限的。
這種方案的思想是將集羣限流最大程度的本地化。
舉個例子,咱們有兩臺服務器實例,對應的是同一個應用程序(Application.name相同),程序中設置的QPS爲100,將應用程序與同一個控制檯程序進行鏈接,控制檯端依據應用的實例數量將QPS進行均分,動態設置每一個實例的QPS爲50,如果遇到兩個服務器的配置並不相同,在負載均衡層的就已經根據服務器的優劣對流量進行分配,例如一臺分配70%流量,另外一臺分配30%的流量。面對這種狀況,控制檯也能夠對其實行加權分配QPS的策略。
客觀來講,這是一種集羣限流的實現方案,但依舊存在不小的問題。該模式的分配比例是創建在大數據流量下的趨勢進行分配,實際狀況中可能並非嚴格的五五分或三七分,偏差不可控,極容易出現用戶連續訪問某一臺服務器遇到請求駁回而另外一臺服務器此刻空閒流量充足的尷尬狀況。
這種方案的思想是創建在Redis令牌桶方案的基礎之上的。如何解決每次取令牌都伴隨一次網絡開銷,該方案的解決方法是創建一層控制端,利用該控制端與Redis令牌桶進行交互,只有當客戶端的剩餘令牌數不足時,客戶端才向該控制層取令牌而且每次取一批。
這種思想相似於Java集合框架的數組擴容,設置一個閾值,只有當超過該臨界值時,纔會觸發異步調用。其他存取令牌的操做與本地限流無二。雖然該方案依舊存在偏差,但偏差最大也就一批次令牌數而已。
上面說了三種分佈式限流方案的實現思路,這裏推薦一個基於發票服務器思想實現的分佈式限流項目SnowJean(https://github.com/yueshutong/SnowJena)。
筆者經過該項目源碼觀察到該限流項目在解決分佈式限流上的有不少巧妙的點,好比,SnowJean內部使用觀察者模式實現動態規則配置,使用工廠模式實現限流器的構造,使用建造者模式構建限流規則。
在解決如何對客戶端實例的健康情況進行檢查時,利用的是Redis的過時時間與客戶端發送的心跳包(發送心跳時再進行延期)。比較不錯的一點是,該項目提供基於前端Echarts圖表的QPS視圖展現,以下圖。