關於分散式服務限流的一些思考

来源:https://www.cnblogs.com/Javajishuzhai/archive/2019/08/15/11358138.html
-Advertisement-
Play Games

一個軟體系統往往會存在很多隱藏的bug,最常用的功能bug往往很少。不常用的功能因為長時間不被人關註缺少重現的機會會一直隱藏在那裡伺機爆發。限流功能就是這些不被關註的功能之一。 ...


限流必然是很有價值的,在系統資源不足時面對外部世界的不確定性(突發流量,超預期的用戶)而形成的一種自我保護機制。 但是價值感是很低的,因為99.99%的時候系統總是工作在安全線之下,甚至一年到頭都碰不到一次撞線的機會。這就好比法律,它始終存在,但是大部分時候對於大多數人它幾乎不存在,或者說感知不到它的存在。

一、限流的作用

由於API介面無法控制調用方的行為,因此當遇到瞬時請求量激增時,會導致介面占用過多伺服器資源,使得其他請求響應速度降低或是超時,更有甚者可能導致伺服器宕機。 

限流(Rate limiting)指對應用服務的請求進行限制,例如某一介面的請求限製為100個每秒,對超過限制的請求則進行快速失敗或丟棄。

限流可以應對:

  • 熱點業務帶來的突發請求;

  • 調用方bug導致的突發請求;

  • 惡意攻擊請求。

因此,對於公開的介面最好採取限流措施。

二、為什麼要分散式限流

 

當應用為單點應用時,只要應用進行了限流,那麼應用所依賴的各種服務也都得到了保護。

 

 

但線上業務出於各種原因考慮,多是分散式系統,單節點的限流僅能保護自身節點,但無法保護應用依賴的各種服務,並且在進行節點擴容、縮容時也無法準確控制整個服務的請求限制。

 

 

而如果實現了分散式限流,那麼就可以方便地控制整個服務集群的請求限制,且由於整個集群的請求數量得到了限制,因此服務依賴的各種資源也得到了限流的保護。

 

三、限流的演算法

實現限流有很多辦法,在程式中時通常是根據每秒處理的事務數(Transaction per second)來衡量介面的流量。 

本文介紹幾種最常用的限流演算法:

  • 固定視窗計數器;

  • 滑動視窗計數器;

  • 漏桶;

  • 令牌桶。

1、固定視窗計數器演算法

 

固定視窗計數器演算法概念如下:

  • 將時間劃分為多個視窗;

  • 在每個視窗內每有一次請求就將計數器加一;

  • 如果計數器超過了限制數量,則本視窗內所有的請求都被丟棄當時間到達下一個視窗時,計數器重置。

固定視窗計數器是最為簡單的演算法,但這個演算法有時會讓通過請求量允許為限制的兩倍。考慮如下情況:限制1秒內最多通過5個請求,在第一個視窗的最後半秒內通過了5個請求,第二個視窗的前半秒內又通過了5個請求。這樣看來就是在1秒內通過了10個請求。 

 

 

2、滑動視窗計數器演算法

 

 

滑動視窗計數器演算法概念如下:

  • 將時間劃分為多個區間;

  • 在每個區間內每有一次請求就將計數器加一維持一個時間視窗,占據多個區間;

  • 每經過一個區間的時間,則拋棄最老的一個區間,並納入最新的一個區間;

  • 如果當前視窗內區間的請求計數總和超過了限制數量,則本視窗內所有的請求都被丟棄。

滑動視窗計數器是通過將視窗再細分,並且按照時間"滑動",這種演算法避免了固定視窗計數器帶來的雙倍突發請求,但時間區間的精度越高,演算法所需的空間容量就越大。

3、漏桶演算法

 

 

漏桶演算法概念如下:

  • 將每個請求視作"水滴"放入"漏桶"進行存儲;

  • "漏桶"以固定速率向外"漏"出請求來執行如果"漏桶"空了則停止"漏水";

  • 如果"漏桶"滿了則多餘的"水滴"會被直接丟棄。

漏桶演算法多使用隊列實現,服務的請求會存到隊列中,服務的提供方則按照固定的速率從隊列中取出請求並執行,過多的請求則放在隊列中排隊或直接拒絕。

漏桶演算法的缺陷也很明顯,當短時間內有大量的突發請求時,即便此時伺服器沒有任何負載,每個請求也都得在隊列中等待一段時間才能被響應。

