1. 我們?yōu)槭裁葱枰蘖?/h2>
為了“反脆弱”李丰,在微服務(wù)復(fù)雜拓?fù)涞那闆r下惩系,限流是保障服務(wù)彈性和拓?fù)浣训闹刂兄亍?/p>
想一想舵揭,如果業(yè)務(wù)推出了一個(gè)秒殺活動(dòng)讹挎,而你沒(méi)有任何的限流措施校赤;當(dāng)你搭建了一個(gè)賬號(hào)平臺(tái),而完全沒(méi)有對(duì)十幾個(gè)業(yè)務(wù)方設(shè)定流量配額……這些很有可能在特定場(chǎng)合下給你的產(chǎn)品帶來(lái)大量的業(yè)務(wù)損失和口碑影響筒溃。
我們通常重點(diǎn)關(guān)注產(chǎn)品業(yè)務(wù)層面正向和逆向功能的完成马篮,而對(duì)于逆向技術(shù)保障,這一點(diǎn)則是企業(yè)發(fā)展過(guò)程中很容易忽視的铡羡,所以一旦業(yè)務(wù)快速增長(zhǎng)积蔚,這將給你的產(chǎn)品帶來(lái)很大的隱患。
當(dāng)然烦周,也不是所有的系統(tǒng)都需要限流尽爆,這取決于架構(gòu)師對(duì)于當(dāng)前業(yè)務(wù)發(fā)展的預(yù)判怎顾。
2. 我們常見(jiàn)的限流手段
我們來(lái)列舉業(yè)內(nèi)比較常見(jiàn)的一些限流手段。
2.1 信號(hào)量計(jì)數(shù)
信號(hào)量競(jìng)爭(zhēng)是用來(lái)控制并發(fā)的一個(gè)常見(jiàn)手段漱贱。比如 C 和 Java 中都有 Semaphore 的實(shí)現(xiàn)可以讓你方便地上手槐雾。鼎鼎大名的彈性框架 Hystrix 也默認(rèn)選擇了信號(hào)量來(lái)作為隔離和控制并發(fā)的辦法。它的優(yōu)點(diǎn)即在于簡(jiǎn)單可靠幅狮,但是只能在單機(jī)環(huán)境中使用募强。
2.2 線程池隔離
隔離艙技術(shù)中也大量使用了線程池隔離的方式來(lái)實(shí)現(xiàn),通過(guò)限制使用的線程數(shù)來(lái)對(duì)流量進(jìn)行限制崇摄,一般會(huì)用阻塞隊(duì)列配合線程池來(lái)實(shí)現(xiàn)擎值。如果線程池和隊(duì)列都被打滿,可以設(shè)計(jì)對(duì)應(yīng)拒絕策略逐抑。需要謹(jǐn)慎調(diào)整其參數(shù)和線程池隔離的個(gè)數(shù)鸠儿,以避免線程過(guò)多導(dǎo)致上下文切換帶來(lái)的高昂成本。也是基于這個(gè)考慮厕氨,Hystrix 默認(rèn)采用了信號(hào)量計(jì)數(shù)的方式來(lái)控制并發(fā)进每。同樣,其也只能在單機(jī)環(huán)境中使用命斧。
2.3 固定窗口計(jì)數(shù)
我們可以以第一次請(qǐng)求訪問(wèn)的時(shí)候開(kāi)始進(jìn)行計(jì)數(shù)田晚,而不嚴(yán)格按照自然時(shí)間來(lái)計(jì)數(shù)。比如可以利用 Redis 的 INCR 和 EXPIRE 組合進(jìn)行計(jì)數(shù)国葬,如下偽代碼所示:
count = redis.incrby(key)
if count == 1
redis.expire(key,3600)
if count >= threshold
println("exceed...")
這種實(shí)現(xiàn)方式簡(jiǎn)單粗暴贤徒,可以解決絕大部分分布式限流的問(wèn)題。但是其存在的問(wèn)題是:
該計(jì)數(shù)方式并不是準(zhǔn)確計(jì)數(shù)汇四,由于時(shí)間窗口一旦過(guò)期泞莉,則之前積累的數(shù)據(jù)就失效,這樣可能導(dǎo)致比如本來(lái)希望限制“一分鐘內(nèi)訪問(wèn)不能超過(guò) 100 次”船殉,但實(shí)際上做不到精準(zhǔn)的限制鲫趁,會(huì)存在誤判放過(guò)本應(yīng)拒絕的流量。
每次請(qǐng)求都將訪問(wèn)一次 Redis利虫,可能存在大流量并發(fā)時(shí)候?qū)⒕彺娲虮雷罱K拖垮業(yè)務(wù)應(yīng)用的問(wèn)題挨厚。這個(gè)在高并發(fā)場(chǎng)景中是非常嚴(yán)重的問(wèn)題。當(dāng)然糠惫,你可以選擇按照業(yè)務(wù)進(jìn)行適當(dāng)?shù)木彺婕呵懈顏?lái)緩解這種問(wèn)題疫剃,但是這仍然是治標(biāo)不治本。當(dāng)然硼讽,如果你選擇單機(jī)限流的實(shí)現(xiàn)方式巢价,則無(wú)需使用 Redis,進(jìn)一步,單機(jī)限流情況下該問(wèn)題不存在壤躲。
2.4 自然窗口計(jì)數(shù)
有的場(chǎng)景會(huì)需要以自然窗口為維度進(jìn)行限制城菊,實(shí)現(xiàn)方式即進(jìn)行分桶計(jì)數(shù)。每個(gè) slot 一般以時(shí)間戳為 key salt碉克,以單 slot 時(shí)間長(zhǎng)度內(nèi)的計(jì)數(shù)值為 Value凌唬,我們可以根據(jù)實(shí)際的需求對(duì)單 slot 的時(shí)間長(zhǎng)度進(jìn)行限制,比如如果你需要限制一天發(fā)送短信數(shù)不超限漏麦,則以 1 個(gè)自然天為 1 個(gè) slot客税,如果希望限制 QPS,則以 1s 為 1 個(gè) slot撕贞。然后起定時(shí)任務(wù)獲取 slot更耻,進(jìn)一步取出實(shí)際的分桶計(jì)算結(jié)果,進(jìn)行判斷是否達(dá)到閾值捏膨,如果超過(guò)閾值則執(zhí)行對(duì)應(yīng)的限制操作酥夭。
該策略如果應(yīng)用在分布式限流環(huán)境下,則會(huì)碰到若干個(gè)問(wèn)題脊奋。這個(gè)后面章節(jié)中會(huì)提到。另外疙描,該策略本質(zhì)上其實(shí)是也是一種特殊的固定窗口計(jì)數(shù)策略诚隙,那么固定窗口所存在的弊端,自然窗口計(jì)數(shù)也會(huì)存在起胰。那么我們不禁會(huì)問(wèn)久又,如果希望規(guī)避固定窗口的一大問(wèn)題——“無(wú)法準(zhǔn)確計(jì)數(shù)”的話,要怎么做呢效五?這時(shí)地消,“滑動(dòng)窗口計(jì)數(shù)”方式應(yīng)運(yùn)而生。
2.5 滑動(dòng)窗口計(jì)數(shù)
滑動(dòng)窗口的出現(xiàn)畏妖,可以很好地解決精準(zhǔn)計(jì)數(shù)的問(wèn)題脉执。隨著時(shí)間窗口不斷地滑動(dòng),動(dòng)態(tài)地進(jìn)行計(jì)數(shù)判斷戒劫“胍模可以規(guī)避自然窗口和固定窗口計(jì)數(shù)所存在的計(jì)數(shù)不準(zhǔn)確的問(wèn)題。以下有兩種常見(jiàn)的滑動(dòng)窗口計(jì)數(shù)的實(shí)現(xiàn)類別迅细。
2.5.1 基于共享分布式內(nèi)存
可以采用 Redis ZSet巫橄,存儲(chǔ)結(jié)構(gòu)如下圖所示。Key 為功能 ID茵典,Value 為 UUID湘换,Score 也記為同一時(shí)間戳。整個(gè)過(guò)程簡(jiǎn)單概括為“添加記錄、設(shè)置失效時(shí)間彩倚、計(jì)數(shù)筹我、刪除過(guò)期記錄”四部分。使用 ZADD署恍、EXPIRE崎溃、ZCOUNT 和 zremrangeScore 來(lái)實(shí)現(xiàn),并同時(shí)注意開(kāi)啟 Pipeline 來(lái)盡可能提升性能盯质。
偽代碼如下:
// 開(kāi)啟pipe
pipeline = redis.pielined()
// 增加一條請(qǐng)求
pipeline.zadd(key, getUUID(), now)
// 重新設(shè)置失效時(shí)間
pipeline.expire(key, 3600)
// 統(tǒng)計(jì)在滑動(dòng)窗口內(nèi)袁串,有多少次的請(qǐng)求
count = pipeline.zcount(key, expireTimeStamp, now)
// 刪除過(guò)期記錄
pipeline.zremrangeByScore(key, 0, expireTimeStamp - 1)
pipeline.sync()
if count >= threshold
println("exceed")
但是該方法,有一個(gè)比較突出的問(wèn)題呼巷。就是這是一個(gè)重操作囱修,將引發(fā)高 QPS 下 Redis 的性能瓶頸,也將消耗較多的資源和時(shí)間王悍。一般我們可以付出秒級(jí)的時(shí)延破镰,對(duì)其做多階段異步化的處理。比如將計(jì)數(shù)压储、刪除過(guò)期數(shù)據(jù)和新增記錄分為三部分去進(jìn)行異步處理鲜漩。此處就不進(jìn)一步展開(kāi)了。
2.5.2 基于本地內(nèi)存
第一個(gè)方案中集惋,分布式滑動(dòng)窗口的難度在于孕似,不得不進(jìn)行內(nèi)存共享來(lái)達(dá)到窗口計(jì)數(shù)準(zhǔn)確的目的。如果考慮分發(fā)時(shí)進(jìn)行 Key Based Routing 是不是能解決這個(gè)問(wèn)題?在付出非冪等、復(fù)雜度抬升等一定代價(jià)的情況下桩撮,引入基于本地內(nèi)存的分布式限流實(shí)現(xiàn)方式。
實(shí)現(xiàn)方式有如下兩種:
如果可以接受準(zhǔn)實(shí)時(shí)計(jì)算的話泛烙,可以采用 Storm,使用 filedsGroup翘紊,指定 Key 到對(duì)應(yīng)的 Bolt 去處理蔽氨;
如果需要實(shí)時(shí)計(jì)算的話,那么就采用 RPC 框架的 LB 策略為指定 Key 的一致性 Hash帆疟。然后路由到對(duì)應(yīng)的服務(wù)實(shí)例去處理孵滞。
以上兩個(gè)實(shí)現(xiàn)方式,當(dāng)?shù)竭_(dá) Bolt 或者服務(wù)實(shí)例后鸯匹,即可基于本地內(nèi)存進(jìn)行處理坊饶,處理方式也有三種。
采用 Esper殴蓬,用 DSL 語(yǔ)句即可簡(jiǎn)單實(shí)現(xiàn)滑動(dòng)窗口匿级。
Storm 1.0 之后提供了滑動(dòng)窗口的實(shí)現(xiàn)蟋滴。
如果希望自實(shí)現(xiàn)滑動(dòng)窗口(不推薦),實(shí)現(xiàn)思路也比較簡(jiǎn)單即:循環(huán)隊(duì)列+自然窗口滑動(dòng)計(jì)數(shù)痘绎。
循環(huán)隊(duì)列來(lái)解決無(wú)限后延的時(shí)間里津函,計(jì)數(shù)空間重復(fù)利用的問(wèn)題。而此處孤页,我們看到了一個(gè)熟悉的名詞——“自然窗口計(jì)數(shù)”尔苦。沒(méi)錯(cuò),底層仍然采用自然窗口計(jì)數(shù)行施,但是區(qū)別在于允坚,我們會(huì)對(duì)自然窗口切分更細(xì)的粒度,每次批量超前獲取多個(gè)分桶蛾号,來(lái)進(jìn)行加和計(jì)算稠项。這樣就可以實(shí)現(xiàn)滑動(dòng)窗口的效果,你可以認(rèn)為鲜结,當(dāng)分桶被細(xì)化到 10s展运、5s 甚至越來(lái)越細(xì)的時(shí)候,計(jì)數(shù)將趨近于更加準(zhǔn)確精刷。
2.6 令牌桶和漏桶算法計(jì)數(shù)
令牌桶的示意圖如下:
而漏桶的示意圖如下:
這個(gè)在業(yè)內(nèi)也是鼎鼎大名拗胜。基本談起限流算法怒允,這兩個(gè)算法必然會(huì)被提起埂软,令牌桶可以有流量應(yīng)對(duì)突發(fā)流量,漏桶則強(qiáng)調(diào)對(duì)流量的整型误算。二者的模型是相反的。令牌桶和漏桶算法在單機(jī)限流中較為常見(jiàn)迷殿,而在分布式限流中罕見(jiàn)蹤跡儿礼。
對(duì)于令牌桶來(lái)說(shuō),你可以采用定時(shí)任務(wù)去做投遞令牌的動(dòng)作庆寺,也可以采用算法的方式去進(jìn)行簡(jiǎn)單的計(jì)算蚊夫。Guava Ratelimiter 采用的是后者。
令牌桶的優(yōu)勢(shì)之一懦尝,在于可以有部分余量用以應(yīng)對(duì)突發(fā)流量知纷。但是在實(shí)際生產(chǎn)環(huán)境中,這不一定是安全的陵霉。如果我們的服務(wù)沒(méi)有做好應(yīng)對(duì)更高突發(fā)流量的準(zhǔn)備琅轧,那么很有可能會(huì)引發(fā)服務(wù)雪崩。所以考慮到這一點(diǎn)踊挠,Guava 采用了令牌桶 + 漏桶結(jié)合的策略來(lái)進(jìn)行限流乍桂。對(duì)于默認(rèn)業(yè)務(wù)冲杀,采用標(biāo)準(zhǔn)令牌桶方式進(jìn)行“可超支”限速,而對(duì)于無(wú)法突然應(yīng)對(duì)高峰流量的業(yè)務(wù)睹酌,會(huì)采用緩慢提升投放令牌速率(即逐步縮短業(yè)務(wù)請(qǐng)求等待時(shí)間)的方式來(lái)進(jìn)行熱啟動(dòng)控制权谁,具體見(jiàn) Guava Ratelimiter 源碼注釋描述,此處不贅述憋沿,其效果如下圖所示:
3.微服務(wù)限流幾個(gè)考慮的點(diǎn)
以上的限流手段旺芽,有的能應(yīng)用在單機(jī)環(huán)境,有的能應(yīng)用在分布式環(huán)境辐啄。而在高并發(fā)的分布式環(huán)境中采章,我們需要考慮清楚如下幾個(gè)問(wèn)題如何解決。
3.1 機(jī)器時(shí)鐘不一致或者時(shí)鐘回退問(wèn)題
一旦出現(xiàn)這種問(wèn)題则披,則可能導(dǎo)致收集的數(shù)據(jù)相互污染而導(dǎo)致判斷出錯(cuò)共缕。所以一方面,在運(yùn)維層面需要確保機(jī)器時(shí)鐘能夠按期同步士复。另一方面图谷,需要有準(zhǔn)實(shí)時(shí)檢測(cè)的手段,及時(shí)發(fā)現(xiàn)時(shí)鐘偏差太大或者時(shí)鐘回退的機(jī)器阱洪,基于一定策略篩選出不合格的數(shù)據(jù)來(lái)源便贵,將其刨除出計(jì)算范圍并發(fā)出警告。
3.2 在 SDK 還是 Server 端做限流邏輯
你需要考慮你的限流策略迭代的頻繁程度冗荸,推動(dòng)業(yè)務(wù)方改造的成本承璃,語(yǔ)言/技術(shù)棧異構(gòu)情況,是否有需要進(jìn)行立多系統(tǒng)聯(lián)合限流的場(chǎng)景蚌本,以此來(lái)進(jìn)行決策盔粹。如果采用 SDK 方式,你需要做好碰到這幾個(gè)棘手問(wèn)題的心理準(zhǔn)備程癌。
而如果采用 Server 方式舷嗡,你則需要更多考慮高并發(fā)下數(shù)據(jù)堆積,機(jī)器資源消耗嵌莉,以及對(duì)業(yè)務(wù)方性能的影響問(wèn)題进萄。一般業(yè)內(nèi)采用的是富 SDK 的方式來(lái)做,但是對(duì)于上述的 SDK 會(huì)面臨的幾個(gè)問(wèn)題沒(méi)有很好的解決方案锐峭。而 ServiceMesh 領(lǐng)軍人物 Istio 采用了 Mixer 來(lái)實(shí)現(xiàn) Server 端限流的方式中鼠,但是碰到了很嚴(yán)重的性能問(wèn)題。所以這是一個(gè)很困難的選擇沿癞。
回顧下架構(gòu)師成長(zhǎng)之路之服務(wù)治理漫談一篇中所講到的服務(wù)治理發(fā)展路徑援雇,是不是有點(diǎn)驚人的相似?是不是也許限流的未來(lái)椎扬,不在 SDK 也不在 Server熊杨,而在于 ServiceMesh曙旭?我不確定,但我覺(jué)得這是一個(gè)很好的探索方向晶府。
3.3 限流是不是會(huì)讓你的系統(tǒng)變得不可控
這是一個(gè)很有意思的問(wèn)題桂躏,限流本身是為了“反脆弱”而存在的,但是如果你的分布式復(fù)雜拓?fù)渲斜椴枷蘖鞴δ艽剑敲匆院竽忝總€(gè)服務(wù)的擴(kuò)容剂习,新的功能上線,拓?fù)浣Y(jié)構(gòu)的變更较沪,都有可能會(huì)導(dǎo)致局部服務(wù)流量的驟增鳞绕,進(jìn)一步引發(fā)限流導(dǎo)致業(yè)務(wù)有損問(wèn)題。這就是“反脆弱”的本身也有可能會(huì)導(dǎo)致“脆弱”的出現(xiàn)尸曼。所以们何,當(dāng)你進(jìn)行大規(guī)模限流能力擴(kuò)張覆蓋的時(shí)候,需要謹(jǐn)慎審視你的限流能力和成熟度是否能夠支撐起如此大規(guī)模的應(yīng)用控轿。
3.4 拓?fù)涞年P(guān)聯(lián)性能給限流帶來(lái)什么
我們置身于復(fù)雜服務(wù)拓?fù)浜透鞣N調(diào)用鏈路中冤竹,這一方面確實(shí)給限流帶來(lái)了很大的麻煩,但另一方面茬射,我們是不是可以思考一下鹦蠕,這些復(fù)雜度,本身是不是可以帶給我們什么樣的利好在抛?比如:底層服務(wù)扛不住钟病,那么是不是可以在更上層的調(diào)用方入口進(jìn)行限流?如此是不是可以給予用戶更友好提示的同時(shí)刚梭,也可避免鏈路上服務(wù)各自限流后帶來(lái)的系統(tǒng)級(jí)聯(lián)處理壓力肠阱?微服務(wù)的本質(zhì)是自治沒(méi)錯(cuò),但是我們是不是可以更好地對(duì)各個(gè)服務(wù)的限流自治能力進(jìn)行編排朴读,以達(dá)到效率屹徘、體驗(yàn)、資源利用的優(yōu)化磨德?
相信大家都會(huì)有自己的答案缘回。這件事情本身的難度是在于決策的準(zhǔn)確性典挑,但如果能很好地進(jìn)行落地實(shí)現(xiàn),則意味著我們的限流從自動(dòng)化已經(jīng)逐步轉(zhuǎn)向了智能化啦吧。這也將是更高一層次的挑戰(zhàn)和機(jī)遇授滓。
3.5 準(zhǔn)確性和實(shí)時(shí)性的權(quán)衡
在高并發(fā)限流場(chǎng)景下在孝,準(zhǔn)確性和實(shí)時(shí)性理論上不可兼得。在特定的場(chǎng)景中仔燕,你需要作出你的選擇造垛,比如前文介紹的基于 Redis ZSet 實(shí)現(xiàn)的滑動(dòng)窗口實(shí)時(shí)計(jì)算方式可以滿足實(shí)時(shí)性和準(zhǔn)確性,但其會(huì)帶來(lái)很明顯的性能問(wèn)題晰搀。所以我們需要作出我們的權(quán)衡五辽,比如犧牲準(zhǔn)確性將滑動(dòng)窗口退化為固定窗口來(lái)保障性能;或者犧牲實(shí)時(shí)性外恕,對(duì)滑動(dòng)窗口多階段去做異步化杆逗,分析和決策兩階段分離,來(lái)保障性能吁讨。這取決于你的判斷髓迎。
4. 總結(jié)
限流是高可用治理中核心的一環(huán),實(shí)現(xiàn)方式也五花八門建丧,每種方式也都有各自的問(wèn)題排龄,本文只是做了一個(gè)簡(jiǎn)單的回顧。希望隨著 ServiceMesh翎朱、AIOps 等理論的興起橄维,我們對(duì)于限流是什么,能做什么拴曲,怎么實(shí)現(xiàn)争舞,能夠釋放出更大的空間去想象。
讀者福利
針對(duì)于上面的文章我總結(jié)出了互聯(lián)網(wǎng)公司java程序員面試涉及到的絕大部分面試題及答案做成了文檔和架構(gòu)視頻資料免費(fèi)分享給大家(包括Dubbo澈灼、Redis竞川、Netty、zookeeper叁熔、Spring cloud委乌、分布式、高并發(fā)等架構(gòu)技術(shù)資料)荣回,希望能幫助到您面試前的復(fù)習(xí)且找到一個(gè)好的工作遭贸,也節(jié)省大家在網(wǎng)上搜索資料的時(shí)間來(lái)學(xué)習(xí)。
資料獲取方式:加qun群:956011797點(diǎn)擊立即加入 找管理小姐姐免費(fèi)獲刃娜怼壕吹!
合理利用自己每一分每一秒的時(shí)間來(lái)學(xué)習(xí)提升自己著蛙,不要再用"沒(méi)有時(shí)間“來(lái)掩飾自己思想上的懶惰!趁年輕耳贬,使勁拼踏堡,給未來(lái)的自己一個(gè)交代!