golang 限流控制

限流

日常开发中,一般会遇到几种场景需要限流,比如有个api-server, 需要限制单个用户的调用频率,避免用户恶意刷接口或者突发大流量导致服务不可用等,这边记录几个常用的限流方法。

并发控制

简单的并发控制,用户的所有请求丢到一个channel里,再指定一个带缓冲区的channel为并发池,缓冲池的长度就是可以同时存在的协程数量,然后将执行完的任务根据需要直接返回或者丢到另外一个channel 里, 这样做的问题是如果任务太多,后面的任务会慢慢等待(因为channel阻塞机制),用户体验不是太好。

在这里插入图片描述

Code:

package main

import (
	"fmt"
	"sync"
)


func setJob() <-chan int {
    var wg sync.WaitGroup
    jobChan := make(chan int, 50)
    wg.Add(1)
    go func(){
        for i := 0; i < 50; i++ {
            jobChan <- i
        }
        close(jobChan)
        wg.Done()
    }()
    wg.Wait()
    return jobChan
}

func main() {
	var wg sync.WaitGroup
    // 创建一个需要处理的数据来源, 假设是个channel
    jobChan := setJob()

    // 将结果存入一个channel
    resChan := make(chan int , 50)

    // 设置一个并发池
	buckets := make(chan bool, 10)
	for job := range jobChan {
		buckets <- true
		wg.Add(1)
		go func(job int) {
            res := 10 * job
            resChan <- res
			<-buckets
			wg.Done()
		}(job)
	}

    wg.Wait()
    close(resChan)
    tmpA := []int{}
	for r := range(resChan) {
		tmpA = append(tmpA, r)
	}
    fmt.Println(tmpA)
    
}
漏桶

简单的限流,指定一个大小固定的桶(bucket),以指定速率流入流出,如果桶满了,则拒绝后续请求,好处是简单,但是由于rate是固定的,导致了在无法在业务突发高峰时候(比如活动期间)有比较好的适配性。

在这里插入图片描述

Code:

package main

import (
    "fmt"
    "math"
    "time"
)

// 漏桶限流器
type BucketLimit struct {
    rate       float64 //漏桶中水的漏出速率, 即每秒流多少水
    bucketSize float64 //漏桶最多能装的水大小
    lastAccessTime   time.Time //上次访问时间
    curWater   float64 //当前桶里面的水
}

func NewBucketLimit(rate float64, bucketSize int64) *BucketLimit {
    return &BucketLimit{
        bucketSize: float64(bucketSize),
        rate:       rate,
        curWater:   0,
    }
}

func (b *BucketLimit) AllowControl() bool {
    now := time.Now()
    pastTime := now.Sub(b.lastAccessTime)

    // 当前剩余水量,当前水量减去距离上次访问的流出水量,如果流完了,即剩余水量为0
    b.curWater = math.Max(0, b.curWater - float64(pastTime) * b.rate)

    b.lastAccessTime = now

    // 当前水量必须小于桶的总量,不然则流出了
    if b.curWater < b.bucketSize {
        b.curWater = b.curWater + 1
        return true
    }
    return false
}

func main() {
    // 创建一个流出速率为1qps,桶的总量为2的限流器
    limit := NewBucketLimit(1, 2)

    // 在桶里放入1000滴水
    for i := 0; i < 1000; i++ {
        allow := limit.AllowControl()
        if allow {
            fmt.Printf("第%d滴水, 顺利流出\n", i)
            continue
        } else {
            fmt.Printf("第%d滴水, 溢出丢弃\n", i)
            time.Sleep(time.Millisecond * 100)
        }
    }
}
令牌桶

令牌桶相比漏桶可以对突发的流量做一定程度的处理,该方法意思是一定速率往一个桶里放令牌(token),同时往桶里注水,每消耗一滴水,需要一定数量的令牌,这里代码假设需要2块令牌才能消耗1滴水,所以处理水滴的速度主要取决于令牌的数量。假设放入令牌的速度是4块/s,每秒消耗的水滴数量是1滴,即每秒消耗2块令牌,那么每秒多出来的2块令牌就会积累在桶里,直到桶满位置,然后因为某次活动大放水,每秒消耗的水滴数量突然变成了3滴,即每秒需要消耗6块另外,大于每秒放入的令牌数量,由于之前桶里有积累的令牌,所以即使放水量加大,依然可以在令牌桶消耗完之前快速处理。

在这里插入图片描述

Code:

package main
 
import (
    "fmt"
    "time"
    "math"
)
 
// 令牌桶限流器
type BucketLimit struct {
    rate       float64 //令牌桶放令牌的速率
    bucketSize float64 //令牌桶最多能装的令牌数量
    lastAccessTime   time.Time //上次访问时间
    curTokenNum  float64 //桶里当前的令牌数量
}