4、令牌桶演算法

 

令牌桶演算法概念如下:

  • 令牌以固定速率生成;

  • 生成的令牌放入令牌桶中存放,如果令牌桶滿了則多餘的令牌會直接丟棄,當請求到達時,會嘗試從令牌桶中取令牌,取到了令牌的請求可以執行;

  • 如果桶空了,那麼嘗試取令牌的請求會被直接丟棄。

令牌桶演算法既能夠將所有的請求平均分佈到時間區間內,又能接受伺服器能夠承受範圍內的突發請求,因此是目前使用較為廣泛的一種限流演算法。

四、代碼實現

作為如此重要的功能,在Java中自然有很多實現限流的類庫,例如Google的開源項目guava提供了RateLimiter類,實現了單點的令牌桶限流。 

而分散式限流常用的則有Hystrix、resilience4j、Sentinel等框架,但這些框架都需引入第三方的類庫,對於國企等一些保守的企業,引入外部類庫都需要經過層層審批,較為麻煩。 

分散式限流本質上是一個集群併發問題,而Redis作為一個應用廣泛的中間件,又擁有單進程單線程的特性,天然可以解決分散式集群的併發問題。本文簡單介紹一個通過Redis實現單次請求判斷限流的功能。

1、腳本編寫

經過上面的對比,最適合的限流演算法就是令牌桶演算法。而為實現限流演算法,需要反覆調用Redis查詢與計算,一次限流判斷需要多次請求較為耗時。因此我們採用編寫Lua腳本運行的方式,將運算過程放在Redis端,使得對Redis進行一次請求就能完成限流的判斷。 

令牌桶演算法需要在Redis中存儲桶的大小、當前令牌數量,並且實現每隔一段時間添加新的令牌。最簡單的辦法當然是每隔一段時間請求一次Redis,將存儲的令牌數量遞增。 

但實際上我們可以通過對限流兩次請求之間的時間和令牌添加速度來計算得出上次請求之後到本次請求時,令牌桶應添加的令牌數量。因此我們在Redis中只需要存儲上次請求的時間和令牌桶中的令牌數量,而桶的大小和令牌的添加速度可以通過參數傳入實現動態修改。 

由於第一次運行腳本時預設令牌桶是滿的,因此可以將數據的過期時間設置為令牌桶恢復到滿所需的時間,及時釋放資源。 

編寫完成的Lua腳本如下:

local ratelimit_info = redis.pcall('HMGET',KEYS[1],'last_time','current_token')

local last_time = ratelimit_info[1]

local current_token = tonumber(ratelimit_info[2])

local max_token = tonumber(ARGV[1])

local token_rate = tonumber(ARGV[2])

local current_time = tonumber(ARGV[3])

local reverse_time = 1000/token_rate

if current_token == nil then

  current_token = max_token

  last_time = current_time

else

  local past_time = current_time-last_time

  local reverse_token = math.floor(past_time/reverse_time)

  current_token = current_token+reverse_token

  last_time = reverse_time*reverse_token+last_time

  if current_token>max_token then

    current_token = max_token

  end

end

local result = 0

if(current_token>0) then

  result = 1

  current_token = current_token-1

end 

redis.call('HMSET',KEYS[1],'last_time',last_time,'current_token',current_token)

redis.call('pexpire',KEYS[1],math.ceil(reverse_time*(max_token-current_token)+(current_time-last_time)))

return result

2、執行限流

這裡使用Spring Data Redis來進行Redis腳本的調用。

編寫Redis腳本類:

public class RedisReteLimitScript implements RedisScript<String> { 

   private static final String SCRIPT = 

      "local ratelimit_info = redis.pcall('HMGET',KEYS[1],'last_time','current_token') local last_time = ratelimit_info[1] local current_token = tonumber(ratelimit_info[2]) local max_token = tonumber(ARGV[1]) local token_rate = tonumber(ARGV[2]) local current_time = tonumber(ARGV[3]) local reverse_time = 1000/token_rate if current_token == nil then current_token = max_token last_time = current_time else local past_time = current_time-last_time; local reverse_token = math.floor(past_time/reverse_time) current_token = current_token+reverse_token; last_time = reverse_time*reverse_token+last_time if current_token>max_token then current_token = max_token end end local result = '0' if(current_token>0) then result = '1' current_token = current_token-1 end redis.call('HMSET',KEYS[1],'last_time',last_time,'current_token',current_toke  redis.call('pexpire',KEYS[1],math.ceil(reverse_time*(max_tokencurrent_token)+(current_time-last_time))) return result"; 



