億級(jí)流量架構(gòu)之服務(wù)限流思路與方法
時(shí)間:2021-09-14 11:32:15
手機(jī)看文章
掃描二維碼
隨時(shí)隨地手機(jī)看文章
[導(dǎo)讀]為什么要限流日常生活中,有哪些需要限流的地方?像我旁邊有一個(gè)國(guó)家AAAA景區(qū),平時(shí)可能根本沒(méi)什么人前往,但是一到五一或者春節(jié)就人滿為患,這時(shí)候景區(qū)管理人員就會(huì)實(shí)行一系列的政策來(lái)限制進(jìn)入人流量,為什么要限流呢?假如景區(qū)能容納一萬(wàn)人,現(xiàn)在進(jìn)去了三萬(wàn)人,勢(shì)必摩肩接踵,整不好還會(huì)有事故發(fā)...
為什么要限流呢?假如景區(qū)能容納一萬(wàn)人,現(xiàn)在進(jìn)去了三萬(wàn)人,勢(shì)必摩肩接踵,整不好還會(huì)有事故發(fā)生,這樣的結(jié)果就是所有人的體驗(yàn)都不好,如果發(fā)生了事故景區(qū)可能還要關(guān)閉,導(dǎo)致對(duì)外不可用,這樣的后果就是所有人都覺(jué)得體驗(yàn)糟糕透了。
限流思路
對(duì)系統(tǒng)服務(wù)進(jìn)行限流,一般有如下幾個(gè)模式:
熔斷
系統(tǒng)在設(shè)計(jì)之初就把熔斷措施考慮進(jìn)去。當(dāng)系統(tǒng)出現(xiàn)問(wèn)題時(shí),如果短時(shí)間內(nèi)無(wú)法修復(fù),系統(tǒng)要自動(dòng)做出判斷,開啟熔斷開關(guān),拒絕流量訪問(wèn),避免大流量對(duì)后端的過(guò)載請(qǐng)求。

服務(wù)降級(jí)
將系統(tǒng)的所有功能服務(wù)進(jìn)行一個(gè)分級(jí),當(dāng)系統(tǒng)出現(xiàn)問(wèn)題需要緊急限流時(shí),可將不是那么重要的功能進(jìn)行降級(jí)處理,停止服務(wù),這樣可以釋放出更多的資源供給核心功能的去用。
延遲處理
這個(gè)模式需要在系統(tǒng)的前端設(shè)置一個(gè)流量緩沖池,將所有的請(qǐng)求全部緩沖進(jìn)這個(gè)池子,不立即處理。然后后端真正的業(yè)務(wù)處理程序從這個(gè)池子中取出請(qǐng)求依次處理,常見的可以用隊(duì)列模式來(lái)實(shí)現(xiàn)。這就相當(dāng)于用異步的方式去減少了后端的處理壓力,但是當(dāng)流量較大時(shí),后端的處理能力有限,緩沖池里的請(qǐng)求可能處理不及時(shí),會(huì)有一定程度延遲。后面具體的漏桶算法以及令牌桶算法就是這個(gè)思路。
特權(quán)處理
這個(gè)模式需要將用戶進(jìn)行分類,通過(guò)預(yù)設(shè)的分類,讓系統(tǒng)優(yōu)先處理需要高保障的用戶群體,其它用戶群的請(qǐng)求就會(huì)延遲處理或者直接不處理。
緩存、降級(jí)、限流區(qū)別
緩存,是用來(lái)增加系統(tǒng)吞吐量,提升訪問(wèn)速度提供高并發(fā)。
限流的算法
限流算法很多,常見的有三類,分別是計(jì)數(shù)器算法、漏桶算法、令牌桶算法,下面逐一講解。
計(jì)數(shù)器算法
簡(jiǎn)單粗暴,比如指定線程池大小,指定數(shù)據(jù)庫(kù)連接池大小、nginx連接數(shù)等,這都屬于計(jì)數(shù)器算法。

漏桶算法
漏桶算法思路很簡(jiǎn)單,水(請(qǐng)求)先進(jìn)入到漏桶里,漏桶以一定的速度出水,當(dāng)水流入速度過(guò)大會(huì)超過(guò)桶可接納的容量時(shí)直接溢出,可以看出漏桶算法能強(qiáng)行限制數(shù)據(jù)的傳輸速率。

