億級流量架構之服務限流思路與方法

爲何要限流

平常生活中,有哪些須要限流的地方?前端

像我旁邊有一個國家AAAA景區,平時可能根本沒什麼人前往,可是一到五一或者春節就人滿爲患,這時候景區管理人員就會實行一系列的政策來限制進入人流量,
爲何要限流呢?假如景區能容納一萬人,如今進去了三萬人,勢必摩肩接踵,整很差還會有事故發生,這樣的結果就是全部人的體驗都很差,若是發生了事故景區可能還要關閉,致使對外不可用,這樣的後果就是全部人都以爲體驗糟糕透了。java

限流的思想就是,在保證可用的狀況下儘量多增長進入的人數,其他的人在外面排隊等待,保證裏面的一萬人能夠正常遊玩。nginx

回到網絡上,一樣也是這個道理,例如某某明星公佈了戀情,訪問從平時的50萬增長到了500萬,系統最多能夠支撐200萬訪問,那麼就要執行限流規則,保證是一個可用的狀態,不至於服務器崩潰致使全部請求不可用。git

限流思路

對系統服務進行限流,通常有以下幾個模式:github

熔斷

系統在設計之初就把熔斷措施考慮進去。當系統出現問題時,若是短期內沒法修復,系統要自動作出判斷,開啓熔斷開關,拒絕流量訪問,避免大流量對後端的過載請求。算法

系統也應該可以動態監測後端程序的修復狀況,當程序已恢復穩定時,能夠關閉熔斷開關,恢復正常服務。常見的熔斷組件有Hystrix以及阿里的Sentinel,兩種互有優缺點,能夠根據業務的實際狀況進行選擇。數據庫

服務降級

將系統的全部功能服務進行一個分級,當系統出現問題須要緊急限流時,可將不是那麼重要的功能進行降級處理,中止服務,這樣能夠釋放出更多的資源供給核心功能的去用。後端

例如在電商平臺中,若是突發流量激增,可臨時將商品評論、積分等非核心功能進行降級,中止這些服務,釋放出機器和CPU等資源來保障用戶正常下單,而這些降級的功能服務能夠等整個系統恢復正常後,再來啓動,進行補單/補償處理。除了功能降級之外,還能夠採用不直接操做數據庫,而所有讀緩存、寫緩存的方式做爲臨時降級方案。緩存

延遲處理

這個模式須要在系統的前端設置一個流量緩衝池,將全部的請求所有緩衝進這個池子,不當即處理。而後後端真正的業務處理程序從這個池子中取出請求依次處理,常見的能夠用隊列模式來實現。這就至關於用異步的方式去減小了後端的處理壓力,可是當流量較大時,後端的處理能力有限,緩衝池裏的請求可能處理不及時,會有必定程度延遲。後面具體的漏桶算法以及令牌桶算法就是這個思路。服務器

特權處理

這個模式須要將用戶進行分類,經過預設的分類,讓系統優先處理須要高保障的用戶羣體,其它用戶羣的請求就會延遲處理或者直接不處理。

緩存、降級、限流區別

緩存,是用來增長系統吞吐量,提高訪問速度提供高併發。

降級,是在系統某些服務組件不可用的時候、流量暴增、資源耗盡等狀況下,暫時屏蔽掉出問題的服務,繼續提供降級服務,給用戶儘量的友好提示,返回兜底數據,不會影響總體業務流程,待問題解決再從新上線服務

限流,是指在使用緩存和降級無效的場景。好比當達到閾值後限制接口調用頻率,訪問次數,庫存個數等,在出現服務不可用以前,提早把服務降級。只服務好一部分用戶。

限流的算法

限流算法不少,常見的有三類,分別是計數器算法、漏桶算法、令牌桶算法,下面逐一講解。

計數器算法

簡單粗暴,好比指定線程池大小,指定數據庫鏈接池大小、nginx鏈接數等,這都屬於計數器算法。

計數器算法是限流算法裏最簡單也是最容易實現的一種算法。舉個例子,好比咱們規定對於A接口,咱們1分鐘的訪問次數不能超過100個。那麼咱們能夠這麼作:在一開 始的時候,咱們能夠設置一個計數器counter,每當一個請求過來的時候,counter就加1,若是counter的值大於100而且該請求與第一個請求的間隔時間還在1分鐘以內,那麼說明請求數過多,拒絕訪問;若是該請求與第一個請求的間隔時間大於1分鐘,且counter的值還在限流範圍內,那麼就重置 counter,就是這麼簡單粗暴。

