究竟什么是限流
限流指的是通過限制到達系統的並發請求數量,保證系統能夠正常響應部分用戶請求,而對於超過限制的流量,則只能通過拒絕服務的方式保證整體系統的可用性。限流策略一般部署在服務的入口層,比如API網關中,這樣可以對系統整體流量做塑形。而在微服務架構中,也可以在RPC客戶端中引入限流的策略,來保證單個服務不會被過大的流量壓垮。
在TCP協議中有一個滑動窗口的概念,可以實現對網絡傳輸流量的控制。如果沒有流量控制,當流量接收方處理速度變慢而發送方還是繼續以之前的速率發送數據,那么必然會導致流量擁塞。而TCP的滑動窗口實際上可以理解為接收方所能提供的緩沖區的大小。
在接收方回復發送方的ACK消息中,會帶上這個窗口的大小。這樣,發送方就可以通過這個滑動窗口的大小決定發送數據的速率了。如果接收方處理了一些緩沖區的數據,那么這個滑動窗口就會變大,發送方發送數據的速率就會提升;反之,如果接收方接收了一些數據還沒有來得及處理,那么這個滑動窗口就會減小,發送方發送數據的速率就會減慢。
而無論是在一體化架構還是微服務化架構中,也可以在多個維度上對到達系統的流量做控制,比如:
- 可以對系統每分鍾處理多少請求做限制;
- 可以針對單個接口設置每分鍾請求流量的限制;
- 可以限制單個IP、用戶ID或者設備ID在一段時間內發送請求的數量;
對於服務於多個第三方應用的開放平台來說,每一個第三方應用對於平台方來說都有一個唯一的appkey來標識,那么你也可以限制單個appkey的訪問接口的速率。
限流算法(類似這篇文章https://www.cnblogs.com/wt645631686/p/6868845.html)
固定窗口與滑動窗口的算法
限流的目的是限制一段時間內發向系統的總體請求量,比如,限制一分鍾之內系統只能承接1萬次請求,那么最暴力的一種方式就是記錄這一分鍾之內訪問系統的請求量有多少,如果超過了1萬次的限制,那么就觸發限流的策略返回請求失敗的錯誤。如果這一分鍾的請求量沒有達到限制,那么在下一分鍾到來的時候先重置請求量的計數,再統計這一分鍾的請求量是否超過限制。
這種算法雖然實現非常簡單,但是卻有一個很大的缺陷 :無法限制短時間之內的集中流量。假如我們需要限制每秒鍾只能處理10次請求,如果前一秒鍾產生了10次請求,這10次請求全部集中在最后的10毫秒中,而下一秒鍾的前10毫秒也產生了10次請求,那么在這20毫秒中就產生了20次請求,超過了限流的閾值。但是因為這20次請求分布在兩個時間窗口內,所以沒有觸發限流,這就造成了限流的策略並沒有生效。
為了解決這個缺陷,就有了基於滑動窗口的算法。 這個算法的原理是將時間的窗口划分為多個小窗口,每個小窗口中都有單獨的請求計數。比如下面這張圖,我們將1s的時間窗口划分為5份,每一份就是200ms;那么當在1s和1.2s之間來了一次新的請求時,我們就需要統計之前的一秒鍾內的請求量,也就是0.2s~1.2s這個區間的總請求量,如果請求量超過了限流閾值那么就執行限流策略。
滑動窗口的算法解決了臨界時間點上突發流量無法控制的問題,但是卻因為要存儲每個小的時間窗口內的計數,所以空間復雜度有所增加。
雖然滑動窗口算法解決了窗口邊界的大流量的問題,但是它和固定窗口算法一樣,還是無法限制短時間之內的集中流量,也就是說無法控制流量讓它們更加平滑。因此,在實際的項目中,我很少使用基於時間窗口的限流算法,而是使用其他限流的算法:一種算法叫做漏桶算法,一種叫做令牌筒算法。
漏桶算法與令牌筒算法
漏桶算法的原理很簡單,它就像在流量產生端和接收端之間增加一個漏桶,流量會進入和暫存到漏桶里面,而漏桶的出口處會按照一個固定的速率將流量漏出到接收端(也就是服務接口)。
如果流入的流量在某一段時間內大增,超過了漏桶的承受極限,那么多余的流量就會觸發限流策略,被拒絕服務。
經過了漏桶算法之后,隨機產生的流量就會被整形成為比較平滑的流量到達服務端,從而避免了突發的大流量對於服務接口的影響。這很像倚天屠龍記里,九陽真經的口訣:他強由他強,清風拂山崗,他橫由他橫,明月照大江 。 也就是說,無論流入的流量有多么強橫,多么不規則,經過漏桶處理之后,流出的流量都會變得比較平滑。
而在實現時,一般會使用消息隊列作為漏桶的實現,流量首先被放入到消息隊列中排隊,由固定的幾個隊列處理程序來消費流量,如果消息隊列中的流量溢出,那么后續的流量就會被拒絕。這個算法的思想是不是與消息隊列削峰填谷的作用相似呢?
另一種令牌桶算法的基本算法是這樣的:
- 如果需要在一秒內限制訪問次數為N次,那么就每隔1/N的時間,往桶內放入一個令牌;
- 在處理請求之前先要從桶中獲得一個令牌,如果桶中已經沒有了令牌,那么就需要等待新的令牌或者直接拒絕服務;
- 桶中的令牌總數也要有一個限制,如果超過了限制就不能向桶中再增加新的令牌了。這樣可以限制令牌的總數,一定程度上可以避免瞬時流量高峰的問題。
如果要從這兩種算法中做選擇,更傾向於使用令牌桶算法,原因是漏桶算法在面對突發流量的時候,采用的解決方式是緩存在漏桶中, 這樣流量的響應時間就會增長,這就與互聯網業務低延遲的要求不符;而令牌桶算法可以在令牌中暫存一定量的令牌,能夠應對一定的突發流量,所以一般我會使用令牌桶算法來實現限流方案,而Guava中的限流方案就是使用令牌桶算法來實現的。
使用令牌桶算法就需要存儲令牌的數量,如果是單機上實現限流的話,可以在進程中使用一個變量來存儲;但是如果在分布式環境下,不同的機器之間無法共享進程中的變量,就一般會使用Redis來存儲這個令牌的數量。這樣的話,每次請求的時候都需要請求一次Redis來獲取一個令牌,會增加幾毫秒的延遲,性能上會有一些損耗。因此,一個折中的思路是: 可以在每次取令牌的時候,不再只獲取一個令牌,而是獲取一批令牌,這樣可以盡量減少請求Redis的次數。
- 限流是一種常見的服務保護策略,你可以在整體服務、單個服務、單個接口、單個IP或者單個用戶等多個維度進行流量的控制;
- 基於時間窗口維度的算法有固定窗口算法和滑動窗口算法,兩者雖然能一定程度上實現限流的目的,但是都無法讓流量變得更平滑;
- 令牌桶算法和漏桶算法則能夠塑形流量,讓流量更加平滑,但是令牌桶算法能夠應對一定的突發流量,所以在實際項目中應用更多。
限流策略是微服務治理中的標配策略,只是你很難在實際中確認限流的閾值是多少,設置的小了容易誤傷正常的請求,設置的大了則達不到限流的目的。所以,一般在實際項目中,我們會把閾值放置在配置中心中方便動態調整;同時,可以通過定期地壓力測試得到整體系統以及每個微服務的實際承載能力,然后再依據這個壓測出來的值設置合適的閾值。