分佈式系統關注點(9)——想通關「限流」?只要這一篇

若是這是第二次看到個人文章,歡迎點擊文末連接訂閱個人我的公衆號(跨界架構師)喲~

每週五11:45 按時送達。固然了,也會時不時加個餐~nginx

本文長度爲3319字,建議閱讀9分鐘。程序員


可能你在網上看過很多「限流」相關的文章,可是z哥的這篇多是最全面,最深刻淺出的一篇了(容我飄幾秒~)。數據庫

開個玩笑,但願你能收穫一些增量價值就好~。後端


以前有了解到z哥的一部分讀者們沒有充分搞清楚「限流」和「熔斷」的關係。咱們先來思考一個問題,生活中也有限流,爲何國慶春節長假熱門景點要限流?而不是一早先開幾小時,若是人多了就關幾小時,人少了就再開呢?其實這就是限流和熔斷表象上的一個區別。數組


在上一篇中咱們聊到了「熔斷」(分佈式系統關注點——99%的人都能看懂的「熔斷」以及最佳實踐),有熔斷機制的系統,它對可用性的做用至少保證了不會全盤崩潰。緩存


可是你能夠想象一個稍微極端一點的場景,若是系統流量不是很穩定,致使頻繁觸發熔斷的話,是否是意味着系統一直熔斷的三種狀態中不斷切換。微信

致使的結果是每次從開啓熔斷到關閉熔斷的期間,必然會致使大量的用戶沒法正常使用。系統層面的可用性大體是這樣的。網絡

另外,從資源利用率上也會很容易發現,波谷的這段時期資源是未充分利用的。架構

因而可知,光有熔斷是遠遠不夠的。併發

在高壓下,只要系統沒宕機,若是能將接收的流量持續保持在高位,但又不超過系統所能承載的上限,會是更有效率的運做模式,由於會將這裏的波谷填滿。

在現在的互聯網已經做爲社會基礎設施的大環境下,上面的這個場景其實離咱們並非那麼遠,同時也會顯得沒那麼極端。例如,層出不窮的營銷玩法,一個接着一個的社會熱點,以及互聯網冰山之下的黑產、刷子的蓬勃發展,更加使得這個場景變的那麼的須要去考慮、去顧忌。由於隨時都有可能會涌入超出你預期的流量,而後壓垮你的系統。

那麼限流的做用就很顯而易見了:只要系統沒宕機,系統只是由於資源不夠,而沒法應對大量的請求,爲了保證有限的系統資源可以提供最大化的服務能力,於是對系統按照預設的規則進行流量(輸出或輸入)限制的一種方法,確保被接收的流量不會超過系統所能承載的上限。


1、怎麼作「限流」

從前面聊到的內容中咱們也知道,限流最好能「限」在一個系統處理能力的上限附近,因此:

  1. 經過「壓力測試」等方式得到系統的能力上限在哪一個水平是第一步。

  2. 其次,就是制定干預流量的策略。好比標準該怎麼定、是否只注重結果仍是也要注重過程的平滑性等。

  3. 最後,就是處理「被幹預掉」的流量。能不能直接丟棄?不能的話該如何處理?


得到系統能力的上限

第一步不是咱們此次內容的重點,提及來就是對系統作一輪壓測。能夠在一個獨立的環境進行,也能夠直接在生產環境的多個節點中選擇一個節點做爲樣原本壓測,固然須要作好與其餘節點的隔離。

通常咱們作壓測爲了得到2個結果,「速率」和「併發數」。前者表示在一個時間單位內可以處理的請求數量,好比xxx次請求/秒。後者表示系統在同一時刻能處理的最大請求數量,好比xxx次的併發。從指標上須要得到「最大值」、「平均值」或者「中位數」。後續限流策略須要設定的具體標準數值就是從這些指標中來的。

題外話:從精益求精的角度來講,其餘的諸如cpu、網絡帶寬以及內存的耗用也能夠做爲參照因素。


