<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
我們目前在工作中遇到一個效能問題,我們有個定時任務需要處理大量的資料,為了提升吞吐量,所以部署了很多臺機器,但這個任務在執行前需要從別的服務那拉取大量的資料,隨著資料量的增大,如果同時多臺機器並行拉取資料,會對下游服務產生非常大的壓力。之前已經增加了單機限流,但無法解決問題,因為這個資料任務執行中只有不到10%的時間拉取資料,如果單機限流限制太狠,雖然叢集總的請求量控制住了,但任務吞吐量又降下來。如果限流閾值太高,多機並行的時候,還是有可能壓垮下游。 所以目前唯一可行的解決方案就是分散式限流。
我目前是選擇直接使用Redisson庫中的RRateLimiter實現了分散式限流,關於Redission可能很多人都有所耳聞,它其實是在Redis能力上構建的開發庫,除了支援Redis的基礎操作外,還封裝了布隆過濾器、分散式鎖、限流器……等工具。今天要說的RRateLimiter及時其實現的限流器。接下來本文將詳細介紹下RRateLimiter的具體使用方式、實現原理還有一些注意事項,最後簡單談談我對分散式限流底層原理的理解。
RRateLimiter的使用方式異常的簡單,引數也不多。只要建立出RedissonClient,就可以從client中獲取到RRateLimiter物件,直接看程式碼範例。
RedissonClient redissonClient = Redisson.create(); RRateLimiter rateLimiter = redissonClient.getRateLimiter("xindoo.limiter"); rateLimiter.trySetRate(RateType.OVERALL, 100, 1, RateIntervalUnit.HOURS);
rateLimiter.trySetRate就是設定限流引數,RateType有兩種,OVERALL是全域性限流 ,PER_CLIENT是單Client限流(可以認為就是單機限流),這裡我們只討論全域性模式。而後面三個引數的作用就是設定在多長時間視窗內(rateInterval+IntervalUnit),許可總量不超過多少(rate),上面程式碼中我設定的值就是1小時內總許可數不超過100個。然後呼叫rateLimiter的tryAcquire()或者acquire()方法即可獲取許可。
rateLimiter.acquire(1); // 申請1份許可,直到成功 boolean res = rateLimiter.tryAcquire(1, 5, TimeUnit.SECONDS); // 申請1份許可,如果5s內未申請到就放棄
使用起來還是很簡單的嘛,以上程式碼中的兩種方式都是同步呼叫,但Redisson還同樣提供了非同步方法acquireAsync()和tryAcquireAsync(),使用其返回的RFuture就可以非同步獲取許可。
接下來我們順著tryAcquire()方法來看下它的實現方式,在RedissonRateLimiter類中,我們可以看到最底層的tryAcquireAsync()方法。
private <T> RFuture<T> tryAcquireAsync(RedisCommand<T> command, Long value) { byte[] random = new byte[8]; ThreadLocalRandom.current().nextBytes(random); return commandExecutor.evalWriteAsync(getRawName(), LongCodec.INSTANCE, command, "——————————————————————————————————————" + "這裡是一大段lua程式碼" + "____________________________________", Arrays.asList(getRawName(), getValueName(), getClientValueName(), getPermitsName(), getClientPermitsName()), value, System.currentTimeMillis(), random); }
映入眼簾的就是一大段lua程式碼,其實這段Lua程式碼就是限流實現的核心,我把這段lua程式碼摘出來,並加了一些註釋,我們來詳細看下。
local rate = redis.call("hget", KEYS[1], "rate") # 100 local interval = redis.call("hget", KEYS[1], "interval") # 3600000 local type = redis.call("hget", KEYS[1], "type") # 0 assert(rate ~= false and interval ~= false and type ~= false, "RateLimiter is not initialized") local valueName = KEYS[2] # {xindoo.limiter}:value 用來儲存剩餘許可數量 local permitsName = KEYS[4] # {xindoo.limiter}:permits 記錄了所有許可發出的時間戳 # 如果是單範例模式,name資訊後面就需要拼接上clientId來區分出來了 if type == "1" then valueName = KEYS[3] # {xindoo.limiter}:value:b474c7d5-862c-4be2-9656-f4011c269d54 permitsName = KEYS[5] # {xindoo.limiter}:permits:b474c7d5-862c-4be2-9656-f4011c269d54 end # 對引數校驗 assert(tonumber(rate) >= tonumber(ARGV[1]), "Requested permits amount could not exceed defined rate") # 獲取當前還有多少許可 local currentValue = redis.call("get", valueName) local res # 如果有記錄當前還剩餘多少許可 if currentValue ~= false then # 回收已過期的許可數量 local expiredValues = redis.call("zrangebyscore", permitsName, 0, tonumber(ARGV[2]) - interval) local released = 0 for i, v in ipairs(expiredValues) do local random, permits = struct.unpack("Bc0I", v) released = released + permits end # 清理已過期的許可記錄 if released > 0 then redis.call("zremrangebyscore", permitsName, 0, tonumber(ARGV[2]) - interval) if tonumber(currentValue) + released > tonumber(rate) then currentValue = tonumber(rate) - redis.call("zcard", permitsName) else currentValue = tonumber(currentValue) + released end redis.call("set", valueName, currentValue) end # ARGV permit timestamp random, random是一個隨機的8位元組 # 如果剩餘許可不夠,需要在res中返回下個許可需要等待多長時間 if tonumber(currentValue) < tonumber(ARGV[1]) then local firstValue = redis.call("zrange", permitsName, 0, 0, "withscores") res = 3 + interval - (tonumber(ARGV[2]) - tonumber(firstValue[2])) else redis.call("zadd", permitsName, ARGV[2], struct.pack("Bc0I", string.len(ARGV[3]), ARGV[3], ARGV[1])) # 減小可用許可量 redis.call("decrby", valueName, ARGV[1]) res = nil end else # 反之,記錄到還有多少許可,說明是初次使用或者之前已記錄的資訊已經過期了,就將設定rate寫進去,並減少許可數 redis.call("set", valueName, rate) redis.call("zadd", permitsName, ARGV[2], struct.pack("Bc0I", string.len(ARGV[3]), ARGV[3], ARGV[1])) redis.call("decrby", valueName, ARGV[1]) res = nil end local ttl = redis.call("pttl", KEYS[1]) # 重置 if ttl > 0 then redis.call("pexpire", valueName, ttl) redis.call("pexpire", permitsName, ttl) end return res
即便是加了註釋,相信你還是很難一下子看懂這段程式碼的,接下來我就以其在Redis中的資料儲存形式,然輔以流程圖讓大家徹底瞭解其實現實現原理。
首先用RRateLimiter有個name,在我程式碼中就是xindoo.limiter
,用這個作為KEY你就可以在Redis中找到一個map,裡面儲存了limiter的工作模式(type)、可數量(rate)、時間視窗大小(interval),這些都是在limiter建立時寫入到的redis中的,在上面的lua程式碼中也使用到了。
其次還倆很重要的key,valueName和permitsName,其中在我的程式碼實現中valueName是{xindoo.limiter}:value
,它儲存的是當前可用的許可數量。我程式碼中permitsName的具體值是{xindoo.limiter}:permits
,它是一個zset,其中儲存了當前所有的許可授權記錄(含有許可授權時間戳),其中SCORE直接使用了時間戳,而VALUE中包含了8位元組的隨機值和許可的數量,如下圖:
{xindoo.limiter}:permits這個zset中儲存了所有的歷史授權記錄,直到了這些資訊,相信你也就理解了RRateLimiter的實現原理,我們還是將上面的那大段Lua程式碼的流程圖繪製出來,整個執行的流程會更直觀。
看到這大家應該能理解這段Lua程式碼的邏輯了,可以看到Redis用了多個欄位來儲存限流的資訊,也有各種各樣的操作,那Redis是如何保證在分散式下這些限流資訊資料的一致性的?
答案是不需要保證,在這個場景下,資訊天然就是一致性的。原因是Redis的單程序資料處理模型,在同一個Key下,所有的eval請求都是序列的,所有不需要考慮資料並行操作的問題。在這裡,Redisson也使用了HashTag,保證所有的限流資訊都儲存在同一個Redis範例上。
瞭解了RRateLimiter的底層原理,再結合Redis自身的特性,我想到了RRateLimiter使用的幾個侷限點(問題點)。
這個是我查閱資料得知,並且在自己程式碼實踐的過程中也得到了驗證,具體表現就是如果多個範例(機器)取競爭這些許可,很可能某些範例會獲取到大部分,而另外一些範例可憐巴巴僅獲取到少量的許可,也就是說容易出現旱的旱死 澇的澇死的情況。在使用過程中,你就必須考慮你能否接受這種情況,如果不能接受就得考慮用某些方式儘可能讓其變公平。
從RRateLimiter的實現原理你也看出了,它採用的是滑動視窗的模式來限流的,而且記錄了所有的許可授權資訊,所以如果你設定的Rate值過大,在Redis中儲存的資訊(permitsName對應的zset)也就越多,每次執行那段lua指令碼的效能也就越差,這對Redis範例也是一種壓力。個人建議如果你是想設定較大的限流閾值,傾向於小Rate+小時間視窗的方式,而且這種設定方式請求也會更均勻一些。
從原理上看,RRateLimiter在Redis上所儲存的資訊都必須在一個Redis範例上,所以它的限流QPS的上限就是Redis單範例的上限,比如你Redis範例就是1w QPS,你想用RRateLimiter實現一個2w QPS的限流器,必然實現不了。 那有沒有突破Redis單範例效能上限的方式?單限流器肯定是實現不了的,我們可以拆分多個限流器,比如我搞10個限流器,名詞用不一樣的,然後每臺機器隨機使用一個限流器限流,實際的流量不就被分散到不同的限流器上了嗎,總的限流上線不也就上來了。
分散式限流的本質實際上就是協同,協同的本質就是資訊交換,資訊交換最重要的的就是資訊的準確性和一致性。 更簡單粗暴理解,分散式限流的本質原理其實還是分散式資料一致性的原理,而限流只是資料結果的一種決策。所以只要以任何方式能讓資訊同步,且保證資訊的正確性就可以實現一個分散式限流器了,這就是我理解的本質思路。
其實從上面的RRateLimiter的實現原理也可以看出來,它不就是儲存了一些資訊嗎! 那我不用Redis,而是使用mysql行不行。實際肯定是可以的,只要將的上面Lua程式碼中的所有操作都放到一個事務裡,且事務的級別改成序列化,依舊能實現RRateLimiter同樣的功能。如果你具備Mysql相關知識的話,肯定也能基於Mysql將RRateLimiter的API封裝出來,但是封裝出來的限流器,其限流的上限就取決於Mysql範例的效能上限。
最近chatGPT比較火,我也問了下它對分散式限流本質原理的理解,下面是它的回答,大家覺得怎麼樣?
分散式限流的本質原理是通過在分散式系統中共用限流狀態來限制系統中單位時間內的請求數量,從而避免系統因流量過大而崩潰。
這是通過使用一些共用的儲存元件,如資料庫,快取,分散式鎖等來實現的。在每次請求時,系統會檢查當前的請求數量是否超過了預先設定的限制,如果超過了限制,請求就會被拒絕;如果未超過限制,請求就會被允許。
通過使用分散式限流技術,系統可以在高並行情況下保持穩定的效能,並避免因流量過大而導致的系統崩潰。
以上就是Redisson分散式限流的實現原理解析的詳細內容,更多關於Redisson分散式限流的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45