  @Override   public String getSha1() { 

    return DigestUtils.sha1Hex(SCRIPT); 

  } 



  @Override   public Class<String> getResultType() {     return String.class; 

  } 



  @Override   public String getScriptAsString() {     return SCRIPT; 

  } 

} 

通過RedisTemplate對象執行腳本:

public boolean rateLimit(String key, int max, int rate) {

    List<String> keyList = new ArrayList<>(1);

    keyList.add(key);

    return "1".equals(stringRedisTemplate

        .execute(new RedisReteLimitScript(), keyList, Integer.toString(max), Integer.toString(rate),

            Long.toString(System.currentTimeMillis())));

  } 

rateLimit方法傳入的key為限流介面的ID,max為令牌桶的最大大小,rate為每秒鐘恢復的令牌數量,返回的boolean即為此次請求是否通過了限流。為了測試Redis腳本限流是否可以正常工作,我們編寫一個單元測試進行測試看看。

@Autowired

  private RedisManager redisManager;



  @Test

  public void rateLimitTest() throws InterruptedException {

    String key = "test_rateLimit_key";

    int max = 10;  //令牌桶大小

    int rate = 10; //令牌每秒恢復速度

    AtomicInteger successCount = new AtomicInteger(0);

    Executor executor = Executors.newFixedThreadPool(10);

    CountDownLatch countDownLatch = new CountDownLatch(30);

    for (int i = 0; i < 30; i++) {

      executor.execute(() -> {

        boolean isAllow = redisManager.rateLimit(key, max, rate);

        if (isAllow) {

          successCount.addAndGet(1);

        }

        log.info(Boolean.toString(isAllow));

        countDownLatch.countDown();

      });

    }

    countDownLatch.await();

    log.info("請求成功{}次", successCount.get());

  }

設置令牌桶大小為10,令牌桶每秒恢復10個,啟動10個線程在短時間內進行30次請求,並輸出每次限流查詢的結果。日誌輸出:

[19:12:50,283]true 

[19:12:50,284]true 

[19:12:50,284]true 

[19:12:50,291]true 

[19:12:50,291]true 

[19:12:50,291]true 

[19:12:50,297]true 

[19:12:50,297]true 

[19:12:50,298]true 

[19:12:50,305]true 

[19:12:50,305]false 

[19:12:50,305]true 

[19:12:50,312]false 

[19:12:50,312]false 

[19:12:50,312]false 

[19:12:50,319]false 

[19:12:50,319]false 

[19:12:50,319]false 

[19:12:50,325]false 

[19:12:50,325]false 

[19:12:50,326]false 

[19:12:50,380]false 

[19:12:50,380]false 

[19:12:50,380]false 

[19:12:50,387]false 

[19:12:50,387]false 

[19:12:50,387]false 

[19:12:50,392]false 

[19:12:50,392]false 

[19:12:50,392]false 

[19:12:50,393]請求成功11次

可以看到,在0.1秒內請求的30次請求中,除了初始的10個令牌以及隨時間恢復的1個令牌外,剩下19個沒有取得令牌的請求均返回了false,限流腳本正確的將超過限制的請求給判斷出來了,業務中此時就可以直接返回系統繁忙或介面請求太過頻繁等提示。

3、開發中遇到的問題

1)Lua變數格式

Lua中的String和Number需要通過tonumber()和tostring()進行轉換。

2)Redis入參

Redis的pexpire等命令不支持小數,但Lua的Number類型可以存放小數,因此Number類型傳遞給 Redis時最好通過math.ceil()等方式轉換以避免存在小數導致命令失敗。

3)Time命令

由於Redis在集群下是通過複製腳本及參數到所有節點上,因此無法在具有不確定性的命令後面執行寫入命令,因此只能請求時傳入時間而無法使用Redis的Time命令獲取時間。

3.2版本之後的Redis腳本支持redis.replicate_commands(),可以改為使用Time命令獲取當前時間。

4)潛在的隱患

由於此Lua腳本是通過請求時傳入的時間做計算,因此務必保證分散式節點上獲取的時間同步,如果時間不同步會導致限流無法正常運作。


 