func NewBucketLimit(rate float64, bucketSize int64) *BucketLimit {
    return &BucketLimit{
        bucketSize: float64(bucketSize),
        rate:       rate,
        curTokenNum:   0,
    }
}

func (b *BucketLimit) AllowControl(tokenNeed float64) bool {
    now := time.Now()
    pastTime := now.Sub(b.lastAccessTime)
 
    // 在距离上次访问期间一共可发放了多少令牌
    newTokenNum := float64(pastTime) / float64(b.rate)

    // 剩余令牌数量不能超过桶的总空间
    b.curTokenNum = math.Min(b.bucketSize, b.curTokenNum + newTokenNum)

    b.lastAccessTime = now
    // tokenNeed 指处理一个请求需要的令牌数量
    if tokenNeed > b.curTokenNum {
        return false
    } else {
        b.curTokenNum = b.curTokenNum - tokenNeed
        return true
    }
}
 
func main() {
    // 创建一个放令牌速率为10qps,桶的总量为20的限流器
    limit := NewBucketLimit(10, 20)

    // 在桶里放入100滴水
    for i := 0; i < 100; i++ {
        // 处理1滴水需要消耗19块令牌
        if i == 50 {
            time.Sleep(3 * time.Second)
        }
        allow := limit.AllowControl(19)
        if allow {
            fmt.Printf("第%d滴水, 顺利流出\n", i)
            continue
        } else {
            fmt.Printf("第%d滴水, 溢出丢弃\n", i)
            time.Sleep(time.Millisecond * 100)
        }
    }
}
他山之石

如果不自己造轮子,其实已经有很多别人造好的轮子,简单记录下,按需求选用:

  • “https://golang.org/x/net/netutil” 官方自带的http限流工具

伪代码

import(
    "fmt"
    "net"
    "golang.org/x/net/netutil"
)
 
func main() {
    l, err := net.Listen("tcp", "127.0.0.1:0")
    if err != nil {
        fmt.Fatalf("Listen: %v", err)
    }
    defer l.Close()
    // 开启限流, 其实就算是记录器,max 就算最大的并发数量
    l = LimitListener(l, max)
    http.Serve(l, http.HandlerFunc())
    ......
}
  • “https://github.com/didip/tollbooth” 1.8K star的http限流中间件
  • “https://golang.org/x/time/rate” 官方的令牌桶限流代码包
  • “https://github.com/uber-go/ratelimit” uber的开源库,基于漏斗算法实现了一个限制器,也值得学习一下


后续代码新增和更新就放这里了:

  • “https://github.com/zyxpaomian/rate_limiter”
个人公众号, 分享一些日常开发,运维工作中的日常以及一些学习感悟,欢迎大家互相学习,交流

在这里插入图片描述

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
在 Go 中,可以使用 channel 实现限流,即通过 channel 的容量控制并发执行的协程数。 示例代码如下: ```go package main import ( "fmt" "time" ) func worker(id int, jobs <-chan int, results chan<- int) { for j := range jobs { fmt.Printf("worker %d 执行任务 %d\n", id, j) time.Sleep(time.Second) // 模拟任务执行时间 results <- j * 2 } } func main() { jobs := make(chan int, 100) // 创建任务 channel,缓冲区大小为 100 results := make(chan int, 100) // 创建结果 channel,缓冲区大小为 100 // 创建 3 个 worker 协程,即最多同时执行 3 个任务 for w := 1; w <= 3; w++ { go worker(w, jobs, results) } // 发送 9 个任务到 jobs channel for j := 1; j <= 9; j++ { jobs <- j } close(jobs) // 关闭 jobs channel,表示所有任务已发送完毕 // 收集所有结果 for a := 1; a <= 9; a++ { <-results } } ``` 在上面的示例中,我们创建了一个 `jobs` channel 和一个 `results` channel,用于分别传递任务和结果。我们创建了 3 个 worker 协程,并将 `jobs` 和 `results` channel 分别传递给它们。在主协程中,我们向 `jobs` channel 发送 9 个任务,并关闭 `jobs` channel,表示所有任务已发送完毕。然后我们收集所有结果。 由于 `jobs` channel 的缓冲区大小为 100,即最多可以存储 100 个任务,而 `results` channel 的缓冲区也为 100,即最多可以存储 100 个结果。因此,当 worker 协程数小于等于 3 时,所有任务都可以立即执行;当 worker 协程数大于 3 时,多余的任务会被存储在 `jobs` channel 中,直到有空闲的 worker 协程可以执行它们。这样就实现了限流的功能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值