常见的限流算法

通过限制并发访问数或者限制一个时间窗口内允许处理的请求数量来保护系统,例如,通过限流,你可以过滤掉产生流量峰值的客户和服务。

令牌桶算法

令牌桶算法是常见的一种限流算法。假设有一个桶,以固定速度(rate)往桶里加入令牌(token)。当桶满了时停止加入。服务收到请求时尝试从桶里取出令牌。如果成功取出,则可以响应本次请求。如果没有取到,可以进行有限时间的等待或者返回超时错误。

遇到流量洪峰时可以应对部分突发流量,但由于桶有容量上限,当消耗完桶里堆积的令牌之后只能根据令牌的生成速率提供服务,从而起到限流的作用。

漏桶算法

一个固定容量的漏桶,按照常量固定速率流出水滴,这里的水滴指的就是能进行响应的请求。当漏桶满了时,请求就会被丢弃,返回一个限流标志。

流量均匀,一般作为计量工具,可以用于流量整形和流量控制。比方说对数据库的操作。经过一层漏桶控制,可以有效控制对数据库的请求,避免数据库被打挂。流量稳定,但是无法应对突发流量。

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.57952663
2 2022-03-24 02:24:52.579526624
3 2022-03-24 02:24:53.579526623
4 2022-03-24 02:24:54.579526617
5 2022-03-24 02:24:55.579526616
6 2022-03-24 02:24:56.579526617
7 2022-03-24 02:24:57.579526616
8 2022-03-24 02:24:58.579526615
9 2022-03-24 02:24:59.579526629

可以看到,通过”漏桶”这层的过滤,可以有效保护我们的服务。

// 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.547745401
2 2022-03-24 02:34:23.54774539   //sleep 2 &#x79D2;&#xFF0C;&#x540E;&#x9762; rps &#x8FD8;&#x662F;&#x5F88;&#x5E73;&#x7A33;
3 2022-03-24 02:34:25.549647721
4 2022-03-24 02:34:26.549647738
5 2022-03-24 02:34:27.549647312
6 2022-03-24 02:34:28.549647722
7 2022-03-24 02:34:29.549647716
8 2022-03-24 02:34:30.549647722
9 2022-03-24 02:34:31.549647599

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.860218897
2 2022-03-24 02:39:59.860218892  //sleep 5 &#x79D2;&#xFF0C;&#x8FD9;&#x91CC;&#x7684;&#x4F8B;&#x5B50;&#x6BD4;&#x8F83;&#x5938;&#x5F20;&#xFF0C;&#x4E0D;&#x8FC7;&#x53EF;&#x770B;&#x5230;&#x6211;&#x4EEC;&#x53EF;&#x4EE5;&#x4E00;&#x6B21;&#x6027;&#x5904;&#x7406; 5 &#x4E2A;
3 2022-03-24 02:40:04.865851924
4 2022-03-24 02:40:04.865855167
5 2022-03-24 02:40:04.86585706
6 2022-03-24 02:40:04.865858894
7 2022-03-24 02:40:04.865860533
8 2022-03-24 02:40:05.860218893
9 2022-03-24 02:40:06.860218883

我们取得可以处理的请求后还应该结合 context 上下午中的上下文时间,避免拿到请求后处理请求超时。

滑动窗口算法

滑动窗口算法是对普通时间窗口计数的优化,我们知道普通时间窗口计数存在精度的不足,比如说我们服务1秒可以处理1000个请求,所以这里我们限制1s处理的请求数为1000。前1秒后500ms 来了600个请求,后一秒前400ms 来了600个请求,那么在这 900ms的间隔里就来了1200 个请求。主要的原因就在于普通时间窗口计数每间隔 1 s 会刷新,所以滑动窗口将间隔时间划分为多个区间,从设计上优化了精度问题。

type slot struct {
    startTime time.Time //slot &#x7684;&#x8D77;&#x59CB;&#x65F6;&#x95F4;
    count     int       //&#x6570;&#x91CF;
}

type slots []*slot

type window slots //&#x7A97;&#x53E3;

func sumReq(win window) int { //&#x8BA1;&#x6570;
    count := 0
    for _, slot := range win {
        count += slot.count
    }
    return count
}

type RollingWindow struct {
    slotLength time.Duration //slot &#x7684;&#x957F;&#x5EA6;
    slotNum    int           //slot &#x4E2A;&#x6570;
    winLenght  time.Duration //&#x7A97;&#x53E3;&#x957F;&#x5EA6;
    maxReqNum  int           //rolling window &#x5185;&#x5141;&#x8BB8;&#x7684;&#x6700;&#x5927;&#x8BF7;&#x6C42;&#x4E66;
    win        window        //&#x7A97;&#x53E3;
    lock       sync.Mutex    //&#x9501;
}

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()
    //&#x6ED1;&#x52A8;&#x7A97;&#x53E3;
    rw = rw.slideRight(now)
    //&#x662F;&#x5426;&#x8D85;&#x9650;
    can := rw.accept()
    if can {
        //&#x8BB0;&#x5F55;&#x8BF7;&#x6C42;&#x6570;
        rw.update(now)
    }

    return can
}

//&#x5411;&#x53F3;&#x79FB;&#x52A8;&#x7A97;&#x53E3; [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 //&#x4E0D;&#x9700;&#x8981;&#x6ED1;&#x52A8;
        }
        offset = i
    }
    if offset > -1 {
        rw.win = rw.win[offset+1:]
    }
    return rw
}

//&#x5224;&#x65AD;&#x8BF7;&#x6C42;&#x662F;&#x5426;&#x8D85;&#x9650; &#x6CA1;&#x6709;&#x8D85;&#x9650;&#x8FD4;&#x56DE; 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) {
            //&#x586B;&#x5165;&#x65B0;&#x7684; 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)
    }
}

//&#x8F93;&#x51FA;
false
false
false
2022-03-24 05:06:04.616315628 +0000 UTC m=+0.000036182 3
false
false
false
2022-03-24 05:06:04.616315628 +0000 UTC m=+0.000036182 3
2022-03-24 05:06:04.817533239 +0000 UTC m=+0.201253793 3
false
false
false
false
true
2022-03-24 05:06:04.616315628 +0000 UTC m=+0.000036182 3
2022-03-24 05:06:04.817533239 +0000 UTC m=+0.201253793 3
2022-03-24 05:06:05.018547679 +0000 UTC m=+0.402268233 4
false
2022-03-24 05:06:06.020410484 +0000 UTC m=+1.404131050 1

Original: https://www.cnblogs.com/arvinhuang/p/16437919.html
Author: 平凡键客
Title: 常见的限流算法

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/621920/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球