我司“雙11”限流方案,進(jìn)來抄作業(yè)!
圖片來自 包圖網(wǎng)
假如景區(qū)能容納一萬人,現(xiàn)在進(jìn)去了三萬人,勢必摩肩接踵,整不好還會有事故發(fā)生,這樣的結(jié)果就是所有人的體驗(yàn)都不好。
如果發(fā)生了事故景區(qū)可能還要關(guān)閉,導(dǎo)致對外不可用,這樣的后果就是所有人都覺得體驗(yàn)糟糕透了。
限流的思想就是,在保證可用的情況下盡可能多增加進(jìn)入的人數(shù),其余的人在外面排隊等待,保證里面的一萬人可以正常游玩。
回到網(wǎng)絡(luò)上,同樣也是這個道理,例如某某明星公布了戀情,訪問從平時的 50 萬增加到了 500 萬,系統(tǒng)最多可以支撐 200 萬訪問、雙十一的秒殺活動、12306 的搶票等。
那么就要執(zhí)行限流規(guī)則,保證是一個可用的狀態(tài),不至于服務(wù)器崩潰導(dǎo)致所有請求不可用。
限流思路
對系統(tǒng)服務(wù)進(jìn)行限流,一般有如下幾個模式:
①熔斷
系統(tǒng)在設(shè)計之初就把熔斷措施考慮進(jìn)去。當(dāng)系統(tǒng)出現(xiàn)問題時,如果短時間內(nèi)無法修復(fù),系統(tǒng)要自動做出判斷,開啟熔斷開關(guān),拒絕流量訪問,避免大流量對后端的過載請求。
系統(tǒng)也應(yīng)該能夠動態(tài)監(jiān)測后端程序的修復(fù)情況,當(dāng)程序已恢復(fù)穩(wěn)定時,可以關(guān)閉熔斷開關(guān),恢復(fù)正常服務(wù)。
常見的熔斷組件有 Hystrix 以及阿里的 Sentinel,兩種互有優(yōu)缺點(diǎn),可以根據(jù)業(yè)務(wù)的實(shí)際情況進(jìn)行選擇。
②服務(wù)降級
將系統(tǒng)的所有功能服務(wù)進(jìn)行一個分級,當(dāng)系統(tǒng)出現(xiàn)問題需要緊急限流時,可將不是那么重要的功能進(jìn)行降級處理,停止服務(wù),這樣可以釋放出更多的資源供給核心功能的去用。
例如在電商平臺中,如果突發(fā)流量激增,可臨時將商品評論、積分等非核心功能進(jìn)行降級,停止這些服務(wù),釋放出機(jī)器和 CPU 等資源來保障用戶正常下單。
而這些降級的功能服務(wù)可以等整個系統(tǒng)恢復(fù)正常后,再來啟動,進(jìn)行補(bǔ)單/補(bǔ)償處理。
除了功能降級以外,還可以采用不直接操作數(shù)據(jù)庫,而全部讀緩存、寫緩存的方式作為臨時降級方案。
③延遲處理
這個模式需要在系統(tǒng)的前端設(shè)置一個流量緩沖池,將所有的請求全部緩沖進(jìn)這個池子,不立即處理。
然后后端真正的業(yè)務(wù)處理程序從這個池子中取出請求依次處理,常見的可以用隊列模式來實(shí)現(xiàn)。
這就相當(dāng)于用異步的方式去減少了后端的處理壓力,但是當(dāng)流量較大時,后端的處理能力有限,緩沖池里的請求可能處理不及時,會有一定程度延遲。后面具體的漏桶算法以及令牌桶算法就是這個思路。
④特權(quán)處理
這個模式需要將用戶進(jìn)行分類,通過預(yù)設(shè)的分類,讓系統(tǒng)優(yōu)先處理需要高保障的用戶群體,其它用戶群的請求就會延遲處理或者直接不處理。
緩存、降級、限流區(qū)別如下:
- 緩存,是用來增加系統(tǒng)吞吐量,提升訪問速度提供高并發(fā)。
- 降級,是在系統(tǒng)某些服務(wù)組件不可用的時候、流量暴增、資源耗盡等情況下,暫時屏蔽掉出問題的服務(wù),繼續(xù)提供降級服務(wù),給用戶盡可能的友好提示,返回兜底數(shù)據(jù),不會影響整體業(yè)務(wù)流程,待問題解決再重新上線服務(wù)
限流,是指在使用緩存和降級無效的場景。比如當(dāng)達(dá)到閾值后限制接口調(diào)用頻率,訪問次數(shù),庫存?zhèn)€數(shù)等,在出現(xiàn)服務(wù)不可用之前,提前把服務(wù)降級。只服務(wù)好一部分用戶。
限流的算法
限流算法很多,常見的有三類,分別是計數(shù)器算法、漏桶算法、令牌桶算法,下面逐一講解。
①計數(shù)器算法
簡單粗暴,比如指定線程池大小,指定數(shù)據(jù)庫連接池大小、Nnginx 連接數(shù)等,這都屬于計數(shù)器算法。
計數(shù)器算法是限流算法里最簡單也是最容易實(shí)現(xiàn)的一種算法。舉個例子,比如我們規(guī)定對于 A 接口,我們 1 分鐘的訪問次數(shù)不能超過 100 個。
那么我們可以這么做:在一開始的時候,我們可以設(shè)置一個計數(shù)器 counter,每當(dāng)一個請求過來的時候,counter 就加 1。
如果 counter 的值大于 100 并且該請求與第一個請求的間隔時間還在 1 分鐘之內(nèi),那么說明請求數(shù)過多,拒絕訪問。
如果該請求與第一個請求的間隔時間大于 1 分鐘,且 counter 的值還在限流范圍內(nèi),那么就重置 counter,就是這么簡單粗暴。
②漏桶算法
漏桶算法思路很簡單,水(請求)先進(jìn)入到漏桶里,漏桶以一定的速度出水,當(dāng)水流入速度過大會超過桶可接納的容量時直接溢出,可以看出漏桶算法能強(qiáng)行限制數(shù)據(jù)的傳輸速率。
- 削峰:有大量流量進(jìn)入時,會發(fā)生溢出,從而限流保護(hù)服務(wù)可用。
- 緩沖:不至于直接請求到服務(wù)器,緩沖壓力,消費(fèi)速度固定,因?yàn)橛嬎阈阅芄潭ā?/li>
③令牌桶算法
令牌桶與漏桶相似,不同的是令牌桶桶中放了一些令牌,服務(wù)請求到達(dá)后,要獲取令牌之后才會得到服務(wù)。
舉個例子,我們平時去食堂吃飯,都是在食堂內(nèi)窗口前排隊的,這就好比是漏桶算法,大量的人員聚集在食堂內(nèi)窗口外,以一定的速度享受服務(wù)。
如果涌進(jìn)來的人太多,食堂裝不下了,可能就有一部分人站到食堂外了,這就沒有享受到食堂的服務(wù),稱之為溢出,溢出可以繼續(xù)請求,也就是繼續(xù)排隊,那么這樣有什么問題呢?
如果這時候有特殊情況,比如有些趕時間的志愿者啦或者高三要高考啦,這種情況就是突發(fā)情況。
如果也用漏桶算法那也得慢慢排隊,這也就沒有解決我們的需求,對于很多應(yīng)用場景來說,除了要求能夠限制數(shù)據(jù)的平均傳輸速率外,還要求允許某種程度的突發(fā)傳輸。
這時候漏桶算法可能就不合適了,令牌桶算法更為適合。
如圖所示,令牌桶算法的原理是系統(tǒng)會以一個恒定的速度往桶里放入令牌,而如果請求需要被處理,則需要先從桶里獲取一個令牌,當(dāng)桶里沒有令牌可取時,則拒絕服務(wù)。
并發(fā)限流
簡單來說就是設(shè)置系統(tǒng)閾值總的 QPS 個數(shù),這些也挺常見的,就拿 Tomcat 來說,很多參數(shù)就是出于這個考慮。
例如配置的 acceptCount 設(shè)置響應(yīng)連接數(shù),maxConnections 設(shè)置瞬時最大連接數(shù),maxThreads 設(shè)置最大線程數(shù)。
在各個框架或者組件中,并發(fā)限流體現(xiàn)在下面幾個方面:
- 限制總并發(fā)數(shù)(如數(shù)據(jù)庫連接池、線程池)
- 限制瞬時并發(fā)數(shù)(nginx 的 limit_conn 模塊,用來限制瞬時并發(fā)連接數(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ù)載等來限流。
有了并發(fā)限流,就意味著在處理高并發(fā)的時候多了一種保護(hù)機(jī)制,不用擔(dān)心瞬間流量導(dǎo)致系統(tǒng)掛掉或雪崩,最終做到有損服務(wù)而不是不服務(wù)。
但是限流需要評估好,不能亂用,否則一些正常流量出現(xiàn)一些奇怪的問題而導(dǎo)致用戶體驗(yàn)很差造成用戶流失。
接口限流
接口限流分為兩個部分,一是限制一段時間內(nèi)接口調(diào)用次數(shù),參照前面限流算法的計數(shù)器算法,二是設(shè)置滑動時間窗口算法。
①接口總數(shù)
控制一段時間內(nèi)接口被調(diào)用的總數(shù)量,可以參考前面的計數(shù)器算法,不再贅述。
②接口時間窗口
固定時間窗口算法(也就是前面提到的計數(shù)器算法)的問題是統(tǒng)計區(qū)間太大,限流不夠精確,而且在第二個統(tǒng)計區(qū)間時沒有考慮與前一個統(tǒng)計區(qū)間的關(guān)系與影響(第一個區(qū)間后半段+第二個區(qū)間前半段也是一分鐘)。
為了解決上面我們提到的臨界問題,我們試圖把每個統(tǒng)計區(qū)間分為更小的統(tǒng)計區(qū)間,更精確的統(tǒng)計計數(shù)。
在上面的例子中,假設(shè) QPS 可以接受 100 次查詢/秒,前一分鐘前 40 秒訪問很低,后 20 秒突增,并且這個持續(xù)了一段時間,直到第二分鐘的第 40 秒才開始降下來。
根據(jù)前面的計數(shù)方法,前一秒的 QPS 為 94,,后一秒的 QPS 為 92,那么沒有超過設(shè)定參數(shù)。
但是在中間區(qū)域,QPS 達(dá)到了 142,,這明顯超過了我們的允許的服務(wù)請求數(shù)目,所以固定窗口計數(shù)器不太可靠,需要滑動窗口計數(shù)器。
計數(shù)器算法其實(shí)就是固定窗口算法,只是它沒有對時間窗口做進(jìn)一步地劃分,所以只有 1 格。
由此可見,當(dāng)滑動窗口的格子劃分的越多,也就是將秒精確到毫秒或者納秒,那么滑動窗口的滾動就越平滑,限流的統(tǒng)計就會越精確。需要注意的是,消耗的空間就越多。
限流實(shí)現(xiàn)
這一部分是限流的具體實(shí)現(xiàn),簡單說說,畢竟長篇代碼沒人愿意看。
①guava 實(shí)現(xiàn)
引入包:
- <!-- 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();
②令牌桶實(shí)現(xiàn)
穩(wěn)定模式(SmoothBursty:令牌生成速度恒定):
- public static void main(String[] args) {
- // 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.create(2) 容量和突發(fā)量,令牌桶算法允許將一段時間內(nèi)沒有消費(fèi)的令牌暫存到令牌桶中,用來突發(fā)消費(fèi)。
漸進(jìn)模式(SmoothWarmingUp:令牌生成速度緩慢提升直到維持在一個穩(wěn)定值):
- // 平滑限流,從冷啟動速率(滿的)到平均消費(fèi)速率的時間間隔
- 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 時間內(nèi)是否能夠獲得令牌,異步執(zhí)行。
③分布式系統(tǒng)限流(Nginx+Lua 實(shí)現(xiàn))
可以使用 resty.lock 保持原子特性,請求之間不會產(chǎn)生鎖的重入:
https://github.com/openresty/lua-resty-lock
使用 lua_shared_dict 存儲數(shù)據(jù):
- 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 -- 計數(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è)置過期時間,設(shè)置key的值為1,
- -- 過期時間為1秒
- else
- limit_counter:incr(key, 1) -- 第二次開始加1即可
- end
- lock:unlock()
- return 1
- end
- ngx.print(acquire())
作者:等不到的口琴
編輯:陶家龍
出處:https://urlify.cn/fuAB7j