制定干預流量的策略

經常使用的策略就4種,我給它起了一個簡單的定義——「兩窗兩桶」。兩窗就是:固定窗口、滑動窗口,兩桶就是:漏桶、令牌桶。

固定窗口

固定窗口就是定義一個「固定」的統計週期,好比1分鐘或者30秒、10秒這樣。而後在每一個週期統計當前週期中被接收到的請求數量,通過計數器累加後若是達到設定的閾值就觸發「流量干預」。直到進入下一個週期後,計數器清零,流量接收恢復正常狀態。

這個策略最簡單,寫起代碼來也沒幾行。

全局變量 int totalCount = 0; //有一個「固定週期」會觸發的定時器將數值清零。

if(totalCount > 限流閾值) {

return; //不繼續處理請求。

}

totalCount++;

// do something...

固定窗口有一點須要注意的是,假如請求的進入很是集中,那麼所設定的「限流閾值」等同於你須要承受的最大併發數。因此,若是須要顧忌到併發問題,那麼這裏的「固定週期」設定的要儘量的短。由於,這樣的話「限流閾值」的數值就能夠相應的減少。甚至,限流閾值就能夠直接用併發數來指定。好比,假設固定週期是3秒,那麼這裏的閾值就能夠設定爲「平均併發數*3」。

不過無論怎麼設定,固定窗口永遠存在的缺點是:因爲流量的進入每每都不是一個恆定的值,因此一旦流量進入速度有所波動,要麼計數器會被提早計滿,致使這個週期內剩下時間段的請求被「限制」。要麼就是計數器計不滿,也就是「限流閾值」設定的過大,致使資源沒法充分利用

「滑動窗口」能夠改善這個問題。


滑動窗口

滑動窗口其實就是對固定窗口作了進一步的細分,將原先的粒度切的更細,好比1分鐘的固定窗口切分爲60個1秒的滑動窗口。而後統計的時間範圍隨着時間的推移同步後移。

同時,咱們還能夠得出一個結論是:若是固定窗口的「固定週期」已經很小了,那麼使用滑動窗口的意義也就沒有了。舉個例子,如今的固定窗口週期已是1秒了,再切分到毫秒級別能反而得不償失,會帶來巨大的性能和資源損耗。

滑動窗口大體的代碼邏輯是這樣:

全局數組 鏈表[] counterList = new 鏈表[切分的滑動窗口數量];

//有一個定時器,在每一次統計時間段起點須要變化的時候就將索引0位置的元素移除,並在末端追加一個新元素。

int sum = counterList.Sum();

if(sum > 限流閾值) {

return; //不繼續處理請求。

}

int 當前索引 = 當前時間的秒數 % 切分的滑動窗口數量;

counterList[當前索引]++;

// do something...

雖說滑動窗口能夠改善這個問題,可是本質上仍是預先劃定時間片的方式,屬於一種「預測」,意味着幾乎確定沒法作到100%的物盡其用。

可是,「桶」模式能夠作的更好,由於「桶」模式中多了一個緩衝區(桶自己)。


漏桶

首先聊聊「漏桶」吧。漏桶模式的核心是固定「出口」的速率,無論進來多少許,出去的速率一直是這麼多。若是涌入的量多到桶都裝不下了,那麼就進行「流量干預」。

整個實現過程咱們來分解一下。

  1. 控制流出的速率。這個其實可使用前面提到的兩個「窗口」的思路來實現。若是當前速率小於閾值則直接處理請求,不然不直接處理請求,進入緩衝區,並增長當前水位。

  2. 緩衝的實現能夠作一個短暫的休眠或者記錄到一個容器中再作異步的重試。

  3. 最後控制桶中的水位不超過最大水位。這個很簡單,就是一個全局計數器,進行加加減減。

這樣一來,你會發現本質就是:經過一個緩衝區將不平滑的流量「整形」成平滑的(高於均值的流量暫存下來補足到低於均值的時期),以此最大化計算處理資源的利用率

