小編所在的部門做爲公司的基礎服務部門,支撐着上層業務的正常運行,當有業務舉辦活動、遭遇攻擊,或者是寫土了代碼,都會或多或少給咱們的服務帶來流量上的衝擊。咱們一般說緩存、降級,以及限流技術是高併發服務的三大利器,爲保證集團其它業務不受影響,限流每每是服務端接口的必要特性之一,用於對抗大規模惡意請求,保護有限的計算和存儲資源。html
關於接口限流有不少成熟的算法可供使用,包括:計數器、漏桶,以及令牌桶等,這些算法都爲實際項目中的限流器設計提供了理論支撐。java
計數器應該是最簡單、最容易想到的限流策略,畢竟限流的本質就是限制一個接口在某個維度上單位時間的響應次數。咱們能夠設置一個計數器對某一時間段內的請求進行計數,當請求量超過某個事先設定的閾值時則觸發飽和策略,拒絕用戶的請求。好比咱們事先設定某個接口單一 IP 維度在 1 分鐘內只能正常響應 100 次用戶請求,那麼若是範圍內某個 IP 請求超過該閾值,就會拒絕後續請求。git
上述方法存在的一個缺點是計數不夠平滑,考慮一個 10 點開放搶購的場景,若是一個惡意用戶若是在 09:59:30 ~ 09:59:59
之間請求了 100 次,而後等到 10 點整時計數器被清空,這個時候該用戶在 10:00:00 ~ 10:00:30
之間又能夠再次請求 100 次,實際上在 09:59:30 ~ 10:00:30
這一分鐘內該惡意用戶請求了 200 次,成功繞過了限流策略。github
針對計數器算法存在的上述缺陷,一種典型的解決方法就是採用 __滑動窗口策略__,本質上就是多級計數。上面咱們介紹的 1 分鐘 100 次的請求上限能夠看作是一個大窗口,咱們還能夠將該窗口進一步細分,好比每 10 秒一個小窗口,該窗口的頻率上限一樣設置爲 100,大窗口中包含了 6 個小窗口,而且沒過 10 秒大窗口就往前移動一個小窗口長度。這樣的設計下,若是一個惡意用戶在 09:59:30 ~ 09:59:59
之間請求了 100 次,等到 10:00:00 ~ 10:00:30
時這 100 次計數仍然是有效的,因此這個時間段該用戶新的請求仍然會被拒絕。算法
漏桶(Leaky Bucket)算法是限流方面比較經典的算法,該算法最先應用於網絡擁塞控制方面。理解該算法能夠聯想一個具體的漏桶模型,無論進水量有多大,漏桶始終以恆定的速率往外排水,若是桶被裝滿則後來涌入的水會漫出去。對應接口限流來講,用戶的請求能夠看作是這裏的水,無論用戶的請求量有多大多不均衡,可以被處理的請求速率是恆定的,並且可以被接受的請求數也是有上限的,超出上限的請求會被拒絕,典型的咱們能夠採用隊列做爲這裏的漏桶實現。緩存
由上面的解釋咱們應該可以感受到漏桶算法很是適用於秒殺系統的限流,漏桶在這種應用場景下能夠起到必定的削峯填谷的做用,而且漏桶的設計從根本上可以應對集中訪問的問題,同時具有平滑策略,可是始終恆定的處理速率有時候並不必定是好事情,對於突發的請求洪峯,在保證服務安全的前提下,應該盡最大努力去響應,這個時候漏桶算法顯得有些呆滯。安全
令牌桶(Token Bucket)算法能夠看做是計數器算法的逆過程,不過相對於計數器來講更加平滑。該算法要求系統以必定的速率發放訪問令牌,用戶的請求必須在持有合法令牌的前提下才可以被響應,咱們能夠按照權重設置一類請求被響應所需持有的令牌數,只有當桶中的令牌數目知足當前請求所需時才授予令牌,對於其餘狀況則拒絕該請求。微信
因爲發放令牌的速率是恆定的,因此對於集中請求來講,令牌桶算法可以很好的作平滑,好比前面列舉的在 09:59:30 ~ 09:59:59
之間有 100 次的突發請求,那麼等到 10 點整的時候系統並不會當即容忍 100 次新的請求,這個時候服務的響應受限於當前桶中的令牌數量。實際項目中咱們能夠基於當前服務能力動態調整令牌的發放速率,此外咱們還須要爲桶設置大小上限(或者爲令牌設置生命週期),以防止大量令牌累積致使的 「僞限流失效」 現象。網絡
Guava 中的 RateLimiter 類對該算法進行了實現,基於 RateLimiter 咱們能夠設置每秒生成的令牌數,並容許以阻塞、嘗試,以及超時等策略獲取令牌。相關實現和使用方式比較簡單,能夠參考官方文檔。併發
此前在寫項目時發現組內的限流模塊散落在各個具體項目中,而且各個項目都根據本身的業務特色對限流實現作了必定的定製化,這樣在必定程度上增長了代碼的實現和維護成本,因而就抽空閒時間對這一塊進行了改造,抽取出了一個公共的 throttle 基礎模塊。考慮到時間成本和兼容性,這一塊的實現仍是採用了平滑版本的計數器,而且在本地和全局維度上進行計數,以應對服務的分佈式部署。
如上圖所示,限流模塊以攔截器的方式進行工做,針對用戶的請求會在本地和 IDC 範圍內進行計數,當任何一個計數器達到閾值即觸發飽和策略。採用計數器的好處是在分佈式限流方面實現上比較簡單,且除全局緩存之外不依賴於第三方服務,不過遠程通訊這一塊的性能損耗不能忽略,關於遠程通訊這一塊仍是有一些優化手段的,好比在本地計數並定時與遠端執行同步,沒有必要每一次用戶請求都進行請求一次遠程計數。另外,考慮到各個業務的限流維度不盡相同,在限流器設計上能夠引入 SPI 機制來提高可擴展性和可配置性。
總的說來限流器在實現上並無太多複雜的邏輯,不過正如之前聽一位長者所說,__限流的難點在於配置__,如何讓限流在不誤傷的前提下儘可能發揮硬件的最大性能是一個富有經驗的問題,而壓測是一個基礎且行之有效的途徑。