漏桶算法

漏桶算法思路很簡單,水(請求)先進入到漏桶裏,漏桶以必定的速度出水,當水流入速度過大會超過桶可接納的容量時直接溢出,能夠看出漏桶算法能強行限制數據的傳輸速率。

2002319-20210220223842536-838208163

這樣作的好處是:

削峯:有大量流量進入時,會發生溢出,從而限流保護服務可用

緩衝:不至於直接請求到服務器,緩衝壓力
消費速度固定 由於計算性能固定

令牌桶算法

令牌桶與漏桶類似,不一樣的是令牌桶桶中放了一些令牌,服務請求到達後,要獲取令牌以後纔會獲得服務,舉個例子,咱們平時去食堂吃飯,都是在食堂內窗口前排隊的,這就比如是漏桶算法,大量的人員彙集在食堂內窗口外,以必定的速度享受服務,若是涌進來的人太多,食堂裝不下了,可能就有一部分人站到食堂外了,這就沒有享受到食堂的服務,稱之爲溢出,溢出能夠繼續請求,也就是繼續排隊,那麼這樣有什麼問題呢?

若是這時候有特殊狀況,好比有些趕時間的志願者啦、或者高三要高考啦,這種狀況就是突發狀況,若是也用漏桶算法那也得慢慢排隊,這也就沒有解決咱們的需求,對於不少應用場景來講,除了要求可以限制數據的平均傳輸速率外,還要求容許某種程度的突發傳輸。這時候漏桶算法可能就不合適了,令牌桶算法更爲適合。如圖所示,令牌桶算法的原理是系統會以一個恆定的速度往桶裏放入令牌,而若是請求須要被處理,則須要先從桶裏獲取一個令牌,當桶裏沒有令牌可取時,則拒絕服務。

2002319-20210220223928172-1995912492

令牌桶好處就是,若是某一瞬間訪問量劇增或者有突發狀況,能夠經過改變桶中令牌數量來改變鏈接數,就比如那個食堂排隊吃飯的問題,若是如今不是直接去窗口排隊,而是先來樓外拿飯票而後再去排隊,那麼有高三的學生時能夠將增長飯票數量或者優先將令牌給高三的學生,這樣比漏桶算法更加靈活。

併發限流

簡單來講就是設置系統閾值總的QPS個數,這些也挺常見的,就拿Tomcat來講,不少參數就是出於這個考慮,例如

配置的acceptCount 設置響應鏈接數, maxConnections設置瞬時最大鏈接數, maxThreads 設置最大線程數,在各個框架或者組件中,併發限流體如今下面幾個方面:

  • 限制總併發數(如數據庫鏈接池、線程池)
  • 限制瞬時併發數(nginx的limit_conn模塊,用來限制瞬時併發鏈接數)
  • 限制時間窗口內的平均速率(如Guava的RateLimiter、nginx的limit_req模塊,限制每秒的平均速率)
  • 其餘的還有限制遠程接口調用速率、限制MQ的消費速率。
  • 另外還能夠根據網絡鏈接數、網絡流量、CPU或內存負載等來限流。

有了併發限流,就意味着在處理高併發的時候多了一種保護機制,不用擔憂瞬間流量致使系統掛掉或雪崩,最終作到有損服務而不是不服務;可是限流須要評估好,不能亂用,不然一些正常流量出現一些奇怪的問題而致使用戶體驗不好形成用戶流失。

接口限流

接口限流分爲兩個部分,一是限制一段時間內接口調用次數,參照前面限流算法的計數器算法, 二是設置滑動時間窗口算法。

接口總數

控制一段時間內接口被調用的總數量,能夠參考前面的計數器算法,再也不贅述。

接口時間窗口

固定時間窗口算法(也就是前面提到的計數器算法)的問題是統計區間太大,限流不夠精確,並且在第二個統計區間 時沒有考慮與前一個統計區間的關係與影響(第一個區間後半段 + 第二個區間前半段也是一分鐘)。爲了解決上面咱們提到的臨界問題,咱們試圖把每一個統計區間分爲更小的統計區間,更精確的統計計數。

