成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

分布式服務限流實戰(zhàn),已經(jīng)為你排好坑了

開發(fā) 架構 分布式
由于API接口無法控制調用方的行為,因此當遇到瞬時請求量激增時,會導致接口占用過多服務器資源,使得其他請求響應速度降低或是超時,更有甚者可能導致服務器宕機。

[[273022]]

一、限流的作用

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

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

限流可以應對:

  • 熱點業(yè)務帶來的突發(fā)請求;
  • 調用方bug導致的突發(fā)請求;
  • 惡意攻擊請求。

因此,對于公開的接口應該采取限流措施。

二、為什么要分布式限流

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

但線上業(yè)務出于各種原因考慮,多是分布式系統(tǒng),單節(jié)點的限流僅能保護自身節(jié)點,但無法保護應用依賴的各種服務,并且在進行節(jié)點擴容、縮容時也無法準確控制整個服務的請求限制。

而如果實現(xiàn)了分布式限流,那么就可以方便地控制整個服務集群的請求限制,且由于整個集群的請求數(shù)量得到了限制,因此服務依賴的各種資源也得到了限流的保護。

三、限流的算法

實現(xiàn)限流有很多辦法,在程序中時通常是根據(jù)每秒處理的事務數(shù)(Transaction per second)來衡量接口的流量。

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

  • 固定窗口計數(shù)器;
  • 滑動窗口計數(shù)器;
  • 漏桶;
  • 令牌桶。

1、固定窗口計數(shù)器算法

固定窗口計數(shù)器算法概念如下:

  • 將時間劃分為多個窗口;
  • 在每個窗口內每有一次請求就將計數(shù)器加一;
  • 如果計數(shù)器超過了限制數(shù)量,則本窗口內所有的請求都被丟棄當時間到達下一個窗口時,計數(shù)器重置。

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

2、滑動窗口計數(shù)器算法

滑動窗口計數(shù)器算法概念如下:

  • 將時間劃分為多個區(qū)間;
  • 在每個區(qū)間內每有一次請求就將計數(shù)器加一維持一個時間窗口,占據(jù)多個區(qū)間;
  • 每經(jīng)過一個區(qū)間的時間,則拋棄最老的一個區(qū)間,并納入最新的一個區(qū)間;
  • 如果當前窗口內區(qū)間的請求計數(shù)總和超過了限制數(shù)量,則本窗口內所有的請求都被丟棄。

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

3、漏桶算法

漏桶算法概念如下:

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

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

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

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

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

4、令牌桶算法

令牌桶算法概念如下:

  • 令牌以固定速率生成;
  • 生成的令牌放入令牌桶中存放,如果令牌桶滿了則多余的令牌會直接丟棄,當請求到達時,會嘗試從令牌桶中取令牌,取到了令牌的請求可以執(zhí)行;
  • 如果桶空了,那么嘗試取令牌的請求會被直接丟棄。

令牌桶算法既能夠將所有的請求平均分布到時間區(qū)間內,又能接受服務器能夠承受范圍內的突發(fā)請求,因此是目前使用較為廣泛的一種限流算法。

四、代碼實現(xiàn)

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

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

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

1、腳本編寫

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

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

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

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