實現代碼的簡化表示以下:

全局變量 int unitSpeed; //出口當前的流出速率。每隔一個速率計算週期(好比1秒)會觸發定時器將數值清零。

全局變量 int waterLevel; //當前緩衝區的水位線。

if(unitSpeed < 速率閾值) {

unitSpeed++;

//do something...

}

else{

if(waterLevel > 水位閾值){

return; //不繼續處理請求。

}

waterLevel++;

while(unitSpeed >= 速率閾值){

sleep(一小段時間)。

}

unitSpeed++;

waterLevel--;

//do something...

}

更優秀的「漏桶」策略已經能夠在流量的總量充足的狀況下發揮你所預期的100%處理能力,但這還不是極致。

你應該知道,一個程序所在的運行環境中,每每不僅僅只有這個程序自己,會存在一些系統進程甚至是其它的用戶進程。也就是說,程序自己的處理能力是會被幹擾的,是會變化的。因此,你能夠預估某一個階段內的平均值、中位數,但沒法預估具體某一個時刻的程序處理能力。又所以,你必然會使用相對悲觀的標準去做爲閾值,防止程序超負荷。

那麼從資源利用率來講,有沒有更優秀的方案呢?有,這就是「令牌桶」。


令牌桶

令牌桶模式的核心是固定「進口」速率。先拿到令牌,再處理請求,拿不到令牌就被「流量干預」。所以,當大量的流量進入時,只要令牌的生成速度大於等於請求被處理的速度,那麼此刻的程序處理能力就是極限

也來分解一下它的實現過程。

  1. 控制令牌生成的速率,並放入桶中。這個其實就是單獨一個線程在不斷的生成令牌。

  2. 控制桶中待領取的令牌水位不超過最大水位。這個和「漏桶」同樣,就是一個全局計數器,進行加加減減。

大體的代碼簡化表示以下(看上去像「固定窗口」的反向邏輯):

全局變量 int tokenCount = 令牌數閾值; //可用令牌數。有一個獨立的線程用固定的頻率增長這個數值,但不大於「令牌數閾值」。

if(tokenCount == 0){

return; //不繼續處理請求。

}

tokenCount--;

//do something...

聰明的你可能也會想到,這樣一來令牌桶的容量大小理論上就是程序須要支撐的最大併發數。的確如此,假設同一時刻進入的流量將令牌取完,可是程序來不及處理,將會致使事故發生。

因此,沒有真正完美的策略,只有合適的策略。所以,根據不一樣的場景可以識別什麼是最合適的策略是更須要鍛鍊的能力。下面z哥分享一些我我的的經驗。


2、作「限流」的最佳實踐

四種策略該如何選擇?

首先,固定窗口。通常來講,如非時間緊迫,不建議選擇這個方案,太過生硬。可是,爲了能快速止損眼前的問題能夠做爲臨時應急的方案。

其次,滑動窗口。這個方案適用於對異常結果「高容忍」的場景,畢竟相比「兩窗」少了一個緩衝區。可是,勝在實現簡單。

而後,漏桶。z哥以爲這個方案最適合做爲一個通用方案。雖然說資源的利用率上不是極致,可是「寬進嚴出」的思路在保護系統的同時還留有一些餘地,使得它的適用場景更廣。

最後,令牌桶。當你須要儘量的壓榨程序的性能(此時桶的最大容量必然會大於等於程序的最大併發能力),而且所處的場景流量進入波動不是很大(不至於一瞬間取完令牌,壓垮後端系統)。


分佈式系統中帶來的新挑戰

一個成熟的分佈式系統大體是這樣的。

