[TOC]
最常用的限流算法以及如何在http中間件中加入流控
何為限流?
通過(guò)對(duì)并發(fā)訪問(wèn)/請(qǐng)求進(jìn)行限速,或者對(duì)一個(gè)時(shí)間窗口內(nèi)的請(qǐng)求進(jìn)行限速來(lái)保護(hù)系統(tǒng),一旦達(dá)到限制速率則可以拒絕服務(wù)、排隊(duì)或等待、降級(jí)等處理
說(shuō)白了就是限制請(qǐng)求數(shù)量,或者是在某一段時(shí)間內(nèi)限制總的請(qǐng)求數(shù)量
例如秒殺網(wǎng)站,限制22點(diǎn)5分 -- 22點(diǎn)10分 秒殺999份產(chǎn)品, 限制放行 5w 個(gè)請(qǐng)求,若在該段時(shí)間內(nèi),請(qǐng)求在第5w以后的請(qǐng)求,直接拒之門外, 也就是我們?cè)谶M(jìn)入網(wǎng)站的時(shí)候顯示,系統(tǒng)繁忙
為什么要限流?
- 后臺(tái)服務(wù)能力有限,需要限流,否則服務(wù)會(huì)崩掉
- 可以根據(jù)測(cè)試性能去評(píng)估限流的設(shè)置,例如測(cè)試最大連接數(shù),qps數(shù)量(每秒鐘能處理的最大請(qǐng)求數(shù))
- 防止爬蟲、惡意攻擊
例如當(dāng)系統(tǒng)的訪問(wèn)量突然劇增,大量的請(qǐng)求涌入過(guò)來(lái),我們可能會(huì)知道會(huì)突然有一波高峰,這個(gè)時(shí)候如果服務(wù)器承受不了壓力的話,就會(huì)崩潰,例如如下幾類業(yè)務(wù)
- 秒殺業(yè)務(wù)
- 各種刷單
- 微博上的熱搜
- 因?yàn)槟承┰蛴脩裘驮?,太過(guò)熱情
- 大量用戶(可以是惡意的,也可以是正常的用戶量請(qǐng)求過(guò)多)高頻訪問(wèn)服務(wù)器,服務(wù)器承受能力不足
- 網(wǎng)頁(yè)爬蟲 等等
限流一般是如何去實(shí)現(xiàn)的?
我們?cè)谀硨毣蚰硸|的熱門節(jié)日上剁手,付款的時(shí)候,還急我們懷著焦灼的心等待著排隊(duì)的人數(shù)一個(gè)一個(gè)下降的時(shí)候嗎?
我們?cè)诏偪駬屬?gòu)商品,由于點(diǎn)擊太快,熱情太高,導(dǎo)致多次彈出系統(tǒng)繁忙,請(qǐng)稍后再試,還記得嗎?
更有甚者,在流量過(guò)大的時(shí)候,直接提示拒絕訪問(wèn)的,這些是不是都一一浮現(xiàn)在腦海呢?
根據(jù)如上場(chǎng)景,我們的限流思路會(huì)是這個(gè)樣子的:
- 拒絕請(qǐng)求
- 設(shè)置排隊(duì),直到單位請(qǐng)求數(shù)趨于正常水平
關(guān)于拒絕請(qǐng)求就相對(duì)簡(jiǎn)單粗暴,對(duì)于設(shè)置排隊(duì)就會(huì)有多種排隊(duì)方式了,咱們繼續(xù)聊
除了限流還有什么方式可以解決或者緩解這種突然大量請(qǐng)求的情況呢?
還有熔斷,降級(jí),都可以有效的解決這樣的問(wèn)題
那啥是降級(jí)?
即服務(wù)降級(jí),當(dāng)我們的服務(wù)器壓力劇增時(shí),為了保證核心模塊的高可用,這里指的是我們自身的系統(tǒng)出現(xiàn)了故障而降級(jí),有如下2個(gè)**常用的解決方式
- 降低非核心模塊的性能
- 直接關(guān)閉不重要的功能,為保障核心模塊的功能正常
如圖,某網(wǎng)站,當(dāng)用戶請(qǐng)求數(shù)猛增,服務(wù)器吃不消的時(shí)候,就可以選擇把評(píng)論功能,修改密碼等功能關(guān)閉,確保支付系統(tǒng),數(shù)據(jù)系統(tǒng)等核心功能能夠正常運(yùn)行
哦?那熔斷是啥?
與服務(wù)降級(jí)還是有區(qū)別的,這里指的是指依賴的外部接口出現(xiàn)故障的情況下,會(huì)設(shè)置斷絕和外部接口的關(guān)系。
服務(wù)器A依賴于服務(wù)器B的對(duì)外接口,在某個(gè)時(shí)刻服務(wù)器B的接口出現(xiàn)異常,響應(yīng)時(shí)間極其的慢,可是此接口會(huì)影響到服務(wù)器的整個(gè)運(yùn)作,那么這個(gè)時(shí)候,服務(wù)器A就可以在請(qǐng)求服務(wù)器B該接口的時(shí)候,默認(rèn)設(shè)置返回錯(cuò)誤
最常用的限流算法
我們來(lái)分享一個(gè)最常用的限流算法,大致分為以下 4 種
- 固定窗口計(jì)數(shù)器
- 滑動(dòng)窗口計(jì)數(shù)器
- 漏桶
- 令牌桶
固定時(shí)間窗口控制
最簡(jiǎn)單的是 使用計(jì)數(shù)器來(lái)控制,設(shè)置固定的時(shí)間內(nèi),處理固定的請(qǐng)求數(shù)
上述圖,固定時(shí)間窗口來(lái)做限制,1 s只能處理2個(gè)請(qǐng)求,紅色請(qǐng)求則會(huì)被直接丟棄
- 固定每1秒限制同時(shí)請(qǐng)求數(shù)為2
- 上述紅色部分的請(qǐng)求會(huì)被扔掉,扔掉之后 整個(gè)服務(wù)負(fù)荷可能會(huì)降低
- 但是這個(gè)會(huì)丟掉請(qǐng)求,對(duì)于體驗(yàn)不好
滑動(dòng)窗口計(jì)數(shù)器算法
能夠去平滑一下處理的任務(wù)數(shù)量?;瑒?dòng)窗口計(jì)數(shù)器是通過(guò)將窗口再細(xì)分,并且按照時(shí)間滑動(dòng),這種算法避免了固定窗口算法帶來(lái)的雙倍突發(fā)請(qǐng)求,但時(shí)間區(qū)間精度越高,算法所需的空間容量越大
- 將時(shí)間劃分為多個(gè)區(qū)間
- 在每個(gè)區(qū)間內(nèi)每有一次請(qǐng)求就講計(jì)數(shù)器加1維持一個(gè)時(shí)間窗口,占據(jù)多個(gè)區(qū)間
- 每經(jīng)過(guò)一個(gè)區(qū)間的時(shí)間,則拋棄最老的一個(gè)區(qū)間,并納入最新的一個(gè)區(qū)間
- 若當(dāng)前的窗口內(nèi)區(qū)間的請(qǐng)求總數(shù)和超過(guò)了限制數(shù)量,則本窗口內(nèi)的請(qǐng)求都被丟棄
漏桶
為了解決上述紅色部分丟掉的問(wèn)題,引入了 漏桶的方式進(jìn)行限流,漏桶是有緩存的,有請(qǐng)求就會(huì)放到緩存中
漏桶,聽起來(lái)有點(diǎn)像漏斗的樣子,也是一滴一滴的滴下去的
如圖,水滴即為請(qǐng)求的事件,如果漏桶可以緩存5000個(gè)事件,實(shí)際服務(wù)器1s處理1000個(gè)事件,那么在高峰期的時(shí)候,響應(yīng)時(shí)間最多等5秒,但是不能一直是高峰期,否則,一直響應(yīng)時(shí)間都是5s,就會(huì)是很慢的時(shí)間了,這個(gè)時(shí)間也是很影響體驗(yàn)的
如果桶滿了,還有請(qǐng)求過(guò)來(lái)的話,則會(huì)被直接丟棄,這種做法,還是丟棄了請(qǐng)求
- 將每個(gè)請(qǐng)求看成 水滴, 放入水滴 進(jìn)行存儲(chǔ)
- 漏桶以固定的速率往外漏水,若桶空了則停止漏水。比如說(shuō),1s 漏 1000滴水,正如1s 處理1000個(gè)請(qǐng)求
- 如果漏桶慢了,則多余的水滴也會(huì)被直接舍棄
優(yōu)勢(shì)
- 有一定的緩存能力,比上述2種方式會(huì)好一些
劣勢(shì)
- 桶滿的時(shí)候若有新請(qǐng)求,仍然會(huì)丟掉數(shù)據(jù)
- 長(zhǎng)時(shí)間桶滿,則會(huì)影響響應(yīng)速率,這個(gè)根據(jù)桶的大小來(lái)定體驗(yàn)是否ok
令牌桶
通過(guò)動(dòng)態(tài)控制令牌的數(shù)量,來(lái)更好的服務(wù)客戶端的請(qǐng)求事情,令牌的生成數(shù)量和生產(chǎn)速率都是可以靈活控制的
如上,令牌桶和漏桶不同的地方在于
- 令牌桶可以自己控制生成令牌的速率,例如高峰期就可以多生成一些令牌來(lái)滿足客戶端的需求
- 還可以緩存數(shù)據(jù)
若發(fā)現(xiàn)一直是出于高峰期,可以考慮擴(kuò)大令牌桶
優(yōu)勢(shì)
- 令牌桶可以動(dòng)態(tài)的自己控制生成令牌的速率
- 還可以緩存數(shù)據(jù)
如何在http middleware加入流控
如何在http 中間件中加入流控呢,目的是限流,每一個(gè)請(qǐng)求,都需要經(jīng)過(guò)這個(gè)中間件,才有機(jī)會(huì)向后走,才有機(jī)會(huì)被處理
type middleWareHandler struct {
r *httprouter.Router
l *ConnLimiter
}
func NewMiddleWareHandler(r *httprouter.Router, cc int) http.Handler {
m := middleWareHandler{}
m.r = r
m.l = NewConnLimiter(cc) // 限制數(shù)量
return m
}
說(shuō)完令牌桶,我們來(lái)說(shuō)說(shuō)限流器
限流器
限流器是后臺(tái)服務(wù)中的非常重要的組件
它可以用做啥呢?
- 限制請(qǐng)求速率
- 保護(hù)服務(wù)
限流器的實(shí)現(xiàn)方法有很多種,基本上都是基于上述的限流算法來(lái)實(shí)現(xiàn)的
- 滑動(dòng)窗口法
- Token Bucket(令牌桶)
- Leaky Bucket(漏桶)
golang標(biāo)準(zhǔn)庫(kù)中就自帶了限流算法的實(shí)現(xiàn),不需要我們自己造輪子
golang.org/x/time/rate,直接用就好了,該限流器是基于Token Bucket(令牌桶)實(shí)現(xiàn)的
令牌桶就是我們上面說(shuō)的桶,里面裝令牌,系統(tǒng)會(huì)以恒定速率向桶中放令牌
桶滿則暫時(shí)不放。 用戶請(qǐng)求就要向桶里面拿令牌
如果有剩余Token就可以一直取
如果沒有剩余令牌,則需要等到系統(tǒng)中被放置了Token才可以往下進(jìn)行
我們來(lái)看看限流器咋用
構(gòu)造一個(gè)限流對(duì)象
limiter := NewLimiter(5, 1);
- 第一個(gè)參數(shù)是r Limit,這是代表每秒可以向令牌桶中產(chǎn)生多少令牌
- 第二個(gè)參數(shù)是b int,這是代表令牌桶的容量大小
也就是說(shuō),其構(gòu)造出的限流器是
- 令牌桶大小為1
- 以每秒5個(gè)令牌的速率向桶中放置令牌
我們當(dāng)然也可以使用另外的設(shè)置方式,包中也有提供
limit := Every(500 * time.Millisecond);
limiter := NewLimiter(limit, 1);
可以用Every方法來(lái)指定向Token桶中放置令牌的間隔,上面代碼就表示每500 ms往桶中放一個(gè)令牌
也就說(shuō),上述代碼是1 秒鐘,產(chǎn)生2個(gè)令牌
Limiter是支持可以調(diào)整速率和桶大小的,我們來(lái)看看源碼
// 改變放入令牌的速率
SetLimit(Limit)
// 改變令牌桶大小
SetBurst(int)
我們來(lái)寫一個(gè)案例看看:
package main
import (
"context"
"log"
"time"
"golang.org/x/time/rate"
)
func main() {
l := rate.NewLimiter(1, 2)
// limit表示每秒產(chǎn)生token數(shù),buret最多存令牌數(shù)
log.Println(l.Limit(), l.Burst())
for i := 0; i < 50; i++ {
//這里是阻塞等待的,一直等到取到一個(gè)令牌為止
log.Println("... ... Wait")
c, _ := context.WithTimeout(context.Background(), time.Second*2)
// Wait阻塞等待
if err := l.Wait(c); err != nil {
log.Println("limiter wait error : " + err.Error())
}
log.Println("Wait ... ... ")
// Reserve返回等待時(shí)間,再去取令牌
// 返回需要等待多久才有新的令牌, 這樣就可以等待指定時(shí)間執(zhí)行任務(wù)
r := l.Reserve()
log.Println("reserve time :", r.Delay())
//判斷當(dāng)前是否可以取到令牌
// Allow判斷當(dāng)前是否可以取到令牌
a := l.Allow()
log.Println("Allow == ", a)
}
}
看到個(gè)數(shù)的案例,我們可以看到,包里面提供給我們使用的消費(fèi)方法有3種
- Wait
Wait , 等于 WaitN(ctx,1)
若此時(shí)桶內(nèi)令牌數(shù)組不足(小于N),那么Wait方法將會(huì)阻塞一段時(shí)間,直至令牌滿足條件,否則就一直阻塞
若滿足條件,則直接返回結(jié)果
Wait的context參數(shù)。 可以設(shè)置超時(shí)時(shí)間
- Allow
看看函數(shù)原型
func (lim *Limiter) Allow() bool
func (lim *Limiter) AllowN(now time.Time, n int) bool
Allow 等于 AllowN(time.Now(),1), 當(dāng)前取一個(gè)令牌,若滿足,則為true,否則 false
AllowN方法 指的是,截止到某一時(shí)刻,目前桶中令牌數(shù)目是否至少為N個(gè),滿足則返回true,同時(shí)從桶中消費(fèi)N個(gè)令牌。 反之返回不消費(fèi)令牌,返回false
- Reserve
Reserve , 等于 ReserveN(time.Now(), 1)
ReserveN 當(dāng)調(diào)用完成后,無(wú)論令牌是否充足,都會(huì)返回一個(gè)Reservation*對(duì)象
我們可以調(diào)用該對(duì)象的Delay()方法,有如下注意:
該方法返回了需要等待的時(shí)間
- 如果等待時(shí)間為0秒,則說(shuō)明不用等待
- 若大于0秒,則必須等到等待時(shí)間之后,才能向后進(jìn)行
當(dāng)然,若不想等待,你可以歸還令牌,一個(gè)都不能少,調(diào)用該對(duì)象的Cancel() 方法即可
總結(jié)
- 簡(jiǎn)單介紹了限流,熔斷,服務(wù)降級(jí)
- 形象分享了限流的4種算法
- 介紹了http 中間件接入流控的簡(jiǎn)單寫法
- 分享了go
golang.org/x/time/rate中,限流器的基本使用
好了,本次就到這里,下一次 互聯(lián)網(wǎng)協(xié)議介紹和分享,
技術(shù)是開放的,我們的心態(tài),更應(yīng)是開放的。擁抱變化,向陽(yáng)而生,努力向前行。
我是小魔童哪吒,歡迎點(diǎn)贊關(guān)注收藏,下次見~