編寫完成的Lua腳本如下:

  1. local ratelimit_info = redis.pcall('HMGET',KEYS[1],'last_time','current_token'
  2. local last_time = ratelimit_info[1] 
  3. local current_token = tonumber(ratelimit_info[2]) 
  4. local max_token = tonumber(ARGV[1]) 
  5. local token_rate = tonumber(ARGV[2]) 
  6. local current_time = tonumber(ARGV[3]) 
  7. local reverse_time = 1000/token_rate 
  8. if current_token == nil then 
  9.   current_token = max_token 
  10.   last_time = current_time 
  11. else 
  12.   local past_time = current_time-last_time 
  13.   local reverse_token = math.floor(past_time/reverse_time) 
  14.   current_token = current_token+reverse_token 
  15.   last_time = reverse_time*reverse_token+last_time 
  16.   if current_token>max_token then 
  17.     current_token = max_token 
  18.   end 
  19. end 
  20. local result = 0 
  21. if(current_token>0) then 
  22.   result = 1 
  23.   current_token = current_token-1 
  24. end  
  25. redis.call('HMSET',KEYS[1],'last_time',last_time,'current_token',current_token) 
  26. redis.call('pexpire',KEYS[1],math.ceil(reverse_time*(max_token-current_token)+(current_time-last_time))) 
  27. return result 

2、執(zhí)行限流

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

編寫Redis腳本類:

  1. public class RedisReteLimitScript implements RedisScript<String> {  
  2.    private static final String SCRIPT =  
  3.       "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";  
  4.  
  5.   @Override   public String getSha1() {  
  6.     return DigestUtils.sha1Hex(SCRIPT);  
  7.   }  
  8.  
  9.   @Override   public Class<String> getResultType() {     return String.class;  
  10.   }  
  11.  
  12.   @Override   public String getScriptAsString() {     return SCRIPT;  
  13.   }  
  14. }  

通過RedisTemplate對象執(zhí)行腳本:

  1. public boolean rateLimit(String keyint maxint rate) { 
  2.     List<String> keyList = new ArrayList<>(1); 
  3.     keyList.add(key); 
  4.     return "1".equals(stringRedisTemplate 
  5.         .execute(new RedisReteLimitScript(), keyList, Integer.toString(max), Integer.toString(rate), 
  6.             Long.toString(System.currentTimeMillis()))); 
  7.   }  

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

  1. @Autowired 
  2.   private RedisManager redisManager; 
  3.  
  4.   @Test 
  5.   public void rateLimitTest() throws InterruptedException { 
  6.     String key = "test_rateLimit_key"
  7.     int max = 10;  //令牌桶大小 
  8.     int rate = 10; //令牌每秒恢復速度 
  9.     AtomicInteger successCount = new AtomicInteger(0); 
  10.     Executor executor = Executors.newFixedThreadPool(10); 
  11.     CountDownLatch countDownLatch = new CountDownLatch(30); 
  12.     for (int i = 0; i < 30; i++) { 
  13.       executor.execute(() -> { 
  14.         boolean isAllow = redisManager.rateLimit(keymax, rate); 
  15.         if (isAllow) { 
  16.           successCount.addAndGet(1); 
  17.         } 
  18.         log.info(Boolean.toString(isAllow)); 
  19.         countDownLatch.countDown(); 
  20.       }); 
  21.     } 
  22.     countDownLatch.await(); 
  23.     log.info("請求成功{}次", successCount.get()); 
  24.   } 

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

  1. [19:12:50,283]true  
  2. [19:12:50,284]true  
  3. [19:12:50,284]true  
  4. [19:12:50,291]true  
  5. [19:12:50,291]true  
  6. [19:12:50,291]true  
  7. [19:12:50,297]true  
  8. [19:12:50,297]true  
  9. [19:12:50,298]true  
  10. [19:12:50,305]true  
  11. [19:12:50,305]false  
  12. [19:12:50,305]true  
  13. [19:12:50,312]false  
  14. [19:12:50,312]false  
  15. [19:12:50,312]false  
  16. [19:12:50,319]false  
  17. [19:12:50,319]false  
  18. [19:12:50,319]false  
  19. [19:12:50,325]false  
  20. [19:12:50,325]false  
  21. [19:12:50,326]false  
  22. [19:12:50,380]false  
  23. [19:12:50,380]false  
  24. [19:12:50,380]false  
  25. [19:12:50,387]false  
  26. [19:12:50,387]false  
  27. [19:12:50,387]false  
  28. [19:12:50,392]false  
  29. [19:12:50,392]false  
  30. [19:12:50,392]false  
  31. [19:12:50,393]請求成功11次 

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

3、開發(fā)中遇到的問題

1)Lua變量格式

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

2)Redis入?yún)?/strong>

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

3)Time命令

由于Redis在集群下是通過復制腳本及參數(shù)到所有節(jié)點上,因此無法在具有不確定性的命令后面執(zhí)行寫入命令,因此只能請求時傳入時間而無法使用Redis的Time命令獲取時間。

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

4)潛在的隱患

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

作者介紹

段然,甜橙金融創(chuàng)新中心開發(fā)工程師,目前負責公司平臺化建設及媒介能力聚合。

責任編輯:武曉燕 來源: DBAplus社群
相關推薦

2019-08-27 08:30:19

分布式服務限流

2019-05-28 08:56:40

PythonCPUThread

2023-05-29 14:07:00

Zuul網(wǎng)關系統(tǒng)

2018-06-28 08:18:56

Ceph運維存儲

2024-04-08 11:04:03

2022-01-12 12:46:32

Go限流保障

2022-06-18 23:03:05

Seata分布式事務

2010-01-21 11:51:11

2018-06-11 11:12:09

秒殺限流分布式

2018-06-19 09:35:51

分布式系統(tǒng)限流

2023-07-11 10:24:00

分布式限流算法

2019-08-05 07:58:01

分布式架構系統(tǒng)

2023-01-13 07:39:07

2012-06-06 09:30:07

2012-06-06 11:29:15

2013-07-23 10:24:00

2023-04-06 08:52:54

Sentinel分布式系統(tǒng)

2022-12-21 08:40:05

限流器分布式限流

2022-01-26 13:46:40

分布式事務集合,這

2016-11-02 12:06:27

分布式系統(tǒng)大數(shù)據(jù)
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 情侣酒店偷拍一区二区在线播放 | 亚洲精品成人网 | 久久综合久 | 99热在线免费| 蜜臀久久| 精品无码久久久久国产 | 天天干天天爱天天 | 国产精品久久久久影院色老大 | 99精品在线免费观看 | 欧美爱爱视频 | 亚洲高清视频在线观看 | 日韩一区在线播放 | 久久在线免费 | 午夜精品久久久久99蜜 | 精品国产一级 | 91视频在线 | 日韩激情网 | 国产精品久久久久久久久久软件 | 噜啊噜在线 | 精品精品 | 久久综合av | 日本视频一区二区 | av成年人网站 | 69视频在线播放 | 精品视频一区二区在线观看 | 一级一级毛片免费看 | 欧产日产国产精品国产 | 国产精品精品久久久 | 蜜臀久久99精品久久久久野外 | 天天射影院 | 日韩精品视频在线 | 91在线观看视频 | 日韩av免费在线观看 | 午夜视频在线观看网址 | 中文字幕亚洲一区二区三区 | 国产成人精品一区二区三区四区 | 一区二区三区四区不卡视频 | 欧美日韩在线视频一区二区 | 日韩欧美在线视频 | 91不卡| 精品美女|