消費(fèi)速度固定 因?yàn)橛?jì)算性能固定
令牌桶算法
令牌桶與漏桶相似,不同的是令牌桶桶中放了一些令牌,服務(wù)請(qǐng)求到達(dá)后,要獲取令牌之后才會(huì)得到服務(wù),舉個(gè)例子,我們平時(shí)去食堂吃飯,都是在食堂內(nèi)窗口前排隊(duì)的,這就好比是漏桶算法,大量的人員聚集在食堂內(nèi)窗口外,以一定的速度享受服務(wù),如果涌進(jìn)來(lái)的人太多,食堂裝不下了,可能就有一部分人站到食堂外了,這就沒(méi)有享受到食堂的服務(wù),稱之為溢出,溢出可以繼續(xù)請(qǐng)求,也就是繼續(xù)排隊(duì),那么這樣有什么問(wèn)題呢?

并發(fā)限流
簡(jiǎn)單來(lái)說(shuō)就是設(shè)置系統(tǒng)閾值總的QPS個(gè)數(shù),這些也挺常見的,就拿Tomcat來(lái)說(shuō),很多參數(shù)就是出于這個(gè)考慮,例如
-
限制總并發(fā)數(shù)(如數(shù)據(jù)庫(kù)連接池、線程池)
-
限制瞬時(shí)并發(fā)數(shù)(nginx的limit_conn模塊,用來(lái)限制瞬時(shí)并發(fā)連接數(shù))
-
限制時(shí)間窗口內(nèi)的平均速率(如Guava的RateLimiter、nginx的limit_req模塊,限制每秒的平均速率)
-
其他的還有限制遠(yuǎn)程接口調(diào)用速率、限制MQ的消費(fèi)速率。
-
另外還可以根據(jù)網(wǎng)絡(luò)連接數(shù)、網(wǎng)絡(luò)流量、CPU或內(nèi)存負(fù)載等來(lái)限流。
接口限流
接口限流分為兩個(gè)部分,一是限制一段時(shí)間內(nèi)接口調(diào)用次數(shù),參照前面限流算法的計(jì)數(shù)器算法, 二是設(shè)置滑動(dòng)時(shí)間窗口算法。
接口總數(shù)
控制一段時(shí)間內(nèi)接口被調(diào)用的總數(shù)量,可以參考前面的計(jì)數(shù)器算法,不再贅述。
接口時(shí)間窗口
固定時(shí)間窗口算法(也就是前面提到的計(jì)數(shù)器算法)的問(wèn)題是統(tǒng)計(jì)區(qū)間太大,限流不夠精確,而且在第二個(gè)統(tǒng)計(jì)區(qū)間 時(shí)沒(méi)有考慮與前一個(gè)統(tǒng)計(jì)區(qū)間的關(guān)系與影響(第一個(gè)區(qū)間后半段 第二個(gè)區(qū)間前半段也是一分鐘)。為了解決上面我們提到的臨界問(wèn)題,我們?cè)噲D把每個(gè)統(tǒng)計(jì)區(qū)間分為更小的統(tǒng)計(jì)區(qū)間,更精確的統(tǒng)計(jì)計(jì)數(shù)。

限流實(shí)現(xiàn)
這一部分是限流的具體實(shí)現(xiàn),簡(jiǎn)單說(shuō)說(shuō),畢竟長(zhǎng)篇代碼沒(méi)人愿意看。
guava實(shí)現(xiàn)
引入包
com.google.guava
guava
28.1-jre
核心代碼
expireAfterWrite(2, TimeUnit.SECONDS)
.build(new CacheLoader
@Override
public AtomicLong load(Long secend) throws Exception {
// TODO Auto-generated method stub
return new AtomicLong(0);
}
});
counter.get(1l).incrementAndGet();
令牌桶實(shí)現(xiàn)
穩(wěn)定模式(SmoothBursty:令牌生成速度恒定)
// RateLimiter.create(2)每秒產(chǎn)生的令牌數(shù)
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 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());;
分布式系統(tǒng)限流
Nginx Lua實(shí)現(xiàn)
local function acquire()
local lock =locks:new("locks")
local elapsed, err =lock:lock("limit_key") --互斥鎖 保證原子特性
local limit_counter =ngx.shared.limit_counter --計(jì)數(shù)器
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) --第一次需要設(shè)置過(guò)期時(shí)間,設(shè)置key的值為1,
--過(guò)期時(shí)間為1秒
else
limit_counter:incr(key, 1) --第二次開始加1即可
end
lock:unlock()
return 1
end
ngx.print(acquire())