一区二区三区日韩精品-日韩经典一区二区三区-五月激情综合丁香婷婷-欧美精品中文字幕专区

分享

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

 vnxy001 2021-10-14

開(kāi)頭:本文由 dbaplus 社群授權(quán)轉(zhuǎn)載。

一、限流的作用

由于 API 接口無(wú)法控制調(diào)用方的行為,因此當(dāng)遇到瞬時(shí)請(qǐng)求量激增時(shí),會(huì)導(dǎo)致接口占用過(guò)多服務(wù)器資源,使得其他請(qǐng)求響應(yīng)速度降低或是超時(shí),更有甚者可能導(dǎo)致服務(wù)器宕機(jī)。

限流(Ratelimiting)指對(duì)應(yīng)用服務(wù)的請(qǐng)求進(jìn)行限制,例如某一接口的請(qǐng)求限制為 100 個(gè)每秒,對(duì)超過(guò)限制的請(qǐng)求則進(jìn)行快速失敗或丟棄。

限流可以應(yīng)對(duì):

  • 熱點(diǎn)業(yè)務(wù)帶來(lái)的突發(fā)請(qǐng)求;

  • 調(diào)用方 bug 導(dǎo)致的突發(fā)請(qǐng)求;

  • 惡意攻擊請(qǐng)求。

因此,對(duì)于公開(kāi)的接口最好采取限流措施。

二、為什么要分布式限流

當(dāng)應(yīng)用為單點(diǎn)應(yīng)用時(shí),只要應(yīng)用進(jìn)行了限流,那么應(yīng)用所依賴的各種服務(wù)也都得到了保護(hù)。

但線上業(yè)務(wù)出于各種原因考慮,多是分布式系統(tǒng),單節(jié)點(diǎn)的限流僅能保護(hù)自身節(jié)點(diǎn),但無(wú)法保護(hù)應(yīng)用依賴的各種服務(wù),并且在進(jìn)行節(jié)點(diǎn)擴(kuò)容、縮容時(shí)也無(wú)法準(zhǔn)確控制整個(gè)服務(wù)的請(qǐng)求限制。

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

三、限流的算法

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

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

  • 固定窗口計(jì)數(shù)器;

  • 滑動(dòng)窗口計(jì)數(shù)器;

  • 漏桶;

  • 令牌桶。

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

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

  • 將時(shí)間劃分為多個(gè)窗口;

  • 在每個(gè)窗口內(nèi)每有一次請(qǐng)求就將計(jì)數(shù)器加一;

  • 如果計(jì)數(shù)器超過(guò)了限制數(shù)量,則本窗口內(nèi)所有的請(qǐng)求都被丟棄當(dāng)時(shí)間到達(dá)下一個(gè)窗口時(shí),計(jì)數(shù)器重置。

固定窗口計(jì)數(shù)器是最為簡(jiǎn)單的算法,但這個(gè)算法有時(shí)會(huì)讓通過(guò)請(qǐng)求量允許為限制的兩倍??紤]如下情況:限制 1 秒內(nèi)最多通過(guò) 5 個(gè)請(qǐng)求,在第一個(gè)窗口的最后半秒內(nèi)通過(guò)了 5 個(gè)請(qǐng)求,第二個(gè)窗口的前半秒內(nèi)又通過(guò)了 5 個(gè)請(qǐng)求。這樣看來(lái)就是在 1 秒內(nèi)通過(guò)了 10 個(gè)請(qǐng)求。

2、滑動(dòng)窗口計(jì)數(shù)器算法

滑動(dòng)窗口計(jì)數(shù)器算法概念如下:

  • 將時(shí)間劃分為多個(gè)區(qū)間;

  • 在每個(gè)區(qū)間內(nèi)每有一次請(qǐng)求就將計(jì)數(shù)器加一維持一個(gè)時(shí)間窗口,占據(jù)多個(gè)區(qū)間;

  • 每經(jīng)過(guò)一個(gè)區(qū)間的時(shí)間,則拋棄最老的一個(gè)區(qū)間,并納入最新的一個(gè)區(qū)間;

  • 如果當(dāng)前窗口內(nèi)區(qū)間的請(qǐng)求計(jì)數(shù)總和超過(guò)了限制數(shù)量,則本窗口內(nèi)所有的請(qǐng)求都被丟棄。

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

3、漏桶算法

漏桶算法概念如下:

  • 將每個(gè)請(qǐng)求視作"水滴"放入"漏桶"進(jìn)行存儲(chǔ);

  • “漏桶"以固定速率向外"漏"出請(qǐng)求來(lái)執(zhí)行如果"漏桶"空了則停止"漏水”;

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

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

漏桶算法的缺陷也很明顯,當(dāng)短時(shí)間內(nèi)有大量的突發(fā)請(qǐng)求時(shí),即便此時(shí)服務(wù)器沒(méi)有任何負(fù)載,每個(gè)請(qǐng)求也都得在隊(duì)列中等待一段時(shí)間才能被響應(yīng)。

4、令牌桶算法

