Guava源碼中很詳盡的解釋了RateLimiter的概念。
從概念上看疏旨,限流器以配置速率釋放允許的請(qǐng)求(permit)膝擂。如有必要,調(diào)用acquire()將會(huì)阻塞知道一個(gè)允許可用作媚。一旦被獲取(acquired)伞访,允許(permits)將不必釋放掂骏。
限流器在并發(fā)環(huán)境中是安全的:它限制所有線程總的調(diào)用速率。但是厚掷,值得注意的是弟灼,它難以保證公平。
限流器經(jīng)常被用來限制一些物理或邏輯資源被訪問的速率冒黑。經(jīng)常和它對(duì)比的是j.u.c.Semaphore田绑,它限制了訪問資源總的并發(fā)數(shù)。(并發(fā)數(shù)和速率緊密相關(guān)抡爹,參見Little's Law)
限流器原始定義為許可被發(fā)布的速率掩驱。沒有多余的配置,許可將被以固定速率(——字面意義被定義為 許可/sec) 分發(fā)冬竟。通過調(diào)節(jié)獨(dú)立的許可之間的延遲欧穴,保證許可按照配置的速率平滑分發(fā)。
在限流器正式進(jìn)入穩(wěn)定速率前泵殴,通常允許限流器有一個(gè)短暫的預(yù)熱階段涮帘。在該階段,許可分發(fā)速率穩(wěn)步提升笑诅,直至預(yù)定到達(dá)速率為止调缨。
舉例,想象我們有一組任務(wù)要執(zhí)行吆你,但我們不想要每秒提交超過2個(gè)任務(wù)弦叶。
final RateLimiter rateLimiter = RateLimiter.create(2.0); // rate is 2 permits per second"
void submitTasks(List<Runnable> tasks, Executor executor) {
for (Runnable task : tasks) {
rateLimiter.acquire(); // may wait
executor.execute(task);
}
}
另一個(gè)例子是,想象我們生產(chǎn)一組數(shù)據(jù)流妇多,但是我們想以5kb/sec速率恒定接收它伤哺。這個(gè)想法可以以限流器方式實(shí)現(xiàn)。即砌梆,每個(gè)許可對(duì)應(yīng)一個(gè)字節(jié)默责,指定(限流器)恒定的速率為每秒5000次許可贬循。
final RateLimiter rateLimiter = RateLimiter.create(5000.0); // rate = 5000 permits per second
void submitPacket(byte[] packet) {
rateLimiter.acquire(packet.length);
networkService.send(packet);
}
注意咸包,請(qǐng)求的許可數(shù)量不會(huì)影響到對(duì)請(qǐng)求本身的壓制桃序。(acquire(1)會(huì)和acquire(1000)產(chǎn)生相同的效果),但是它會(huì)影響到對(duì)下一個(gè)請(qǐng)求的壓制作用烂瘫。如果成本較大的任務(wù)在空閑時(shí)到達(dá)限流器媒熊,將會(huì)被立即允許。但這之后的請(qǐng)求將會(huì)遭受額外的限制坟比,為上次高昂代價(jià)的任務(wù)買單芦鳍。
下面是一個(gè)SmoothRateLimiter的設(shè)計(jì)原理:
限流器的基本提點(diǎn)是一個(gè)“穩(wěn)定的速率”,即在正常條件下的最大速率葛账。未達(dá)到這個(gè)目的柠衅,限流器會(huì)根據(jù)需要壓制到達(dá)的請(qǐng)求。通過計(jì)算籍琳,限流器會(huì)確保到達(dá)請(qǐng)求等待合理的時(shí)間菲宴,以此達(dá)到壓制的目的。
維持一個(gè)速率(通常被指定為QPS)最簡(jiǎn)單的方法是記住上個(gè)被允許請(qǐng)求的最后時(shí)間戳趋急,并保證在1/QPS時(shí)間內(nèi)不執(zhí)行請(qǐng)求喝峦。例如,QPS=5(每秒5個(gè)token)呜达,如果我們能夠確保自從上個(gè)請(qǐng)求后谣蠢,在200ms內(nèi)沒有請(qǐng)求被允許執(zhí)行,那么我們將獲得一個(gè)想要的速率查近。如果一個(gè)請(qǐng)求在上個(gè)請(qǐng)求被放行100ms后到達(dá)眉踱,那么我們需要等待額外的100ms。在這個(gè)速率下霜威,15個(gè)新的許可耗時(shí)3秒鐘(例如對(duì)于請(qǐng)求 acquire(15))谈喳。
很重要的一點(diǎn),是能夠意識(shí)到限流器對(duì)過去只有很淺的記憶侥祭。它只會(huì)記住上一個(gè)請(qǐng)求叁执。那如果限流器很久沒有被使用,然后一個(gè)請(qǐng)求突然到達(dá)并被立即允許怎么辦矮冬?這可能會(huì)有兩種情形谈宛,一種是資源利用不充分,另一種則是導(dǎo)致溢出胎署,具體取決于沒有遵循預(yù)定速率的真實(shí)原因吆录。
之前的利用不足意味多余的資源可被獲取。限流器應(yīng)該加速一段時(shí)間琼牧,以利用這些資源恢筝。速率適應(yīng)網(wǎng)絡(luò)(帶寬)很重要哀卫,過去的利用不足被解釋為“幾乎為空的緩沖”,可以被快速填補(bǔ)撬槽。
另一方面此改,過去的利用不足也可能意味著“服務(wù)器沒有準(zhǔn)備好處理將來的請(qǐng)求”,例如侄柔,緩存失效共啃,請(qǐng)求更有可能會(huì)觸發(fā)耗時(shí)的操作(一個(gè)更極端的例子是,當(dāng)一個(gè)服務(wù)器剛剛被引導(dǎo)暂题,它更可能忙于自身的喚醒)移剪。
為應(yīng)對(duì)以上場(chǎng)景,我們?cè)鎏硪环N維度薪者。即“過去的利用不足”被建模為變量“storedPermits”纵苛。這個(gè)變量在沒有使用時(shí)為0,當(dāng)有大量使用時(shí)言津,它可以增長(zhǎng)到maxStoredPermits攻人。所以,請(qǐng)求會(huì)被函數(shù)acquire(permits)觸發(fā)許可纺念,提供以下兩種類型的許可:
-stored permits(可獲取的已存許可)
-fresh permits(新的的許可)
工作原理如下:
對(duì)于一個(gè)限流器贝椿,每秒產(chǎn)生一個(gè)令牌。不使用限流器時(shí)陷谱,我們都會(huì)給storedPermits加1烙博。如果說我們有10sec不使用限流器(例如預(yù)計(jì)請(qǐng)求在時(shí)刻X到來,但在請(qǐng)求到來之前烟逊,我們?cè)赬+10渣窜。這也是上段所描述的點(diǎn)。)宪躯。因此storedPermits變?yōu)?0(假設(shè)maxStoredPermits>=10)乔宿。在這時(shí),一個(gè)人acquire(3)的請(qǐng)求到了访雪。我們從已有的storedPermits拿出許可服務(wù)這個(gè)請(qǐng)求详瑞,并將許可數(shù)降至7.(這如何被解釋為壓制時(shí)間,將會(huì)在之后被詳細(xì)討論臣缀。)這之后坝橡,假設(shè)馬上有一個(gè)acquire(10)的請(qǐng)求到達(dá),我們用剩下所有的7個(gè)許可數(shù)來應(yīng)對(duì)這個(gè)請(qǐng)求精置,還有3個(gè)許可數(shù)计寇,我們需要通過刷新限流器新提供。
我們也已經(jīng)知道花費(fèi)在3個(gè)新的許可上的時(shí)間:如果速率是1令牌/sec,那么我們將花費(fèi)3秒番宁。但是使用7個(gè)已存許可又是什么意思呢元莫?正如上面所說,這里沒有固定答案蝶押。如果我們主要興趣在應(yīng)對(duì)資源利用不足上踱蠢,我們想要存儲(chǔ)許可釋放比刷新許可快。因?yàn)槔貌蛔?尚有未被占用的資源播聪。如果我們主要興趣點(diǎn)在應(yīng)對(duì)溢出朽基,那么存儲(chǔ)的許可數(shù)應(yīng)該釋放的比刷新的慢布隔。因此离陶,我們想要一個(gè)(在每種情形都不同的)方法來解釋storePermits,以此壓制時(shí)間衅檀。storedPermitsToWaitTime(double storedPermits, double permitsToTake) 在其中扮演重要角色招刨。底層的模型是一個(gè)持續(xù)變化的函數(shù)映射storedPermits(從0到maxStoredPermits)到1/rate(時(shí)間間隔)。storedPermits在衡量未使用時(shí)間上是必不可少的哀军。我們使用未利用時(shí)間換取許可數(shù)(permits)沉眶。速率是permits/time,因此1/rate=time/permits.因此"1/rate"(time/permits)乘以permits等于給定時(shí)間杉适。對(duì)于指定數(shù)量的請(qǐng)求許可來說谎倔,這個(gè)積分函數(shù)(storedPermitsToWaitTime()計(jì)算)與持續(xù)請(qǐng)求的最小時(shí)間間隔相關(guān)。
這里有個(gè)storePermitsToWaitTime的例子猿推。如果storedPermits=10片习,我們想要3個(gè)permits,我們從storedPermits中去獲取蹬叭,減少他們到7個(gè)藕咏,并且計(jì)算壓制時(shí)間作為一個(gè)調(diào)用storedPermitsToWaitTime(storedPermits=10,permitsToTake=3)秽五,這將會(huì)評(píng)估這個(gè)函數(shù)積分從7到10.
使用積分保證acquire(3)效果等同于3次acquire(1)孽查,或一次acquire(2)+一次acquire(1)。因?yàn)榉e分在[7.0,10.0]等同于在[7.0,8.0],[8.0,9.0],[9.0,10.0]等等坦喘。無論這個(gè)函數(shù)是什么盲再。這使得我們可以正確處理不同權(quán)重(permits)的請(qǐng)求時(shí),不論真正的函數(shù)是什么瓣铣。所以我們可以自由調(diào)整答朋。(唯一的條件顯然是我們能夠計(jì)算出他的間隔時(shí)間)。
注意坯沪,對(duì)于這個(gè)函數(shù)绿映,我們選擇水平線,高度為1/QPS,因此這個(gè)函數(shù)的影響是不存在的叉弦。對(duì)于storedPermits將會(huì)完全等同于刷新一個(gè)新的(1/QPS是他的代價(jià))丐一。我們將會(huì)在之后使用這個(gè)小訣竅。
如果我們采用一個(gè)低于這條水平線的函數(shù)淹冰,這意味著我們減少了這個(gè)函數(shù)的區(qū)域库车,也就是時(shí)間。因此限流器就會(huì)在一段時(shí)間的利用不足后變快樱拴。另一方面柠衍,如果我們使用一個(gè)高于此水平線的函數(shù),這就意味著代表時(shí)間的區(qū)域增大晶乔,因此storedPermits將會(huì)比刷新一個(gè)新許可更耗時(shí)珍坊,相應(yīng)地,限流器就會(huì)在一段時(shí)間的利用不足后變慢正罢。
最后阵漏,考慮一個(gè)限流器以1permit/sec速率,當(dāng)前未被使用翻具,有一個(gè)acquire(100)的請(qǐng)求到來履怯。等待100sec才開始執(zhí)行任務(wù)將會(huì)是很愚蠢的行為。為什么不作任何事情只等待呢裆泳?一個(gè)更好的方法是立刻允許請(qǐng)求(正如它是acquire(1)的請(qǐng)求一樣)叹洲,并且按需要延緩此后的需求。在這個(gè)版本工禾,我們?cè)试S立刻開始執(zhí)行任務(wù)运提,并且延緩100秒之后的請(qǐng)求,因此我們?cè)试S工作執(zhí)行而不是讓它空閑等待帜篇。
這里有很重要的因果關(guān)系糙捺。這意味著限流器不會(huì)記住最后請(qǐng)求的時(shí)刻,但它會(huì)記住下一個(gè)請(qǐng)求(預(yù)計(jì))時(shí)間笙隙。這也使我們能夠立即知道(見tryAcquire(timeout))指定時(shí)間timeout是否足夠?qū)⑽覀儙У较乱粋€(gè)調(diào)度的時(shí)間點(diǎn)洪灯,因?yàn)槲覀兛偩S持那個(gè)。并且我們所指的“未被使用的限流器”也被這所定義:但我們觀察“下一個(gè)請(qǐng)求的期待到達(dá)時(shí)間”在過去竟痰,那么(now-past)的時(shí)間差將被看作RateLimiter未被正式使用時(shí)間签钩。這也是被我們解釋為storedPermits的時(shí)間。(我們用空閑的時(shí)間產(chǎn)生的許可數(shù)來增加storedPermits)坏快。所以铅檩,如果速率=1許可/sec,并且請(qǐng)求在之前那個(gè)請(qǐng)求后一秒后準(zhǔn)時(shí)到達(dá)莽鸿,那么storedPermits將永遠(yuǎn)不會(huì)增加昧旨。我們只會(huì)在當(dāng)晚于預(yù)期一秒時(shí)間的到達(dá)拾给,才會(huì)增加它。