每個上游系統均可以理解爲是其下游系統的客戶端。而後咱們回想一下前面的內容,可能你發現了,前面聊的「限流」都沒有提到究竟是在客戶端作限流仍是服務端作,甚至看起來更傾向是創建在服務端的基礎上作。可是你知道,在一個分佈式系統中,一個服務端自己就可能存在多個副本,而且還會提供給多個客戶端調用,甚至其自身也會做爲客戶端角色。那麼,在如此交錯複雜的一個環境中,該如何下手作限流呢?個人思路是經過「一縱一橫」來考量。


都知道「限流」是一個保護措施,那麼能夠將它想象成一個盾牌。另外,一個請求在系統中的處理過程是鏈式的。那麼,正如古時候軍隊打仗同樣,盾牌兵除了有小部分在老大周圍保護,剩下的全在最前線。由於盾的位置越前,能受益的範圍越大


分佈式系統中最前面的是什麼?接入層。若是你的系統有接入層,好比用nginx作的反向代理。那麼能夠經過它的ngx_http_limit_conn_module以及ngx_http_limit_req_module來作限流,是很成熟的一個解決方案。

若是沒有接入層,那麼只能在應用層以AOP的思路去作了。可是,因爲應用是分散的,出於成本考慮你須要針對性的去作限流。好比ToC的應用必然比ToB的應用更須要作,高頻的緩存系統必然比低頻的報表系統更須要作,Web應用因爲存在Filter的機制作起來必然比Service應用更方便。


那麼應用間的限流究竟是作到客戶端仍是服務端呢?

z哥的觀點是,從效果上客戶端模式確定是優於服務端模式的,由於當處於被限流狀態的時候,客戶端模式連創建鏈接的動做都省了。另外一個潛在的好處是,與集中式的服務端模式相比,能夠把少數的服務端程序的壓力分散掉。可是在客戶端作成本也更高,由於它是去中心化的,假如須要多個節點之間的數據共通的話,是一個很麻煩的事情。

因此,最終z哥建議你:若是考慮成本就服務端模式,考慮效果就客戶端模式。固然也不是絕對,好比一個服務端的流量大部分都來源於某一個客戶端,那麼就能夠直接在這個客戶端作限流,這也不失爲一個好方案。


數據庫層面的話,通常鏈接字符串中自己就會包含「最大鏈接數」的概念,就能夠起到限流的做用。若是想作更精細的控制就只能作到統一封裝的數據庫訪問層框架中了。

聊完了「縱」,那麼「橫」是什麼呢?


無論是多個客戶端,仍是同一個服務端的多個副本。每一個節點的性能必然會存在差別,如何設立合適的閾值?以及如何讓策略的變動儘量快的在集羣中的多個節點生效?提及來很簡單,引入一個性能監控平臺和配置中心。但這些真真要作好不容易,後續咱們再展開這塊內容。


3、總結

限流就比如保險絲,根據你制定的標準,達到了就拉閘。

不過,觸發限流後的措施除了直接丟棄請求以外,還有一個方式是「降級」,那麼降級有哪些方式呢?咱們下一篇再聊吧。



Question:
你在工做中有遇到過什麼場景須要作「限流」嗎?歡迎分享交流一下。



相關文章:



▶ 關於做者:張帆(Zachary,我的微信號:Zachary-ZF)。堅持用心打磨每一篇高質量原創。本文首發於公衆號:「 跨界架構師」(ID:Zachary_ZF)。  
若是你是初級程序員,想提高但不知道如何下手。又或者作程序員多年,陷入了一些瓶頸想拓寬一下視野。歡迎關注個人公衆號「 跨界架構師」,回覆「技術」,送你一份我長期收集和整理的思惟導圖。 
若是你是運營,面對不斷變化的市場一籌莫展。又或者想了解主流的運營策略,以豐富本身的「倉庫」。歡迎關注個人公衆號「 跨界架構師」,回覆「運營」,送你一份我長期收集和整理的思惟導圖。

微信公衆號(首發):跨界架構師<-- 點擊後閱讀熱門文章

按期發表原創內容:架構設計丨分佈式系統丨產品丨運營丨一些深度思考

相關文章
相關標籤/搜索