令牌桶算法概念如下:

  • 令牌以固定速率生成;

  • 生成的令牌放入令牌桶中存放,如果令牌桶滿了則多余的令牌會(huì)直接丟棄,當(dāng)請(qǐng)求到達(dá)時(shí),會(huì)嘗試從令牌桶中取令牌,取到了令牌的請(qǐng)求可以執(zhí)行;

  • 如果桶空了,那么嘗試取令牌的請(qǐng)求會(huì)被直接丟棄。

令牌桶算法既能夠?qū)⑺械恼?qǐng)求平均分布到時(shí)間區(qū)間內(nèi),又能接受服務(wù)器能夠承受范圍內(nèi)的突發(fā)請(qǐng)求,因此是目前使用較為廣泛的一種限流算法。

四、代碼實(shí)現(xiàn)

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

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

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

1、腳本編寫(xiě)

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

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

但實(shí)際上我們可以通過(guò)對(duì)限流兩次請(qǐng)求之間的時(shí)間和令牌添加速度來(lái)計(jì)算得出上次請(qǐng)求之后到本次請(qǐng)求時(shí),令牌桶應(yīng)添加的令牌數(shù)量。因此我們?cè)?Redis 中只需要存儲(chǔ)上次請(qǐng)求的時(shí)間和令牌桶中的令牌數(shù)量,而桶的大小和令牌的添加速度可以通過(guò)參數(shù)傳入實(shí)現(xiàn)動(dòng)態(tài)修改。

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

編寫(xiě)完成的 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_rateif current_token == nil then  current_token = max_token  last_time = current_timeelse  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  endendlocal result = 0if(current_token>0) then  result = 1  current_token = current_token-1endredis.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
復(fù)制代碼

2、執(zhí)行限流

這里使用 SpringDataRedis 來(lái)進(jìn)行 Redis 腳本的調(diào)用。

編寫(xiě) Redis 腳本類(lèi):

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; }}
復(fù)制代碼

通過(guò) RedisTemplate 對(duì)象執(zhí)行腳本:

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())));  }
復(fù)制代碼

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

@Autowired  private RedisManager redisManager;
@Test public void rateLimitTest() throws InterruptedException { String key = "test_rateLimit_key"; int max = 10; //令牌桶大小 int rate = 10; //令牌每秒恢復(fù)速度 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("請(qǐng)求成功{}次", successCount.get()); }
復(fù)制代碼

設(shè)置令牌桶大小為 10,令牌桶每秒恢復(fù) 10 個(gè),啟動(dòng) 10 個(gè)線程在短時(shí)間內(nèi)進(jìn)行 30 次請(qǐng)求,并輸出每次限流查詢的結(jié)果。日志輸出:

[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]請(qǐng)求成功11次
復(fù)制代碼

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

3、開(kāi)發(fā)中遇到的問(wèn)題

1)Lua 變量格式

Lua 中的 String 和 Number 需要通過(guò) tonumber()和 tostring()進(jìn)行轉(zhuǎn)換。

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

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

3)Time 命令

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

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

4)潛在的隱患

由于此 Lua 腳本是通過(guò)請(qǐng)求時(shí)傳入的時(shí)間做計(jì)算,因此務(wù)必保證分布式節(jié)點(diǎn)上獲取的時(shí)間同步,如果時(shí)間不同步會(huì)導(dǎo)致限流無(wú)法正常運(yùn)作。

作者介紹

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

原文鏈接

https://mp.weixin.qq.com/s/qb3rg_ZpcMcvyaIRsvc1fw

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買(mǎi)等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類(lèi)似文章 更多

    高跟丝袜av在线一区二区三区| 98精品永久免费视频| 中日韩美女黄色一级片| 欧美极品欧美精品欧美| 国产一级内片内射免费看| 国产一区麻豆水好多高潮| 日韩三极片在线免费播放| 国产日韩欧美专区一区| 免费人妻精品一区二区三区久久久| 狠色婷婷久久一区二区三区| 欧美乱妇日本乱码特黄大片| 国产日产欧美精品大秀| 久久99精品国产麻豆婷婷洗澡| 欧美整片精品日韩综合| 久久精品欧美一区二区三不卡| 亚洲天堂男人在线观看| 亚洲最大福利在线观看| 国产精品内射婷婷一级二级| 日本成人三级在线播放| 成年人视频日本大香蕉久久| 欧美成人免费一级特黄| 国产又粗又硬又大又爽的视频| 加勒比日本欧美在线观看| 亚洲中文字幕高清视频在线观看| 国产亚洲精品久久久优势| 青青操日老女人的穴穴| 欧美午夜伦理在线观看| 91午夜少妇极品福利| 91亚洲国产日韩在线| 亚洲国产成人久久99精品| 亚洲熟女少妇精品一区二区三区| 后入美臀少妇一区二区| 久久精品久久精品中文字幕| 亚洲精选91福利在线观看| 天堂网中文字幕在线视频| 欧美乱码精品一区二区三| 97人妻精品免费一区二区| 日韩国产传媒在线精品| 激情综合网俺也狠狠地| 国产一区二区三区四区中文| 亚洲国产av在线视频|