在上面的例子中,假設QPS能夠接受100次查詢/秒, 前一分鐘前40秒訪問很低,後20秒突增,而且這個持續了一段時間,直到第二分鐘的第40秒纔開始降下來,根據前面的計數方法,前一秒的QPS爲94,後一秒的QPS爲92,那麼沒有超過設定參數,可是!可是在中間區域,QPS達到了142,這明顯超過了咱們的容許的服務請求數目,因此固定窗口計數器不太可靠,須要滑動窗口計數器。

計數器算法其實就是固定窗口算法, 只是它沒有對時間窗口作進一步地劃分,因此只有1格;因而可知,當滑動窗口的格子劃分的越多,也就是將秒精確到毫秒或者納秒, 那麼滑動窗口的滾動就越平滑,限流的統計就會越精確。

須要注意的是,消耗的空間就越多。

限流實現

這一部分是限流的具體實現,簡單說說,畢竟長篇代碼沒人願意看。

guava實現

引入包

<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>28.1-jre</version>
</dependency>

核心代碼

LoadingCache<Long, AtomicLong> counter = CacheBuilder.newBuilder().
				expireAfterWrite(2, TimeUnit.SECONDS)
				.build(new CacheLoader<Long, AtomicLong>() {

					@Override
					public AtomicLong load(Long secend) throws Exception {
						// TODO Auto-generated method stub
						return new AtomicLong(0);
					}
				});
		counter.get(1l).incrementAndGet();

令牌桶實現

穩定模式(SmoothBursty:令牌生成速度恆定)

public static void main(String[] args) {
		// RateLimiter.create(2)每秒產生的令牌數
		RateLimiter limiter = RateLimiter.create(2);
        // limiter.acquire() 阻塞的方式獲取令牌
		System.out.println(limiter.acquire());;
		try {
			Thread.sleep(2000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		System.out.println(limiter.acquire());;
		System.out.println(limiter.acquire());;
		System.out.println(limiter.acquire());;
		System.out.println(limiter.acquire());;
		
		System.out.println(limiter.acquire());;
		System.out.println(limiter.acquire());;
	}

```RateLimiter.create(2)`` 容量和突發量,令牌桶算法容許將一段時間內沒有消費的令牌暫存到令牌桶中,用來突發消費。

漸進模式(SmoothWarmingUp:令牌生成速度緩慢提高直到維持在一個穩定值)

// 平滑限流,從冷啓動速率(滿的)到平均消費速率的時間間隔
		RateLimiter limiter = RateLimiter.create(2,1000l,TimeUnit.MILLISECONDS);
		System.out.println(limiter.acquire());;
		try {
			Thread.sleep(2000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		System.out.println(limiter.acquire());;
		System.out.println(limiter.acquire());;
		System.out.println(limiter.acquire());;
		System.out.println(limiter.acquire());;
		
		System.out.println(limiter.acquire());;
		System.out.println(limiter.acquire());;

超時

boolean tryAcquire = limiter.tryAcquire(Duration.ofMillis(11));

在timeout時間內是否可以得到令牌,異步執行

分佈式系統限流

Nginx + Lua實現

可使用resty.lock保持原子特性,請求之間不會產生鎖的重入

https://github.com/openresty/lua-resty-lock

使用lua_shared_dict存儲數據

local locks = require "resty.lock"

local function acquire()
    local lock =locks:new("locks")
    local elapsed, err =lock:lock("limit_key") --互斥鎖 保證原子特性
    local limit_counter =ngx.shared.limit_counter --計數器

    local key = "ip:" ..os.time()
    local limit = 5 --限流大小
    local current =limit_counter:get(key)

    if current ~= nil and current + 1> limit then --若是超出限流大小
       lock:unlock()
       return 0
    end
    if current == nil then
       limit_counter:set(key, 1, 1) --第一次須要設置過時時間,設置key的值爲1,
--過時時間爲1秒
    else
        limit_counter:incr(key, 1) --第二次開始加1便可
    end
    lock:unlock()
    return 1
end
ngx.print(acquire())
相關文章
相關標籤/搜索