通過限制并發(fā)訪問數(shù)或者限制一個(gè)時(shí)間窗口內(nèi)允許處理的請求數(shù)量來保護(hù)系統(tǒng),例如,通過限流,你可以過濾掉產(chǎn)生流量峰值的客戶和服務(wù)。
成都創(chuàng)新互聯(lián)公司成立十多年來,這條路我們正越走越好,積累了技術(shù)與客戶資源,形成了良好的口碑。為客戶提供成都網(wǎng)站制作、成都網(wǎng)站建設(shè)、外貿(mào)營銷網(wǎng)站建設(shè)、網(wǎng)站策劃、網(wǎng)頁設(shè)計(jì)、域名注冊、網(wǎng)絡(luò)營銷、VI設(shè)計(jì)、網(wǎng)站改版、漏洞修補(bǔ)等服務(wù)。網(wǎng)站是否美觀、功能強(qiáng)大、用戶體驗(yàn)好、性價(jià)比高、打開快等等,這些對于網(wǎng)站建設(shè)都非常重要,成都創(chuàng)新互聯(lián)公司通過對建站技術(shù)性的掌握、對創(chuàng)意設(shè)計(jì)的研究為客戶提供一站式互聯(lián)網(wǎng)解決方案,攜手廣大客戶,共同發(fā)展進(jìn)步。
令牌桶算法是常見的一種限流算法。假設(shè)有一個(gè)桶,以固定速度(rate)往桶里加入令牌(token)。當(dāng)桶滿了時(shí)停止加入。服務(wù)收到請求時(shí)嘗試從桶里取出令牌。如果成功取出,則可以響應(yīng)本次請求。如果沒有取到,可以進(jìn)行有限時(shí)間的等待或者返回超時(shí)錯(cuò)誤。
遇到流量洪峰時(shí)可以應(yīng)對部分突發(fā)流量,但由于桶有容量上限,當(dāng)消耗完桶里堆積的令牌之后只能根據(jù)令牌的生成速率提供服務(wù),從而起到限流的作用。
Golang rate包提供了 token limiter 的實(shí)現(xiàn),具體可以點(diǎn)擊鏈接查看rate package
一個(gè)固定容量的漏桶,按照常量固定速率流出水滴,這里的水滴指的就是能進(jìn)行響應(yīng)的請求。當(dāng)漏桶滿了時(shí),請求就會被丟棄,返回一個(gè)限流標(biāo)志。
流量均勻,一般作為計(jì)量工具,可以用于流量整形和流量控制。比方說對數(shù)據(jù)庫的操作。經(jīng)過一層漏桶控制,可以有效控制對數(shù)據(jù)庫的請求,避免數(shù)據(jù)庫被打掛。流量穩(wěn)定,但是無法應(yīng)對突發(fā)流量。
uber 開源了一個(gè)基于漏桶的限流器ratelimit
func main() {
rl := ratelimit.New(1) // per second
for i := 0; i < 10; i++ {
now := rl.Take()
if i > 0 {
fmt.Println(i, now)
}
}
}
1 2022-03-24 02:24:51.
2 2022-03-24 02:24:52.
3 2022-03-24 02:24:53.
4 2022-03-24 02:24:54.
5 2022-03-24 02:24:55.
6 2022-03-24 02:24:56.
7 2022-03-24 02:24:57.
8 2022-03-24 02:24:58.
9 2022-03-24 02:24:59.
可以看到,通過“漏桶”這層的過濾,可以有效保護(hù)我們的服務(wù)。
// WithoutSlack configures the limiter to be strict and not to accumulate
// previously "unspent" requests for future bursts of traffic.
var WithoutSlack Option = slackOption(0)
// WithSlack configures custom slack.
// Slack allows the limiter to accumulate "unspent" requests
// for future bursts of traffic.
func WithSlack(slack int) Option {
return slackOption(slack)
}
可以簡單對比一下
rl := ratelimit.New(1, ratelimit.WithoutSlack) // per second
for i := 0; i < 10; i++ {
now := rl.Take()
if i == 2 {
time.Sleep(2 * time.Second)
}
if i > 0 {
fmt.Println(i, now)
}
}
1 2022-03-24 02:34:22.
2 2022-03-24 02:34:23. //sleep 2 秒,后面 rps 還是很平穩(wěn)
3 2022-03-24 02:34:25.
4 2022-03-24 02:34:26.
5 2022-03-24 02:34:27.
6 2022-03-24 02:34:28.
7 2022-03-24 02:34:29.
8 2022-03-24 02:34:30.
9 2022-03-24 02:34:31.
rl := ratelimit.New(1, ratelimit.WithSlack(5)) // per second
for i := 0; i < 10; i++ {
now := rl.Take()
if i == 2 {
time.Sleep(5 * time.Second)
}
if i > 0 {
fmt.Println(i, now)
}
}
1 2022-03-24 02:39:58.
2 2022-03-24 02:39:59. //sleep 5 秒,這里的例子比較夸張,不過可看到我們可以一次性處理 5 個(gè)
3 2022-03-24 02:40:04.
4 2022-03-24 02:40:04.
5 2022-03-24 02:40:04.
6 2022-03-24 02:40:04.
7 2022-03-24 02:40:04.
8 2022-03-24 02:40:05.
9 2022-03-24 02:40:06.
我們?nèi)〉每梢蕴幚淼恼埱蠛筮€應(yīng)該結(jié)合 context 上下午中的上下文時(shí)間,避免拿到請求后處理請求超時(shí)。
滑動(dòng)窗口算法是對普通時(shí)間窗口計(jì)數(shù)的優(yōu)化,我們知道普通時(shí)間窗口計(jì)數(shù)存在精度的不足,比如說我們服務(wù)1秒可以處理1000個(gè)請求,所以這里我們限制1s處理的請求數(shù)為1000。前1秒后500ms 來了600個(gè)請求,后一秒前400ms 來了600個(gè)請求,那么在這 900ms的間隔里就來了1200 個(gè)請求。主要的原因就在于普通時(shí)間窗口計(jì)數(shù)每間隔 1 s 會刷新,所以滑動(dòng)窗口將間隔時(shí)間劃分為多個(gè)區(qū)間,從設(shè)計(jì)上優(yōu)化了精度問題。
type slot struct {
startTime time.Time //slot 的起始時(shí)間
count int //數(shù)量
}
type slots []*slot
type window slots //窗口
func sumReq(win window) int { //計(jì)數(shù)
count := 0
for _, slot := range win {
count += slot.count
}
return count
}
type RollingWindow struct {
slotLength time.Duration //slot 的長度
slotNum int //slot 個(gè)數(shù)
winLenght time.Duration //窗口長度
maxReqNum int //rolling window 內(nèi)允許的最大請求書
win window //窗口
lock sync.Mutex //鎖
}
func NewRollingWindow(slotLength time.Duration, slotNum int, maxReqNum int) *RollingWindow {
return &RollingWindow{
slotLength: slotLength,
slotNum: slotNum,
winLenght: slotLength * time.Duration(slotNum),
maxReqNum: maxReqNum,
win: make(window, 0, slotNum),
lock: sync.Mutex{},
}
}
func (rw *RollingWindow) IsLimit() bool {
return !rw.validate()
}
func (rw *RollingWindow) validate() bool {
now := time.Now()
rw.lock.Lock()
defer rw.lock.Unlock()
//滑動(dòng)窗口
rw = rw.slideRight(now)
//是否超限
can := rw.accept()
if can {
//記錄請求數(shù)
rw.update(now)
}
return can
}
//向右移動(dòng)窗口 [0,1,2,3,4,5] -> [1,2,3,4,5]
func (rw *RollingWindow) slideRight(now time.Time) *RollingWindow {
offset := -1
for i, slot := range rw.win {
if slot.startTime.Add(rw.winLenght).After(now) {
break //不需要滑動(dòng)
}
offset = i
}
if offset > -1 {
rw.win = rw.win[offset+1:]
}
return rw
}
//判斷請求是否超限 沒有超限返回 true
func (rw *RollingWindow) accept() bool {
return sumReq(rw.win) < rw.maxReqNum
}
func (rw *RollingWindow) update(now time.Time) *RollingWindow {
if len(rw.win) > 0 {
lastOffset := len(rw.win) - 1
lastSlot := rw.win[lastOffset]
if lastSlot.startTime.Add(rw.slotLength).Before(now) {
//填入新的 slot
newSlot := &slot{startTime: now, count: 1}
rw.win = append(rw.win, newSlot)
} else {
rw.win[lastOffset].count++
}
} else {
newSlot := &slot{startTime: now, count: 1}
rw.win = append(rw.win, newSlot)
}
return rw
}
func main() {
l := NewRollingWindow(100*time.Millisecond, 10, 10)
fakeDealReq(l, 3)
printRollingWindow(l)
time.Sleep(200 * time.Millisecond)
fakeDealReq(l, 3)
printRollingWindow(l)
time.Sleep(200 * time.Millisecond)
fakeDealReq(l, 5)
printRollingWindow(l)
time.Sleep(1 * time.Second)
fakeDealReq(l, 1)
printRollingWindow(l)
}
func fakeDealReq(l *RollingWindow, num int) {
for i := 0; i < num; i++ {
fmt.Println(l.IsLimit())
}
}
func printRollingWindow(l *RollingWindow) {
for _, v := range l.win {
fmt.Println(v.startTime, v.count)
}
}
//輸出
false
false
false
2022-03-24 05:06:04. +0000 UTC m=+0.0000 3
false
false
false
2022-03-24 05:06:04. +0000 UTC m=+0.0000 3
2022-03-24 05:06:04. +0000 UTC m=+0. 3
false
false
false
false
true
2022-03-24 05:06:04. +0000 UTC m=+0.0000 3
2022-03-24 05:06:04. +0000 UTC m=+0. 3
2022-03-24 05:06:05.0 +0000 UTC m=+0. 4
false
2022-03-24 05:06:06.0 +0000 UTC m=+1. 1