最後

一個軟體系統往往會存在很多隱藏的bug,最常用的功能bug往往很少。不常用的功能因為長時間不被人關註缺少重現的機會會一直隱藏在那裡伺機爆發。而且隨著軟體系統的迭代更新,不受關註的功能極有可能被測試人員忽視從覆蓋測試中被遺忘了。結果一旦遇到了突發的場景(墨菲定律),這一段被忽視的存在bug的代碼邏輯被喚醒的,然後就會導致系統出錯甚至奔潰。限流功能就是這些不被關註的功能之一。那麼通過本篇文章,你對分散式服務限流有一個大概的瞭解了嗎?

本文僅代表作者的一家之言,關於本文有什麼疑問可以在下麵留言交流,如果您覺得本文對您有幫助,歡迎關註我的公眾號【Java技術zhai】,有新文章發佈會第一時間通知您。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 前言 今天沒有什麼前言,就是想分享些關於 的技術,任性。來吧,各位客官,裡邊請... 開篇第一問: 是什麼嘞? 首先咱們說哈,爬蟲不是“蟲子”,姑涼們不要害怕。 一種通過一定方式按照一定規則抓取數據的操作或方法。 開篇第二問: 能做什麼嘞? 來來來,談談需求 產品MM: 1. 愛豆的新電影上架了,整 ...
  • 微服務與K8S容器雲平臺架構微服務與12要素網路日誌收集服務網關服務註冊服務治理- java agent監控今天先到這兒,希望對技術領導力, 企業管理,系統架構設計與評估,團隊管理, 項目管理, 產品管理,團隊建設 有參考作用 , 您可能感興趣的文章: 領導人怎樣帶領好團隊構建創業公司突擊小團隊國際... ...
  • 優雅的在WinForm/WPF/控制台 中使用特性封裝WebApi 說明 ` 在C/S端作為Server,建立HTTP請求,方便快捷。 ` 1.使用到的類庫 ` Newtonsoft.dll ` 2.封裝 HttpListener HttpApi類 特性類 ActionName 特性類 HttpMe ...
  • 簡單工廠模式 簡單工廠模式不是 23 種里的一種,簡而言之,就是有一個專門生產某個產品的類。 比如下圖中的滑鼠工廠,專業生產滑鼠,給參數 0,生產戴爾滑鼠,給參數 1,生產惠普滑鼠。 工廠模式 工廠模式也就是滑鼠工廠是個父類,有生產滑鼠這個介面。 戴爾滑鼠工廠,惠普滑鼠工廠繼承它,可以分別生產戴爾鼠 ...
  • 一、小案例分析 1、功能需求: 現需要建房子,建房流程:挖地基、砌牆、封頂。對於不同種類的房子(高樓,別墅),流程雖然一樣,但是具體功能實現不同。如何實現建房子? 2、小菜雞的答案: (1)定義一個抽象介面,並定義三個抽象方法(挖地基、砌牆、封頂)。(2)對於不同種類的房子,實現該介面,並重寫相關方 ...
  • 架構雜談《十》 常用開發模式 一、瀑布式開發 瀑布式開發是在1970年提出的軟體開發模型,是一種較老的電腦軟體開發模式,也是典型的預見性的開發模式,在瀑布式開發中,開發嚴格遵循預先計劃的需求分析、設計、編碼、集成、測試、維護的步驟進行,步驟的成果作為衡量進度的方法。瀑布式開發最早強調系統開發應有完 ...
  • 前言 今年年初來了一家國內某電器大廠,本來技術面試的時候提供的offer說的是架構組崗位,主要是搭建公司平臺的基礎設施,不會接觸業務或者離業務很遠,剛開始以為很有技術含量,公司又是大廠,offer就接下來了,但是進來後才知道是業務導向型團隊,因為當時面試我的技術經理離職,原先架構組領導崗位就由原項目 ...
  • 前言 在正式開始正文之前,我想你思考幾個問題: 1.什麼是面向對象? 2.面向對象的特性是什麼? 3.面向對象有哪些繼承方式? 好了,看完這三個問題,開始正文的內容吧。 正文 一、面向對象js 面向對象是一個思想,就是把解決問題的註意力集中到對象上,也可以說是通過函數封裝得到的一個類。 面向對象有三 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...