限流该怎么做?
https://www.infoq.cn/article/UhixHoWebU_TYJewJwcL
有熔斷機(jī)制的系統(tǒng),它對(duì)可用性的作用至少保證了不會(huì)全盤崩潰。
但是你可以想象一個(gè)稍微極端一點(diǎn)的場景,如果系統(tǒng)流量不是很穩(wěn)定,導(dǎo)致頻繁觸發(fā)熔斷的話,是不是意味著系統(tǒng)一直熔斷的三種狀態(tài)中不斷切換。
導(dǎo)致的結(jié)果是每次從開啟熔斷到關(guān)閉熔斷的期間,必然會(huì)導(dǎo)致大量的用戶無法正常使用。系統(tǒng)層面的可用性大致是這樣的。
另外,從資源利用率上也會(huì)很容易發(fā)現(xiàn),波谷的這段時(shí)期資源是未充分利用的。
由此可見,光有熔斷是遠(yuǎn)遠(yuǎn)不夠的。
在高壓下,只要系統(tǒng)沒宕機(jī),如果能將接收的流量持續(xù)保持在高位,但又不超過系統(tǒng)所能承載的上限,會(huì)是更有效率的運(yùn)作模式,因?yàn)闀?huì)將這里的波谷填滿。
在如今的互聯(lián)網(wǎng)已經(jīng)作為社會(huì)基礎(chǔ)設(shè)施的大環(huán)境下,上面的這個(gè)場景其實(shí)離我們并不是那么遠(yuǎn),同時(shí)也會(huì)顯得沒那么極端。例如,層出不窮的營銷玩法,一個(gè)接著一個(gè)的社會(huì)熱點(diǎn),以及互聯(lián)網(wǎng)冰山之下的黑產(chǎn)、刷子的蓬勃發(fā)展,更加使得這個(gè)場景變的那么的需要去考慮、去顧忌。因?yàn)殡S時(shí)都有可能會(huì)涌入超出你預(yù)期的流量,然后壓垮你的系統(tǒng)。
那么限流的作用就很顯而易見了:只要系統(tǒng)沒宕機(jī),系統(tǒng)只是因?yàn)橘Y源不夠,而無法應(yīng)對(duì)大量的請(qǐng)求,為了保證有限的系統(tǒng)資源能夠提供最大化的服務(wù)能力,因而對(duì)系統(tǒng)按照預(yù)設(shè)的規(guī)則進(jìn)行流量(輸出或輸入)限制的一種方法,確保被接收的流量不會(huì)超過系統(tǒng)所能承載的上限。
一、怎么做「限流」
從前面聊到的內(nèi)容中我們也知道,限流最好能“限”在一個(gè)系統(tǒng)處理能力的上限附近,所以:
獲得系統(tǒng)能力的上限
第一步不是我們這次內(nèi)容的重點(diǎn),說起來就是對(duì)系統(tǒng)做一輪壓測。可以在一個(gè)獨(dú)立的環(huán)境進(jìn)行,也可以直接在生產(chǎn)環(huán)境的多個(gè)節(jié)點(diǎn)中選擇一個(gè)節(jié)點(diǎn)作為樣本來壓測,當(dāng)然需要做好與其他節(jié)點(diǎn)的隔離。
一般我們做壓測為了獲得 2 個(gè)結(jié)果,「速率」和「并發(fā)數(shù)」。前者表示在一個(gè)時(shí)間單位內(nèi)能夠處理的請(qǐng)求數(shù)量,比如 xxx 次請(qǐng)求 / 秒。后者表示系統(tǒng)在同一時(shí)刻能處理的最大請(qǐng)求數(shù)量,比如 xxx 次的并發(fā)。從指標(biāo)上需要獲得「最大值」、「平均值」或者「中位數(shù)」。后續(xù)限流策略需要設(shè)定的具體標(biāo)準(zhǔn)數(shù)值就是從這些指標(biāo)中來的。
題外話:從精益求精的角度來說,其他的諸如 cpu、網(wǎng)絡(luò)帶寬以及內(nèi)存的耗用也可以作為參照因素。
制定干預(yù)流量的策略
常用的策略就 4 種,我給它起了一個(gè)簡單的定義——「兩窗兩桶」。兩窗就是:固定窗口、滑動(dòng)窗口,兩桶就是:漏桶、令牌桶。
固定窗口
固定窗口就是定義一個(gè)“固定”的統(tǒng)計(jì)周期,比如 1 分鐘或者 30 秒、10 秒這樣。然后在每個(gè)周期統(tǒng)計(jì)當(dāng)前周期中被接收到的請(qǐng)求數(shù)量,經(jīng)過計(jì)數(shù)器累加后如果達(dá)到設(shè)定的閾值就觸發(fā)「流量干預(yù)」。直到進(jìn)入下一個(gè)周期后,計(jì)數(shù)器清零,流量接收恢復(fù)正常狀態(tài)。
這個(gè)策略最簡單,寫起代碼來也沒幾行。
復(fù)制代碼| ? | 全局變量 int totalCount = 0; // 有一個(gè)「固定周期」會(huì)觸發(fā)的定時(shí)器將數(shù)值清零。 |
| ? | ? |
| ? | if(totalCount > 限流閾值) { |
| ? | return; // 不繼續(xù)處理請(qǐng)求。 |
| ? | } |
| ? | totalCount++; |
| ? | ? |
| ? | // do something... |
固定窗口有一點(diǎn)需要注意的是,假如請(qǐng)求的進(jìn)入非常集中,那么所設(shè)定的「限流閾值」等同于你需要承受的最大并發(fā)數(shù)。所以,如果需要顧忌到并發(fā)問題,那么這里的「固定周期」設(shè)定的要盡可能的短。因?yàn)?#xff0c;這樣的話「限流閾值」的數(shù)值就可以相應(yīng)的減小。甚至,限流閾值就可以直接用并發(fā)數(shù)來指定。比如,假設(shè)固定周期是 3 秒,那么這里的閾值就可以設(shè)定為「平均并發(fā)數(shù) *3」。
不過不管怎么設(shè)定,固定窗口永遠(yuǎn)存在的缺點(diǎn)是:由于流量的進(jìn)入往往都不是一個(gè)恒定的值,所以一旦流量進(jìn)入速度有所波動(dòng),要么計(jì)數(shù)器會(huì)被提前計(jì)滿,導(dǎo)致這個(gè)周期內(nèi)剩下時(shí)間段的請(qǐng)求被“限制”。要么就是計(jì)數(shù)器計(jì)不滿,也就是「限流閾值」設(shè)定的過大,導(dǎo)致資源無法充分利用。
「滑動(dòng)窗口」可以改善這個(gè)問題。
滑動(dòng)窗口
滑動(dòng)窗口其實(shí)就是對(duì)固定窗口做了進(jìn)一步的細(xì)分,將原先的粒度切的更細(xì),比如 1 分鐘的固定窗口切分為 60 個(gè) 1 秒的滑動(dòng)窗口。然后統(tǒng)計(jì)的時(shí)間范圍隨著時(shí)間的推移同步后移。
同時(shí),我們還可以得出一個(gè)結(jié)論是:如果固定窗口的「固定周期」已經(jīng)很小了,那么使用滑動(dòng)窗口的意義也就沒有了。舉個(gè)例子,現(xiàn)在的固定窗口周期已經(jīng)是 1 秒了,再切分到毫秒級(jí)別能反而得不償失,會(huì)帶來巨大的性能和資源損耗。
滑動(dòng)窗口大致的代碼邏輯是這樣:
復(fù)制代碼| ? | 全局?jǐn)?shù)組 鏈表 [] counterList = new 鏈表 [切分的滑動(dòng)窗口數(shù)量]; |
| ? | // 有一個(gè)定時(shí)器,在每一次統(tǒng)計(jì)時(shí)間段起點(diǎn)需要變化的時(shí)候就將索引 0 位置的元素移除,并在末端追加一個(gè)新元素。 |
| ? | ? |
| ? | int sum = counterList.Sum(); |
| ? | if(sum > 限流閾值) { |
| ? | return; // 不繼續(xù)處理請(qǐng)求。 |
| ? | } |
| ? | ? |
| ? | int 當(dāng)前索引 = 當(dāng)前時(shí)間的秒數(shù) % 切分的滑動(dòng)窗口數(shù)量 ; |
| ? | counterList[當(dāng)前索引]++; |
| ? | ? |
| ? | // do something... |
雖然說滑動(dòng)窗口可以改善這個(gè)問題,但是本質(zhì)上還是預(yù)先劃定時(shí)間片的方式,屬于一種“預(yù)測”,意味著幾乎肯定無法做到 100% 的物盡其用。
但是,「桶」模式可以做的更好,因?yàn)椤竿啊鼓J街卸嗔艘粋€(gè)緩沖區(qū)(桶本身)。
漏桶
首先聊聊「漏桶」吧。漏桶模式的核心是固定“出口”的速率,不管進(jìn)來多少量,出去的速率一直是這么多。如果涌入的量多到桶都裝不下了,那么就進(jìn)行「流量干預(yù)」。
整個(gè)實(shí)現(xiàn)過程我們來分解一下。
這樣一來,你會(huì)發(fā)現(xiàn)本質(zhì)就是:通過一個(gè)緩沖區(qū)將不平滑的流量“整形”成平滑的(高于均值的流量暫存下來補(bǔ)足到低于均值的時(shí)期),以此最大化計(jì)算處理資源的利用率。
實(shí)現(xiàn)代碼的簡化表示如下:
復(fù)制代碼| ? | 全局變量 int unitSpeed; // 出口當(dāng)前的流出速率。每隔一個(gè)速率計(jì)算周期(比如 1 秒)會(huì)觸發(fā)定時(shí)器將數(shù)值清零。 |
| ? | 全局變量 int waterLevel; // 當(dāng)前緩沖區(qū)的水位線。 |
| ? | ? |
| ? | if(unitSpeed < 速率閾值) { |
| ? | unitSpeed++; |
| ? | ? |
| ? | //do something... |
| ? | } |
| ? | else{ |
| ? | if(waterLevel > 水位閾值){ |
| ? | return; // 不繼續(xù)處理請(qǐng)求。 |
| ? | } |
| ? | ? |
| ? | waterLevel++; |
| ? | ? |
| ? | while(unitSpeed >= 速率閾值){ |
| ? | sleep(一小段時(shí)間)。 |
| ? | } |
| ? | unitSpeed++; |
| ? | waterLevel--; |
| ? | ? |
| ? | //do something... |
| ? | } |
更優(yōu)秀的「漏桶」策略已經(jīng)可以在流量的總量充足的情況下發(fā)揮你所預(yù)期的 100% 處理能力,但這還不是極致。
你應(yīng)該知道,一個(gè)程序所在的運(yùn)行環(huán)境中,往往不單單只有這個(gè)程序本身,會(huì)存在一些系統(tǒng)進(jìn)程甚至是其它的用戶進(jìn)程。也就是說,程序本身的處理能力是會(huì)被干擾的,是會(huì)變化的。所以,你可以預(yù)估某一個(gè)階段內(nèi)的平均值、中位數(shù),但無法預(yù)估具體某一個(gè)時(shí)刻的程序處理能力。又因此,你必然會(huì)使用相對(duì)悲觀的標(biāo)準(zhǔn)去作為閾值,防止程序超負(fù)荷。
那么從資源利用率來說,有沒有更優(yōu)秀的方案呢?有,這就是「令牌桶」。
令牌桶
令牌桶模式的核心是固定“進(jìn)口”速率。先拿到令牌,再處理請(qǐng)求,拿不到令牌就被「流量干預(yù)」。因此,當(dāng)大量的流量進(jìn)入時(shí),只要令牌的生成速度大于等于請(qǐng)求被處理的速度,那么此刻的程序處理能力就是極限。
也來分解一下它的實(shí)現(xiàn)過程。
大致的代碼簡化表示如下(看上去像「固定窗口」的反向邏輯):
全局變量 int tokenCount = 令牌數(shù)閾值 ; // 可用令牌數(shù)。有一個(gè)獨(dú)立的線程用固定的頻率增加這個(gè)數(shù)值,但不大于「令牌數(shù)閾值」。
復(fù)制代碼| ? | if(tokenCount == 0){ |
| ? | return; // 不繼續(xù)處理請(qǐng)求。 |
| ? | } |
| ? | ? |
| ? | tokenCount--; |
| ? | ? |
| ? | //do something... |
聰明的你可能也會(huì)想到,這樣一來令牌桶的容量大小理論上就是程序需要支撐的最大并發(fā)數(shù)。的確如此,假設(shè)同一時(shí)刻進(jìn)入的流量將令牌取完,但是程序來不及處理,將會(huì)導(dǎo)致事故發(fā)生。
所以,沒有真正完美的策略,只有合適的策略。因此,根據(jù)不同的場景能夠識(shí)別什么是最合適的策略是更需要鍛煉的能力。下面 z 哥分享一些我個(gè)人的經(jīng)驗(yàn)。
二、做「限流」的最佳實(shí)踐
四種策略該如何選擇?
首先,固定窗口。一般來說,如非時(shí)間緊迫,不建議選擇這個(gè)方案,太過生硬。但是,為了能快速止損眼前的問題可以作為臨時(shí)應(yīng)急的方案。
其次,滑動(dòng)窗口。這個(gè)方案適用于對(duì)異常結(jié)果「高容忍」的場景,畢竟相比“兩窗”少了一個(gè)緩沖區(qū)。但是,勝在實(shí)現(xiàn)簡單。
然后,漏桶。z 哥覺得這個(gè)方案最適合作為一個(gè)通用方案。雖說資源的利用率上不是極致,但是「寬進(jìn)嚴(yán)出」的思路在保護(hù)系統(tǒng)的同時(shí)還留有一些余地,使得它的適用場景更廣。
最后,令牌桶。當(dāng)你需要盡可能的壓榨程序的性能(此時(shí)桶的最大容量必然會(huì)大于等于程序的最大并發(fā)能力),并且所處的場景流量進(jìn)入波動(dòng)不是很大(不至于一瞬間取完令牌,壓垮后端系統(tǒng))。
分布式系統(tǒng)中帶來的新挑戰(zhàn)
一個(gè)成熟的分布式系統(tǒng)大致是這樣的。
每一個(gè)上游系統(tǒng)都可以理解為是其下游系統(tǒng)的客戶端。然后我們回想一下前面的內(nèi)容,可能你發(fā)現(xiàn)了,前面聊的「限流」都沒有提到到底是在客戶端做限流還是服務(wù)端做,甚至看起來更傾向是建立在服務(wù)端的基礎(chǔ)上做。但是你知道,在一個(gè)分布式系統(tǒng)中,一個(gè)服務(wù)端本身就可能存在多個(gè)副本,并且還會(huì)提供給多個(gè)客戶端調(diào)用,甚至其自身也會(huì)作為客戶端角色。那么,在如此交錯(cuò)復(fù)雜的一個(gè)環(huán)境中,該如何下手做限流呢?我的思路是通過「一縱一橫」來考量。
縱
都知道「限流」是一個(gè)保護(hù)措施,那么可以將它想象成一個(gè)盾牌。另外,一個(gè)請(qǐng)求在系統(tǒng)中的處理過程是鏈?zhǔn)降摹D敲?#xff0c;正如古時(shí)候軍隊(duì)打仗一樣,盾牌兵除了有小部分在老大周圍保護(hù),剩下的全在最前線。因?yàn)槎艿奈恢迷角?#xff0c;能受益的范圍越大。
分布式系統(tǒng)中最前面的是什么?接入層。如果你的系統(tǒng)有接入層,比如用 nginx 做的反向代理。那么可以通過它的 ngx_http_limit_conn_module 以及 ngx_http_limit_req_module 來做限流,是很成熟的一個(gè)解決方案。
如果沒有接入層,那么只能在應(yīng)用層以 AOP 的思路去做了。但是,由于應(yīng)用是分散的,出于成本考慮你需要針對(duì)性的去做限流。比如 ToC 的應(yīng)用必然比 ToB 的應(yīng)用更需要做,高頻的緩存系統(tǒng)必然比低頻的報(bào)表系統(tǒng)更需要做,Web 應(yīng)用由于存在 Filter 的機(jī)制做起來必然比 Service 應(yīng)用更方便。
那么應(yīng)用間的限流到底是做到客戶端還是服務(wù)端呢?
z 哥的觀點(diǎn)是,從效果上客戶端模式肯定是優(yōu)于服務(wù)端模式的,因?yàn)楫?dāng)處于被限流狀態(tài)的時(shí)候,客戶端模式連建立連接的動(dòng)作都省了。另一個(gè)潛在的好處是,與集中式的服務(wù)端模式相比,可以把少數(shù)的服務(wù)端程序的壓力分散掉。但是在客戶端做成本也更高,因?yàn)樗侨ブ行幕?#xff0c;假如需要多個(gè)節(jié)點(diǎn)之間的數(shù)據(jù)共通的話,是一個(gè)很麻煩的事情。
所以,最終 z 哥建議你:如果考慮成本就服務(wù)端模式,考慮效果就客戶端模式。當(dāng)然也不是絕對(duì),比如一個(gè)服務(wù)端的流量大部分都來源于某一個(gè)客戶端,那么就可以直接在這個(gè)客戶端做限流,這也不失為一個(gè)好方案。
數(shù)據(jù)庫層面的話,一般連接字符串中本身就會(huì)包含「最大連接數(shù)」的概念,就可以起到限流的作用。如果想做更精細(xì)的控制就只能做到統(tǒng)一封裝的數(shù)據(jù)庫訪問層框架中了。
聊完了「縱」,那么「橫」是什么呢?
橫
不管是多個(gè)客戶端,還是同一個(gè)服務(wù)端的多個(gè)副本。每個(gè)節(jié)點(diǎn)的性能必然會(huì)存在差異,如何設(shè)立合適的閾值?以及如何讓策略的變更盡可能快的在集群中的多個(gè)節(jié)點(diǎn)生效?說起來很簡單,引入一個(gè)性能監(jiān)控平臺(tái)和配置中心。但這些真真要做好不容易,后續(xù)我們?cè)僬归_這塊內(nèi)容。
三、總結(jié)
限流就好比保險(xiǎn)絲,根據(jù)你制定的標(biāo)準(zhǔn),達(dá)到了就拉閘。
不過,觸發(fā)限流后的措施除了直接丟棄請(qǐng)求之外,還有一個(gè)方式是「降級(jí)」,那么降級(jí)有哪些方式呢?我們下一篇再聊吧。
轉(zhuǎn)載于:https://www.cnblogs.com/davidwang456/articles/10132640.html
總結(jié)
- 上一篇: 99% 人看得懂的“熔断”以及最佳实践
- 下一篇: Sentinel 与 Hystrix、r