你們好,我是 yes。java
今天來講說限流的相關內容,包括常見的限流算法、單機限流場景、分佈式限流場景以及一些常見限流組件。web
固然在介紹限流算法和具體場景以前咱們先得明確什麼是限流,爲何要限流?。算法
任何技術都要搞清它的來源,技術的產生來自痛點,明確痛點咱們才能抓住關鍵對症下藥。後端
限流是什麼?
首先來解釋下什麼是限流?服務器
在平常生活中限流很常見,例如去有些景區玩,天天售賣的門票數是有限的,例如 2000 張,即天天最多隻有 2000 我的能進去遊玩。微信
題外話:我以前看到個新聞,最不想賣門票的景區「盧旺達火山公園」,天天就賣 32 張,而且每張門票須要 1 萬元!網絡
再回到主題,那在咱們工程上限流是什麼呢?限制的是 「流」,在不一樣場景下「流」的定義不一樣,能夠是每秒請求數、每秒事務處理數、網絡流量等等。併發
而一般咱們說的限流指代的是 限制到達系統的併發請求數,使得系統可以正常的處理 部分 用戶的請求,來保證系統的穩定性。框架
限流不可避免的會形成用戶的請求變慢或者被拒的狀況,從而會影響用戶體驗。所以限流是須要在用戶體驗和系統穩定性之間作平衡的,即咱們常說的 trade off
。編輯器
對了,限流也稱流控(流量控制)。
爲何要限流?
前面咱們有提到限流是爲了保證系統的穩定性。
平常的業務上有相似秒殺活動、雙十一大促或者突發新聞等場景,用戶的流量突增,後端服務的處理能力是有限的,若是不能處理好突發流量,後端服務很容易就被打垮。
亦或是爬蟲等不正常流量,咱們對外暴露的服務都要以最大惡意去防備咱們的調用者。咱們不清楚調用者會如何調用咱們的服務。假設某個調用者開幾十個線程一天二十四小時瘋狂調用你的服務,不作啥處理咱服務也算完了。更勝的還有 DDos 攻擊。
還有對於不少第三方開發平臺來講,不只僅是爲了防備不正常流量,也是爲了資源的公平利用,有些接口都免費給你用了,資源都不可能一直都被你佔着吧,別人也得調的。
固然加錢的話好商量。
在以前公司還作過一個系統,當時SaaS版本還沒出來。所以系統須要部署到客戶方。
當時老闆的要求是,咱們須要給他個限流降級版本,不但系統出的方案是降級後的方案,核心接口天天最多隻能調用20次,還須要限制系統所在服務器的配置和數量,即限制部署的服務器的CPU核數等,還限制全部部署的服務器數量,防止客戶集羣部署,提升系統的性能。
固然這一切須要能動態配置,由於加錢的話好商量。客戶一直都不知道。
估計老闆在等客戶說感受系統有點慢吧。而後就搞個2.0版本?我讓咱們研發部加班加點給你搞出來。
小結一下,限流的本質是由於後端處理能力有限,須要截掉超過處理能力以外的請求,亦或是爲了均衡客戶端對服務端資源的公平調用,防止一些客戶端餓死。
常見的限流算法
有關限流算法我給出了對應的圖解和相關僞代碼,有些人喜歡看圖,有些人更喜歡看代碼。
計數限流
最簡單的限流算法就是計數限流了,例如系統能同時處理100個請求,保存一個計數器,處理了一個請求,計數器加一,一個請求處理完畢以後計數器減一。
每次請求來的時候看看計數器的值,若是超過閾值要麼拒絕。
很是的簡單粗暴,計數器的值要是存內存中就算單機限流算法。存中心存儲裏,例如 Redis 中,集羣機器訪問就算分佈式限流算法。
優勢就是:簡單粗暴,單機在 Java 中可用 Atomic 等原子類、分佈式就 Redis incr。
缺點就是:假設咱們容許的閾值是1萬,此時計數器的值爲0, 當1萬個請求在前1秒內一古腦兒的都涌進來,這突發的流量但是頂不住的。緩緩的增長處理和一會兒涌入對於程序來講是不同的。
並且通常的限流都是爲了限制在指定時間間隔內的訪問量,所以還有個算法叫固定窗口。
固定窗口限流
它相比於計數限流主要是多了個時間窗口的概念。計數器每過一個時間窗口就重置。規則以下:
-
請求次數小於閾值,容許訪問而且計數器 +1; -
請求次數大於閾值,拒絕訪問; -
這個時間窗口過了以後,計數器清零;
看起來好像很完美,實際上仍是有缺陷的。
固定窗口臨界問題
假設系統每秒容許 100 個請求,假設第一個時間窗口是 0-1s,在第 0.55s 處一下次涌入 100 個請求,過了 1 秒的時間窗口後計數清零,此時在 1.05 s 的時候又一下次涌入100個請求。
雖然窗口內的計數沒超過閾值,可是全局來看在 0.55s-1.05s 這 0.1 秒內涌入了 200 個請求,這其實對於閾值是 100/s 的系統來講是沒法接受的。
爲了解決這個問題引入了滑動窗口限流。
滑動窗口限流
滑動窗口限流解決固定窗口臨界值的問題,能夠保證在任意時間窗口內都不會超過閾值。
相對於固定窗口,滑動窗口除了須要引入計數器以外還須要記錄時間窗口內每一個請求到達的時間點,所以對內存的佔用會比較多。
規則以下,假設時間窗口爲 1 秒:
-
記錄每次請求的時間 -
統計每次請求的時間 至 往前推1秒這個時間窗口內請求數,而且 1 秒前的數據能夠刪除。 -
統計的請求數小於閾值就記錄這個請求的時間,並容許經過,反之拒絕。
可是滑動窗口和固定窗口都沒法解決短期以內集中流量的突擊。
咱們所想的限流場景,例如每秒限制 100 個請求。但願請求每 10ms 來一個,這樣咱們的流量處理就很平滑,可是真實場景很難控制請求的頻率。所以可能存在 5ms 內就打滿了閾值的狀況。
固然對於這種狀況仍是有變型處理的,例如設置多條限流規則。不只限制每秒 100 個請求,再設置每 10ms 不超過 2 個。
再多說一句,這個滑動窗口可與TCP的滑動窗口不同。TCP的滑動窗口是接收方告知發送方本身能接多少「貨」,而後發送方控制發送的速率。
接下來再說說漏桶,它能夠解決時間窗口類算法的痛點,使得流量更加的平滑。
漏桶算法
以下圖所示,水滴持續滴入漏桶中,底部定速流出。若是水滴滴入的速率大於流出的速率,當存水超過桶的大小的時候就會溢出。
規則以下:
-
請求來了放入桶中 -
桶內請求量滿了拒絕請求 -
服務定速從桶內拿請求處理
能夠看到水滴對應的就是請求。它的特色就是寬進嚴出,不管請求多少,請求的速率有多大,都按照固定的速率流出,對應的就是服務按照固定的速率處理請求。「他強任他強,老子尼克楊」。
看到這想到啥,是否是和消息隊列思想有點像,削峯填谷。通常而言漏桶也是由隊列來實現的,處理不過來的請求就排隊,隊列滿了就開始拒絕請求。看到這又想到啥,線程池不就是這樣實現的嘛?
通過漏洞這麼一過濾,請求就能平滑的流出,看起來很像很挺完美的?實際上它的優勢也即缺點。
面對突發請求,服務的處理速度和平時是同樣的,這其實不是咱們想要的,面對突發流量咱們但願在系統平穩的同時,提高用戶體驗即能更快的處理請求,而不是和正常流量同樣,循規蹈矩的處理(看看,以前滑動窗口說流量不夠平滑,如今太平滑了又不行,難搞啊)。
而令牌桶在應對突擊流量的時候,能夠更加的「激進」。
令牌桶算法
令牌桶其實和漏桶的原理相似,只不過漏桶是定速地流出,而令牌桶是定速地往桶裏塞入令牌,而後請求只有拿到了令牌才能經過,以後再被服務器處理。
固然令牌桶的大小也是有限制的,假設桶裏的令牌滿了以後,定速生成的令牌會丟棄。
規則:
-
定速的往桶內放入令牌 -
令牌數量超過桶的限制,丟棄 -
請求來了先向桶內索要令牌,索要成功則經過被處理,反之拒絕
看到這又想到啥?Semaphore 信號量啊,信號量可控制某個資源被同時訪問的個數,其實和我們拿令牌思想同樣,一個是拿信號量,一個是拿令牌。只不過信號量用完了返還,而我們令牌用了不歸還,由於令牌會定時再填充。
再來看看令牌桶的僞代碼實現,能夠看出和漏桶的區別就在於一個是加法,一個是減法。
能夠看出令牌桶在應對突發流量的時候,桶內假若有 100 個令牌,那麼這 100 個令牌能夠立刻被取走,而不像漏桶那樣勻速的消費。因此在應對突發流量的時候令牌桶表現的更佳。
限流算法小結
上面所述的算法其實只是這些算法最粗略的實現和最本質的思想,在工程上其實仍是有不少變型的。
從上面看來好像漏桶和令牌桶比時間窗口算法好多了,那時間窗口算法有啥子用,扔了扔了?
並非的,雖然漏桶和令牌桶對比時間窗口對流量的整形效果更佳,流量更加得平滑,可是也有各自的缺點(上面已經提到了一部分)。
拿令牌桶來講,假設你沒預熱,那是否是上線時候桶裏沒令牌?沒令牌請求過來不就直接拒了麼?這就誤殺了,明明系統沒啥負載如今。
再好比說請求的訪問實際上是隨機的,假設令牌桶每 20ms 放入一個令牌,桶內初始沒令牌,這請求就恰好在第一個 20ms 內有兩個請求,再過 20ms 裏面沒請求,其實從 40ms 來看只有 2 個請求,應該都放行的,而有一個請求就直接被拒了。這就有可能形成不少請求的誤殺,可是若是看監控曲線的話,好像流量很平滑,峯值也控制的很好。
再拿漏桶來講,漏桶中請求是暫時存在桶內的。這其實不符合互聯網業務低延遲的要求。
因此漏桶和令牌桶其實比較適合阻塞式限流場景,即沒令牌我就等着,這就不會誤殺了,而漏桶本就是等着。比較適合後臺任務類的限流。而基於時間窗口的限流比較適合對時間敏感的場景,請求過不了您就快點兒告訴我,等的花兒都謝了(給阿姨倒一杯卡布奇諾。爲何我會忽然打這句話??)。
單機限流和分佈式限流
本質上單機限流和分佈式限流的區別其實就在於 「閾值」 存放的位置。
單機限流就上面所說的算法直接在單臺服務器上實現就行了,而每每咱們的服務是集羣部署的。所以須要多臺機器協同提供限流功能。
像上述的計數器或者時間窗口的算法,能夠將計數器存放至 Tair 或 Redis 等分佈式 K-V 存儲中。
例如滑動窗口的每一個請求的時間記錄能夠利用 Redis 的 zset
存儲,利用ZREMRANGEBYSCORE
刪除時間窗口以外的數據,再用 ZCARD
計數。
像令牌桶也能夠將令牌數量放到 Redis 中。
不過這樣的方式等於每個請求咱們都須要去Redis
判斷一下能不能經過,在性能上有必定的損耗,因此有個優化點就是 「批量」。例如每次取令牌不是一個一取,而是取一批,不夠了再去取一批。這樣能夠減小對 Redis 的請求。
不過要注意一點,批量獲取會致使必定範圍內的限流偏差。好比你取了 10 個此時不用,等下一秒再用,那同一時刻集羣機器總處理量可能會超過閾值。
其實「批量」這個優化點太常見了,不管是 MySQL 的批量刷盤,仍是 Kafka 消息的批量發送仍是分佈式 ID 的高性能發號,都包含了「批量」的思想。
固然分佈式限流還有一種思想是平分,假設以前單機限流 500,如今集羣部署了 5 臺,那就讓每臺繼續限流 500 唄,即在總的入口作總的限流限制,而後每臺機子再本身實現限流。
限流的難點
能夠看到每一個限流都有個閾值,這個閾值如何定是個難點。
定大了服務器可能頂不住,定小了就「誤殺」了,沒有資源利用最大化,對用戶體驗很差。
我能想到的就是限流上線以後先預估個大概的閾值,而後不執行真正的限流操做,而是採起日誌記錄方式,對日誌進行分析查看限流的效果,而後調整閾值,推算出集羣總的處理能力,和每臺機子的處理能力(方便擴縮容)。
而後將線上的流量進行重放,測試真正的限流效果,最終閾值肯定,而後上線。
我以前還看過一篇耗子叔的文章,講述了在自動化伸縮的狀況下,咱們要動態地調整限流的閾值很難,因而基於TCP擁塞控制的思想,根據請求響應在一個時間段的響應時間P90或者P99值來肯定此時服務器的健康情況,來進行動態限流。在他的 Ease Gateway 產品中實現了這套算法,有興趣的同窗能夠自行搜索。
其實真實的業務場景很複雜,須要限流的條件和資源不少,每一個資源限流要求還不同。因此我上面就是嘴強王者
。
限流組件
通常而言咱們不須要本身實現限流算法來達到限流的目的,無論是接入層限流仍是細粒度的接口限流其實都有現成的輪子使用,其實現也是用了上述咱們所說的限流算法。
好比Google Guava
提供的限流工具類 RateLimiter
,是基於令牌桶實現的,而且擴展了算法,支持預熱功能。
阿里開源的限流框架Sentinel
中的勻速排隊限流策略,就採用了漏桶算法。
Nginx 中的限流模塊 limit_req_zone
,採用了漏桶算法,還有 OpenResty 中的 resty.limit.req
庫等等。
具體的使用仍是很簡單的,有興趣的同窗能夠自行搜索,對內部實現感興趣的同窗能夠下個源碼看看,學習下生產級別的限流是如何實現的。
最後
今天只是粗略講解了限流相關的內容,具體應用到工程上仍是有不少點須要考慮的,而且限流只是保證系統穩定性中的一個環節,還須要配合降級、熔斷等相關內容。之後有機會再講講。
往期推薦:
Kafka索引設計的亮點:https://juejin.im/post/5efdeae7f265da22d017e58d
Kafka日誌段讀寫分析:https://juejin.im/post/5ef6b94ae51d4534a1236cb0
我是yes,一個在互聯網摸爬滾打且莫得感情的工具人。
本文分享自微信公衆號 - yes的練級攻略(yes_java)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。