常见的限流算法

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

令牌桶算法

令牌桶算法是常见的一种限流算法。假设有一个桶,以固定速度(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/516267/

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

(0)

大家都在看

  • 为开源项目 go-gin-api 增加后台任务模块

    任务管理界面 (WEB) 任务调度器 任务执行器 小结 推荐阅读 任务管理界面 (WEB) 支持在 WEB &#x754C;&#x9762; 中对任务进行管理,例如…

    Go语言 2023年5月25日
    0108
  • muduo源码分析之回调模块

    这次我们主要来说说 muduo库中大量使用的回调机制。 muduo主要使用的是利用 Callback的方式来实现回调,首先我们在自己的 EchoServer构造函数中有这样几行代码…

    Go语言 2023年5月25日
    040
  • golang初探

    最近两天对go语言做了一个初步的了解,记录一下。之前没有按照原创发表,所以重新发布一次。 第一个感受就是使用起来方便,从官网下载安装包,参考https://golang.googl…

    Go语言 2023年5月25日
    072
  • 字符集与编码

    一个比特(bit)可以是0,或者是1,8个比特(bit),组成一个字节(byte)。全为0时代表数字0,全为1时代表数字255。 一个字节可以表示256个数字,两个字节可以表示65…

    Go语言 2023年5月25日
    048
  • 使用Go http重试请求

    原文连接:https://www.zhoubotong.site/post/78.html开发中对于http请求是经常遇到,一般可能网络延迟或接口返回超时,对于发起客户端的请求, …

    Go语言 2023年5月25日
    054
  • sync:一. 原子操作(atomic)

    原子操作是指在程序运行中不能被中断的操作,原子操作是无锁的常常是由CPU指令直接实现,而锁一般由操作系统的调度器实现,所以原子操作的效率一般更高。 golang中原子操作支持的类型…

    Go语言 2023年5月25日
    055
  • 通过Go语言创建CA与签发证书

    404. 抱歉,您访问的资源不存在。 可能是网址有误,或者对应的内容被删除,或者处于私有状态。 代码改变世界,联系邮箱 contact@cnblogs.com 园子的商业化努力-困…

    Go语言 2023年5月25日
    062
  • Excelize 发布 2.6.1 版本,支持工作簿加密

    Excelize 是 Go 语言编写的用于操作 Office Excel 文档基础库,基于 ECMA-376,ISO/IEC 29500 国际标准。可以使用它来读取、写入由 Mic…

    Go语言 2023年5月25日
    064
  • Go – 如何编写 ProtoBuf 插件(二)?

    上篇文章《Go – 如何编写 ProtoBuf 插件 (一) 》,分享了使用 proto3 的 &#x81EA;&#x5B9A;&#x4E49;…

    Go语言 2023年5月25日
    057
  • Go语言之反射

    一、反射的基本概念 (一)什么是反射 反射可以再运行时动态获取变量的各种信息,比如变量的类型、值等 如果时结构体变量,还可以获取到结构体本身的各种信息,比如结构体的字段、方法 通过…

    Go语言 2023年5月29日
    076
  • Operator 示例:使用 Redis 部署 PHP 留言板应用程序

    安装 Docker Desktop,并启动内置的 Kubernetes 集群 注册一个hub.docker.com 账户,需要将本地构建好的镜像推送至公开仓库中 安装 operat…

    Go语言 2023年5月25日
    066
  • Excelize 2.5.0 正式发布,这些新增功能值得关注

    Excelize 是 Go 语言编写的用于操作 Office Excel 文档基础库,基于 ECMA-376,ISO/IEC 29500 国际标准。可以使用它来读取、写入由 Mic…

    Go语言 2023年5月25日
    065
  • Go Error 嵌套到底是怎么实现的?

    原文链接: Go Error 嵌套到底是怎么实现的? Go Error 的设计哲学是 「Errors Are Values」。 这句话应该怎么理解呢?翻译起来挺难的。不过从源码的角…

    Go语言 2023年5月25日
    065
  • Golang笔记

    本文主要为go的学习过程笔记。 一、基本介绍 1、开发环境安装-windows安装 打开Golang官网,选择对应版本,进行安装。 2、环境变量配置 1)步骤 (1)首先在环境变量…

    Go语言 2023年5月25日
    052
  • TCP粘”包”问题浅析及解决方案Golang代码实现

    一、粘”包”问题简介 在socket网络编程中,都是端到端通信, 客户端端口+客户端IP+服务端端口+服务端IP+传输协议就组成一个可以唯一可以明确的标识一…

    Go语言 2023年5月25日
    057
  • go语言异常处理

    go语言异常处理 go语言引入了一个关于错误错里的标准模式,即error接口,该接口的定义如下: type error interface{ Error() string } 对于…

    Go语言 2023年5月29日
    060
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球