?1. 我們?yōu)槭裁葱枰蘖?br>
在上一篇架構(gòu)師成長之路之服務(wù)治理漫談里面榨了,我們已經(jīng)談到了高可用治理的部分珊燎。為了“反脆弱”评也,在微服務(wù)復(fù)雜拓?fù)涞那闆r下,限流是保障服務(wù)彈性和拓?fù)浣训闹刂兄亍?/p>
想一想泳桦,如果業(yè)務(wù)推出了一個(gè)秒殺活動,而你沒有任何的限流措施娩缰;當(dāng)你搭建了一個(gè)賬號平臺灸撰,而完全沒有對十幾個(gè)業(yè)務(wù)方設(shè)定流量配額……這些很有可能在特定場合下給你的產(chǎn)品帶來大量的業(yè)務(wù)損失和口碑影響。
我們通常重點(diǎn)關(guān)注產(chǎn)品業(yè)務(wù)層面正向和逆向功能的完成拼坎,而對于逆向技術(shù)保障浮毯,這一點(diǎn)則是企業(yè)發(fā)展過程中很容易忽視的,所以一旦業(yè)務(wù)快速增長泰鸡,這將給你的產(chǎn)品帶來很大的隱患债蓝。
當(dāng)然,也不是所有的系統(tǒng)都需要限流盛龄,這取決于架構(gòu)師對于當(dāng)前業(yè)務(wù)發(fā)展的預(yù)判饰迹。
2. 我們常見的限流手段
我們來列舉業(yè)內(nèi)比較常見的一些限流手段。
2.1 信號量計(jì)數(shù)
信號量競爭是用來控制并發(fā)的一個(gè)常見手段余舶。比如 C 和 Java 中都有 Semaphore 的實(shí)現(xiàn)可以讓你方便地上手啊鸭。鼎鼎大名的彈性框架 Hystrix 也默認(rèn)選擇了信號量來作為隔離和控制并發(fā)的辦法。它的優(yōu)點(diǎn)即在于簡單可靠匿值,但是只能在單機(jī)環(huán)境中使用赠制。
2.2 線程池隔離
隔離艙技術(shù)中也大量使用了線程池隔離的方式來實(shí)現(xiàn),通過限制使用的線程數(shù)來對流量進(jìn)行限制千扔,一般會用阻塞隊(duì)列配合線程池來實(shí)現(xiàn)憎妙。如果線程池和隊(duì)列都被打滿,可以設(shè)計(jì)對應(yīng)拒絕策略曲楚。需要謹(jǐn)慎調(diào)整其參數(shù)和線程池隔離的個(gè)數(shù)厘唾,以避免線程過多導(dǎo)致上下文切換帶來的高昂成本。也是基于這個(gè)考慮龙誊,Hystrix 默認(rèn)采用了信號量計(jì)數(shù)的方式來控制并發(fā)抚垃。同樣,其也只能在單機(jī)環(huán)境中使用。
2.3 固定窗口計(jì)數(shù)
我們可以以第一次請求訪問的時(shí)候開始進(jìn)行計(jì)數(shù)鹤树,而不嚴(yán)格按照自然時(shí)間來計(jì)數(shù)铣焊。比如可以利用 Redis 的 INCR 和 EXPIRE 組合進(jìn)行計(jì)數(shù),如下偽代碼所示:
count = redis.incrby(key)
ifcount == 1
redis.expire(key,3600)
ifcount >= threshold
println("exceed...")
這種實(shí)現(xiàn)方式簡單粗暴罕伯,可以解決絕大部分分布式限流的問題曲伊。但是其存在的問題是:
1.該計(jì)數(shù)方式并不是準(zhǔn)確計(jì)數(shù),由于時(shí)間窗口一旦過期追他,則之前積累的數(shù)據(jù)就失效坟募,這樣可能導(dǎo)致比如本來希望限制“一分鐘內(nèi)訪問不能超過 100 次”,但實(shí)際上做不到精準(zhǔn)的限制邑狸,會存在誤判放過本應(yīng)拒絕的流量懈糯。
2.每次請求都將訪問一次 Redis,可能存在大流量并發(fā)時(shí)候?qū)⒕彺娲虮雷罱K拖垮業(yè)務(wù)應(yīng)用的問題单雾。這個(gè)在高并發(fā)場景中是非常嚴(yán)重的問題赚哗。當(dāng)然,你可以選擇按照業(yè)務(wù)進(jìn)行適當(dāng)?shù)木彺婕呵懈顏砭徑膺@種問題硅堆,但是這仍然是治標(biāo)不治本屿储。當(dāng)然,如果你選擇單機(jī)限流的實(shí)現(xiàn)方式硬萍,則無需使用 Redis扩所,進(jìn)一步,單機(jī)限流情況下該問題不存在朴乖。
2.4 自然窗口計(jì)數(shù)
有的場景會需要以自然窗口為維度進(jìn)行限制祖屏,實(shí)現(xiàn)方式即進(jìn)行分桶計(jì)數(shù)。每個(gè) slot 一般以時(shí)間戳為 key salt买羞,以單 slot 時(shí)間長度內(nèi)的計(jì)數(shù)值為 Value袁勺,我們可以根據(jù)實(shí)際的需求對單 slot 的時(shí)間長度進(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á)到閾值舶衬,如果超過閾值則執(zhí)行對應(yīng)的限制操作埠通。
該策略如果應(yīng)用在分布式限流環(huán)境下,則會碰到若干個(gè)問題逛犹。這個(gè)后面章節(jié)中會提到端辱。另外梁剔,該策略本質(zhì)上其實(shí)是也是一種特殊的固定窗口計(jì)數(shù)策略,那么固定窗口所存在的弊端舞蔽,自然窗口計(jì)數(shù)也會存在荣病。那么我們不禁會問,如果希望規(guī)避固定窗口的一大問題——“無法準(zhǔn)確計(jì)數(shù)”的話渗柿,要怎么做呢个盆?這時(shí),“滑動窗口計(jì)數(shù)”方式應(yīng)運(yùn)而生朵栖。
2.5 滑動窗口計(jì)數(shù)
滑動窗口的出現(xiàn)砾省,可以很好地解決精準(zhǔn)計(jì)數(shù)的問題。隨著時(shí)間窗口不斷地滑動混槐,動態(tài)地進(jìn)行計(jì)數(shù)判斷⌒裕可以規(guī)避自然窗口和固定窗口計(jì)數(shù)所存在的計(jì)數(shù)不準(zhǔn)確的問題声登。以下有兩種常見的滑動窗口計(jì)數(shù)的實(shí)現(xiàn)類別。
2.5.1 基于共享分布式內(nèi)存
可以采用 Redis ZSet揣苏,存儲結(jié)構(gòu)如下圖所示悯嗓。Key 為功能 ID,Value 為 UUID卸察,Score 也記為同一時(shí)間戳脯厨。整個(gè)過程簡單概括為“添加記錄、設(shè)置失效時(shí)間坑质、計(jì)數(shù)合武、刪除過期記錄”四部分。使用 ZADD涡扼、EXPIRE稼跳、ZCOUNT 和 zremrangeScore 來實(shí)現(xiàn),并同時(shí)注意開啟 Pipeline 來盡可能提升性能吃沪。
>need-to-insert-img
偽代碼如下:
// 開啟pipe
pipeline = redis.pielined()
// 增加一條請求
pipeline.zadd(key, getUUID(), now)
// 重新設(shè)置失效時(shí)間
pipeline.expire(key, 3600)
// 統(tǒng)計(jì)在滑動窗口內(nèi)汤善,有多少次的請求
count = pipeline.zcount(key, expireTimeStamp, now)
// 刪除過期記錄
pipeline.zremrangeByScore(key, 0, expireTimeStamp - 1)
pipeline.sync()
ifcount >= threshold
println("exceed")
但是該方法,有一個(gè)比較突出的問題票彪。就是這是一個(gè)重操作红淡,將引發(fā)高 QPS 下 Redis 的性能瓶頸,也將消耗較多的資源和時(shí)間降铸。一般我們可以付出秒級的時(shí)延在旱,對其做多階段異步化的處理。比如將計(jì)數(shù)垮耳、刪除過期數(shù)據(jù)和新增記錄分為三部分去進(jìn)行異步處理颈渊。此處就不進(jìn)一步展開了遂黍。
2.5.2 基于本地內(nèi)存
第一個(gè)方案中,分布式滑動窗口的難度在于俊嗽,不得不進(jìn)行內(nèi)存共享來達(dá)到窗口計(jì)數(shù)準(zhǔn)確的目的雾家。如果考慮分發(fā)時(shí)進(jìn)行 Key Based Routing 是不是能解決這個(gè)問題?在付出非冪等绍豁、復(fù)雜度抬升等一定代價(jià)的情況下芯咧,引入基于本地內(nèi)存的分布式限流實(shí)現(xiàn)方式。
實(shí)現(xiàn)方式有如下兩種:
1.如果可以接受準(zhǔn)實(shí)時(shí)計(jì)算的話竹揍,可以采用 Storm敬飒,使用 filedsGroup,指定 Key 到對應(yīng)的 Bolt 去處理芬位;
2.如果需要實(shí)時(shí)計(jì)算的話无拗,那么就采用 RPC 框架的 LB 策略為指定 Key 的一致性 Hash。然后路由到對應(yīng)的服務(wù)實(shí)例去處理昧碉。
以上兩個(gè)實(shí)現(xiàn)方式英染,當(dāng)?shù)竭_(dá) Bolt 或者服務(wù)實(shí)例后,即可基于本地內(nèi)存進(jìn)行處理被饿,處理方式也有三種四康。
1.采用 Esper,用 DSL 語句即可簡單實(shí)現(xiàn)滑動窗口狭握。
2.Storm 1.0 之后提供了滑動窗口的實(shí)現(xiàn)闪金。
3.如果希望自實(shí)現(xiàn)滑動窗口(不推薦),實(shí)現(xiàn)思路也比較簡單即:循環(huán)隊(duì)列+自然窗口滑動計(jì)數(shù)论颅。
循環(huán)隊(duì)列來解決無限后延的時(shí)間里哎垦,計(jì)數(shù)空間重復(fù)利用的問題。而此處恃疯,我們看到了一個(gè)熟悉的名詞——“自然窗口計(jì)數(shù)”撼泛。沒錯(cuò),底層仍然采用自然窗口計(jì)數(shù)澡谭,但是區(qū)別在于愿题,我們會對自然窗口切分更細(xì)的粒度,每次批量超前獲取多個(gè)分桶蛙奖,來進(jìn)行加和計(jì)算潘酗。這樣就可以實(shí)現(xiàn)滑動窗口的效果,你可以認(rèn)為雁仲,當(dāng)分桶被細(xì)化到 10s仔夺、5s 甚至越來越細(xì)的時(shí)候,計(jì)數(shù)將趨近于更加準(zhǔn)確攒砖。
2.6 令牌桶和漏桶算法計(jì)數(shù)
令牌桶的示意圖如下:
而漏桶的示意圖如下:
這個(gè)在業(yè)內(nèi)也是鼎鼎大名缸兔∪杖梗基本談起限流算法,這兩個(gè)算法必然會被提起惰蜜,令牌桶可以有流量應(yīng)對突發(fā)流量昂拂,漏桶則強(qiáng)調(diào)對流量的整型。二者的模型是相反的抛猖。令牌桶和漏桶算法在單機(jī)限流中較為常見格侯,而在分布式限流中罕見蹤跡。
對于令牌桶來說财著,你可以采用定時(shí)任務(wù)去做投遞令牌的動作联四,也可以采用算法的方式去進(jìn)行簡單的計(jì)算。Guava Ratelimiter 采用的是后者撑教。
令牌桶的優(yōu)勢之一朝墩,在于可以有部分余量用以應(yīng)對突發(fā)流量。但是在實(shí)際生產(chǎn)環(huán)境中伟姐,這不一定是安全的鱼辙。如果我們的服務(wù)沒有做好應(yīng)對更高突發(fā)流量的準(zhǔn)備,那么很有可能會引發(fā)服務(wù)雪崩玫镐。所以考慮到這一點(diǎn),Guava 采用了令牌桶 + 漏桶結(jié)合的策略來進(jìn)行限流怠噪。對于默認(rèn)業(yè)務(wù)恐似,采用標(biāo)準(zhǔn)令牌桶方式進(jìn)行“可超支”限速,而對于無法突然應(yīng)對高峰流量的業(yè)務(wù)傍念,會采用緩慢提升投放令牌速率(即逐步縮短業(yè)務(wù)請求等待時(shí)間)的方式來進(jìn)行熱啟動控制矫夷,具體見 Guava Ratelimiter 源碼注釋描述,此處不贅述憋槐,其效果如下圖所示:
3. 微服務(wù)限流幾個(gè)考慮的點(diǎn)
以上的限流手段双藕,有的能應(yīng)用在單機(jī)環(huán)境,有的能應(yīng)用在分布式環(huán)境阳仔。而在高并發(fā)的分布式環(huán)境中忧陪,我們需要考慮清楚如下幾個(gè)問題如何解決。
3.1 機(jī)器時(shí)鐘不一致或者時(shí)鐘回退問題
一旦出現(xiàn)這種問題近范,則可能導(dǎo)致收集的數(shù)據(jù)相互污染而導(dǎo)致判斷出錯(cuò)嘶摊。所以一方面,在運(yùn)維層面需要確保機(jī)器時(shí)鐘能夠按期同步评矩。另一方面叶堆,需要有準(zhǔn)實(shí)時(shí)檢測的手段,及時(shí)發(fā)現(xiàn)時(shí)鐘偏差太大或者時(shí)鐘回退的機(jī)器斥杜,基于一定策略篩選出不合格的數(shù)據(jù)來源虱颗,將其刨除出計(jì)算范圍并發(fā)出警告沥匈。
3.2 在 SDK 還是 Server 端做限流邏輯
你需要考慮你的限流策略迭代的頻繁程度,推動業(yè)務(wù)方改造的成本忘渔,語言/技術(shù)棧異構(gòu)情況高帖,是否有需要進(jìn)行立多系統(tǒng)聯(lián)合限流的場景,以此來進(jìn)行決策辨萍。如果采用 SDK 方式棋恼,你需要做好碰到這幾個(gè)棘手問題的心理準(zhǔn)備。
而如果采用 Server 方式锈玉,你則需要更多考慮高并發(fā)下數(shù)據(jù)堆積爪飘,機(jī)器資源消耗,以及對業(yè)務(wù)方性能的影響問題拉背。一般業(yè)內(nèi)采用的是富 SDK 的方式來做师崎,但是對于上述的 SDK 會面臨的幾個(gè)問題沒有很好的解決方案。而 ServiceMesh 領(lǐng)軍人物 Istio 采用了 Mixer 來實(shí)現(xiàn) Server 端限流的方式椅棺,但是碰到了很嚴(yán)重的性能問題犁罩。所以這是一個(gè)很困難的選擇。
回顧下架構(gòu)師成長之路之服務(wù)治理漫談一篇中所講到的服務(wù)治理發(fā)展路徑两疚,是不是有點(diǎn)驚人的相似床估?是不是也許限流的未來,不在 SDK 也不在 Server诱渤,而在于 ServiceMesh丐巫?我不確定,但我覺得這是一個(gè)很好的探索方向勺美。
3.3 限流是不是會讓你的系統(tǒng)變得不可控
這是一個(gè)很有意思的問題递胧,限流本身是為了“反脆弱”而存在的,但是如果你的分布式復(fù)雜拓?fù)渲斜椴枷蘖鞴δ苌娜祝敲匆院竽忝總€(gè)服務(wù)的擴(kuò)容缎脾,新的功能上線,拓?fù)浣Y(jié)構(gòu)的變更占卧,都有可能會導(dǎo)致局部服務(wù)流量的驟增遗菠,進(jìn)一步引發(fā)限流導(dǎo)致業(yè)務(wù)有損問題。這就是“反脆弱”的本身也有可能會導(dǎo)致“脆弱”的出現(xiàn)华蜒。所以舷蒲,當(dāng)你進(jìn)行大規(guī)模限流能力擴(kuò)張覆蓋的時(shí)候,需要謹(jǐn)慎審視你的限流能力和成熟度是否能夠支撐起如此大規(guī)模的應(yīng)用友多。
3.4 拓?fù)涞年P(guān)聯(lián)性能給限流帶來什么
我們置身于復(fù)雜服務(wù)拓?fù)浜透鞣N調(diào)用鏈路中牲平,這一方面確實(shí)給限流帶來了很大的麻煩,但另一方面域滥,我們是不是可以思考一下纵柿,這些復(fù)雜度蜈抓,本身是不是可以帶給我們什么樣的利好?比如:底層服務(wù)扛不住昂儒,那么是不是可以在更上層的調(diào)用方入口進(jìn)行限流沟使?如此是不是可以給予用戶更友好提示的同時(shí),也可避免鏈路上服務(wù)各自限流后帶來的系統(tǒng)級聯(lián)處理壓力渊跋?微服務(wù)的本質(zhì)是自治沒錯(cuò)腊嗡,但是我們是不是可以更好地對各個(gè)服務(wù)的限流自治能力進(jìn)行編排,以達(dá)到效率拾酝、體驗(yàn)燕少、資源利用的優(yōu)化?
相信大家都會有自己的答案蒿囤。這件事情本身的難度是在于決策的準(zhǔn)確性客们,但如果能很好地進(jìn)行落地實(shí)現(xiàn),則意味著我們的限流從自動化已經(jīng)逐步轉(zhuǎn)向了智能化材诽。這也將是更高一層次的挑戰(zhàn)和機(jī)遇底挫。
3.5 準(zhǔn)確性和實(shí)時(shí)性的權(quán)衡
在高并發(fā)限流場景下,準(zhǔn)確性和實(shí)時(shí)性理論上不可兼得脸侥。在特定的場景中建邓,你需要作出你的選擇,比如前文介紹的基于 Redis ZSet 實(shí)現(xiàn)的滑動窗口實(shí)時(shí)計(jì)算方式可以滿足實(shí)時(shí)性和準(zhǔn)確性睁枕,但其會帶來很明顯的性能問題官边。所以我們需要作出我們的權(quán)衡,比如犧牲準(zhǔn)確性將滑動窗口退化為固定窗口來保障性能譬重;或者犧牲實(shí)時(shí)性,對滑動窗口多階段去做異步化罐氨,分析和決策兩階段分離臀规,來保障性能。這取決于你的判斷栅隐。
4. 總結(jié)
限流是高可用治理中核心的一環(huán)塔嬉,實(shí)現(xiàn)方式也五花八門,每種方式也都有各自的問題租悄,本文只是做了一個(gè)簡單的回顧谨究。希望隨著 ServiceMesh、AIOps 等理論的興起泣棋,我們對于限流是什么胶哲,能做什么,怎么實(shí)現(xiàn)潭辈,能夠釋放出更大的空間去想象鸯屿。