開濤大神在博客中說過:在開發高併發系統時有三把利器用來保護系統:緩存、降級和限流。本文結合做者的一些經驗介紹限流的相關概念、算法和常規的實現方式。html
緩存比較好理解,在大型高併發系統中,若是沒有緩存數據庫將分分鐘被爆,系統也會瞬間癱瘓。使用緩存不僅僅可以提高系統訪問速度、提升併發訪問量,也是保護數據庫、保護系統的有效方式。大型網站通常主要是「讀」,緩存的使用很容易被想到。在大型「寫」系統中,緩存也經常扮演者很是重要的角色。好比累積一些數據批量寫入,內存裏面的緩存隊列(生產消費),以及HBase寫數據的機制等等也都是經過緩存提高系統的吞吐量或者實現系統的保護措施。甚至消息中間件,你也能夠認爲是一種分佈式的數據緩存。node
服務降級是當服務器壓力劇增的狀況下,根據當前業務狀況及流量對一些服務和頁面有策略的降級,以此釋放服務器資源以保證核心任務的正常運行。降級每每會指定不一樣的級別,面臨不一樣的異常等級執行不一樣的處理。根據服務方式:能夠拒接服務,能夠延遲服務,也有時候能夠隨機服務。根據服務範圍:能夠砍掉某個功能,也能夠砍掉某些模塊。總之服務降級須要根據不一樣的業務需求採用不一樣的降級策略。主要的目的就是服務雖然有損可是總比沒有好。nginx
限流能夠認爲服務降級的一種,限流就是限制系統的輸入和輸出流量已達到保護系統的目的。通常來講系統的吞吐量是能夠被測算的,爲了保證系統的穩定運行,一旦達到的須要限制的閾值,就須要限制流量並採起一些措施以完成限制流量的目的。好比:延遲處理,拒絕處理,或者部分拒絕處理等等。算法
常見的限流算法有:計數器、漏桶和令牌桶算法。數據庫
計數器api
計數器是最簡單粗暴的算法。好比某個服務最多隻能每秒鐘處理100個請求。咱們能夠設置一個1秒鐘的滑動窗口,窗口中有10個格子,每一個格子100毫秒,每100毫秒移動一次,每次移動都須要記錄當前服務請求的次數。內存中須要保存10次的次數。能夠用數據結構LinkedList來實現。格子每次移動的時候判斷一次,當前訪問次數和LinkedList中最後一個相差是否超過100,若是超過就須要限流了。緩存
很明顯,當滑動窗口的格子劃分的越多,那麼滑動窗口的滾動就越平滑,限流的統計就會越精確。服務器
示例代碼以下:網絡
//服務訪問次數,能夠放在Redis中,實現分佈式系統的訪問計數 Long counter = 0L; //使用LinkedList來記錄滑動窗口的10個格子。 LinkedList<Long> ll = new LinkedList<Long>(); public static void main(String[] args) { Counter counter = new Counter(); counter.doCheck(); } private void doCheck() { while (true) { ll.addLast(counter); if (ll.size() > 10) { ll.removeFirst(); } //比較最後一個和第一個,二者相差一秒 if ((ll.peekLast() - ll.peekFirst()) > 100) { //To limit rate } Thread.sleep(100); } }
漏桶算法數據結構
漏桶算法即leaky bucket是一種很是經常使用的限流算法,能夠用來實現流量整形(Traffic Shaping)和流量控制(Traffic Policing)。貼了一張維基百科上示意圖幫助你們理解:
漏桶算法的主要概念以下:
一個固定容量的漏桶,按照常量固定速率流出水滴;
若是桶是空的,則不需流出水滴;
能夠以任意速率流入水滴到漏桶;
若是流入水滴超出了桶的容量,則流入的水滴溢出了(被丟棄),而漏桶容量是不變的。
漏桶算法比較好實現,在單機系統中可使用隊列來實現(.Net中TPL DataFlow能夠較好的處理相似的問題,你能夠在這裏找到相關的介紹),在分佈式環境中消息中間件或者Redis都是可選的方案。
令牌桶算法
令牌桶算法是一個存放固定容量令牌(token)的桶,按照固定速率往桶裏添加令牌。令牌桶算法基本能夠用下面的幾個概念來描述:
以下圖:
令牌算法是根據放令牌的速率去控制輸出的速率,也就是上圖的to network的速率。to network咱們能夠理解爲消息的處理程序,執行某段業務或者調用某個RPC。
漏桶和令牌桶的比較
令牌桶能夠在運行時控制和調整數據處理的速率,處理某時的突發流量。放令牌的頻率增長能夠提高總體數據處理的速度,而經過每次獲取令牌的個數增長或者放慢令牌的發放速度和下降總體數據處理速度。而漏桶不行,由於它的流出速率是固定的,程序處理速度也是固定的。
總體而言,令牌桶算法更優,可是實現更爲複雜一些。
Guava
Guava是一個Google開源項目,包含了若干被Google的Java項目普遍依賴的核心庫,其中的RateLimiter提供了令牌桶算法實現:平滑突發限流(SmoothBursty)和平滑預熱限流(SmoothWarmingUp)實現。
1. 常規速率:
建立一個限流器,設置每秒放置的令牌數:2個。返回的RateLimiter對象能夠保證1秒內不會給超過2個令牌,而且是固定速率的放置。達到平滑輸出的效果
public void test() { /** * 建立一個限流器,設置每秒放置的令牌數:2個。速率是每秒能夠2個的消息。 * 返回的RateLimiter對象能夠保證1秒內不會給超過2個令牌,而且是固定速率的放置。達到平滑輸出的效果 */ RateLimiter r = RateLimiter.create(2); while (true) { /** * acquire()獲取一個令牌,而且返回這個獲取這個令牌所須要的時間。若是桶裏沒有令牌則等待,直到有令牌。 * acquire(N)能夠獲取多個令牌。 */ System.out.println(r.acquire()); } }
上面代碼執行的結果以下圖,基本是0.5秒一個數據。拿到令牌後才能處理數據,達到輸出數據或者調用接口的平滑效果。acquire()的返回值是等待令牌的時間,若是須要對某些突發的流量進行處理的話,能夠對這個返回值設置一個閾值,根據不一樣的狀況進行處理,好比過時丟棄。
2. 突發流量:
突發流量能夠是突發的多,也能夠是突發的少。首先來看個突發多的例子。仍是上面例子的流量,每秒2個數據令牌。以下代碼使用acquire方法,指定參數。
System.out.println(r.acquire(2));
System.out.println(r.acquire(1));
System.out.println(r.acquire(1));
System.out.println(r.acquire(1));
獲得以下相似的輸出。
若是要一次新處理更多的數據,則須要更多的令牌。代碼首先獲取2個令牌,那麼下一個令牌就不是0.5秒以後得到了,仍是1秒之後,以後又恢復常規速度。這是一個突發多的例子,若是是突發沒有流量,以下代碼:
System.out.println(r.acquire(1));
Thread.sleep(2000);
System.out.println(r.acquire(1));
System.out.println(r.acquire(1));
System.out.println(r.acquire(1));
獲得以下相似的結果:
等了兩秒鐘以後,令牌桶裏面就積累了3個令牌,能夠連續不花時間的獲取出來。處理突發其實也就是在單位時間內輸出恆定。這兩種方式都是使用的RateLimiter的子類SmoothBursty。另外一個子類是SmoothWarmingUp,它提供的有必定緩衝的流量輸出方案。
/** * 建立一個限流器,設置每秒放置的令牌數:2個。速率是每秒能夠210的消息。 * 返回的RateLimiter對象能夠保證1秒內不會給超過2個令牌,而且是固定速率的放置。達到平滑輸出的效果 * 設置緩衝時間爲3秒 */ RateLimiter r = RateLimiter.create(2,3,TimeUnit.SECONDS); while (true) { /** * acquire()獲取一個令牌,而且返回這個獲取這個令牌所須要的時間。若是桶裏沒有令牌則等待,直到有令牌。 * acquire(N)能夠獲取多個令牌。 */ System.out.println(r.acquire(1)); System.out.println(r.acquire(1)); System.out.println(r.acquire(1)); System.out.println(r.acquire(1)); }
輸出結果以下圖,因爲設置了緩衝的時間是3秒,令牌桶一開始並不會0.5秒給一個消息,而是造成一個平滑線性降低的坡度,頻率愈來愈高,在3秒鐘以內達到本來設置的頻率,之後就以固定的頻率輸出。圖中紅線圈出來的3次累加起來正好是3秒左右。這種功能適合系統剛啓動須要一點時間來「熱身」的場景。
Nginx
對於Nginx接入層限流可使用Nginx自帶了兩個模塊:鏈接數限流模塊ngx_http_limit_conn_module和漏桶算法實現的請求限流模塊ngx_http_limit_req_module。
1. ngx_http_limit_conn_module
咱們常常會遇到這種狀況,服務器流量異常,負載過大等等。對於大流量惡意的攻擊訪問,會帶來帶寬的浪費,服務器壓力,影響業務,每每考慮對同一個ip的鏈接數,併發數進行限制。ngx_http_limit_conn_module 模塊來實現該需求。該模塊能夠根據定義的鍵來限制每一個鍵值的鏈接數,如同一個IP來源的鏈接數。並非全部的鏈接都會被該模塊計數,只有那些正在被處理的請求(這些請求的頭信息已被徹底讀入)所在的鏈接纔會被計數。
咱們能夠在nginx_conf的http{}中加上以下配置實現限制:
#限制每一個用戶的併發鏈接數,取名one limit_conn_zone $binary_remote_addr zone=one:10m; #配置記錄被限流後的日誌級別,默認error級別 limit_conn_log_level error; #配置被限流後返回的狀態碼,默認返回503 limit_conn_status 503;
而後在server{}里加上以下代碼:
#限制用戶併發鏈接數爲1
limit_conn one 1;
而後咱們是使用ab測試來模擬併發請求:
ab -n 5 -c 5 http://10.23.22.239/index.html
獲得下面的結果,很明顯併發被限制住了,超過閾值的都顯示503:
另外剛纔是配置針對單個IP的併發限制,仍是能夠針對域名進行併發限制,配置和客戶端IP相似。
#http{}段配置
limit_conn_zone $ server_name zone=perserver:10m;
#server{}段配置
limit_conn perserver 1;
2. ngx_http_limit_req_module
上面咱們使用到了ngx_http_limit_conn_module 模塊,來限制鏈接數。那麼請求數的限制該怎麼作呢?這就須要經過ngx_http_limit_req_module 模塊來實現,該模塊能夠經過定義的鍵值來限制請求處理的頻率。特別的,能夠限制來自單個IP地址的請求處理頻率。 限制的方法是使用了漏斗算法,每秒固定處理請求數,推遲過多請求。若是請求的頻率超過了限制域配置的值,請求處理會被延遲或被丟棄,因此全部的請求都是以定義的頻率被處理的。
在http{}中配置
#區域名稱爲one,大小爲10m,平均處理的請求頻率不能超過每秒一次。
limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
在server{}中配置
#設置每一個IP桶的數量爲5
limit_req zone=one burst=5;
上面設置定義了每一個IP的請求處理只能限制在每秒1個。而且服務端能夠爲每一個IP緩存5個請求,若是操做了5個請求,請求就會被丟棄。
使用ab測試模擬客戶端連續訪問10次:ab -n 10 -c 10 http://10.23.22.239/index.html
以下圖,設置了通的個數爲5個。一共10個請求,第一個請求立刻被處理。第2-6個被存放在桶中。因爲桶滿了,沒有設置nodelay所以,餘下的4個請求被丟棄。