浅谈流量限制

本文介绍了I/O密集型和CPU密集型程序的区别,并阐述了在遇到性能瓶颈时如何通过优化资源来提升系统性能。重点讨论了令牌桶算法作为流量限制手段,用于控制请求速率并防止系统过载。详细解析了令牌桶的工作原理,并展示了如何在Go语言中实现令牌桶,包括批量放令牌和惰性求值的概念,以减少不必要的计算和提高效率。
摘要由CSDN通过智能技术生成
I/O密集型和CPU密集型

对于IO/Network瓶颈类的程序,其表现是网卡/磁盘IO会先于CPU打满,这种情况即使优化CPU的使用也不能提高整个系统的吞吐量,只能提高磁盘的读写速度,增加内存大小,提升网卡的带宽来提升整体性能。而CPU瓶颈类的程序,则是在存储和网卡未打满之前CPU占用率先到达100%,CPU忙于各种计算任务,IO设备相对则较闲。

无论哪种类型的服务,在资源使用到极限的时候都会导致请求堆积,超时,系统hang死,最终伤害到终端用户。对于分布式的Web服务来说,瓶颈还不一定总在系统内部,也有可能在外部。非计算密集型的系统往往会在关系型数据库环节失守,而这时候Web模块本身还远远未达到瓶颈。

常见的流量限制手段
漏桶

漏桶是指我们有一个一直装满了水的桶,每过固定的一段时间即向外漏一滴水。如果你接到了这滴水,那么你就可以继续服务请求,如果没有接到,那么就需要等待下一滴水

令牌桶

令牌桶则是指匀速向桶中添加令牌,服务请求时需要从桶中获取令牌,令牌的数目可以按照需要消耗的资源进行相应的调整。如果没有令牌,可以选择等待,或者放弃

这两种方法,漏桶流出的速率固定,而令牌桶只要桶中有令牌就可以访问,即令牌桶允许一定程度的并发。令牌桶在桶中没有令牌的情况下会退化成漏桶模型。
在这里插入图片描述

开源github.com/juju/ratelimit提供了几种不同的令牌桶填充方式

默认
func NewBucket(fillInterval time.Duration, capacity int64) *Bucket
  • fillInterval 指每过多长时间向桶里放一个令牌
  • capacity是桶的容量,超过桶容量的部分会被直接丢弃
  • 桶初始是满的
批量放令牌
func NewBucketWithRate(rate float64, capacity int64) *Bucket

这个接口每次向桶中放令牌时,是放 quantum 个令牌,而不是一个令牌。会按照提供的比例,每秒钟填充令牌数。例如 capacity 是100,而 rate 是0.1,那么每秒会填充100*0.1=10个令牌

在桶中获取令牌
func (tb *Bucket) Take(count int64) time.Duration {}
func (tb *Bucket) TakeAvailable(count int64) int64 {}
func (tb *Bucket) TakeMaxDuration(count int64, maxWait time.Duration) (time.Duration, bool) {}
func (tb *Bucket) Wait(count int64) {}
func (tb *Bucket) WaitMaxDuration(count int64, maxWait time.Duration) bool {}
原理

从功能上看,令牌桶模型就是对全局计数的加减法操作,使用计数需要我们去加读写锁。在Go语言中,可以通过buffer channel来完成加减令牌的操作:

var tokenBucket = make(chan struct{}, capacity)

每过一段时间向tokenBucket中添加token,如果bucket已经满了,那么直接放弃。这里就是一个生产消费者模型

package main

import (
    "fmt"
    "math/rand"
    "time"
)

var tokenBucket = make(chan struct{}, kCapcity)

const (
    kFillInterval = 1 * time.Millisecond
    kCapcity      = 100
)

func main() {

    fillToken := func() {
        ticker := time.NewTicker(kFillInterval)
        for {
            select {
            case <-ticker.C:
                select {
                case tokenBucket <- struct{}{}:
                default:
                }
                fmt.Println("current token cnt:", len(tokenBucket), time.Now())
            }
        }

    }

    getRandom := func() int {
        seed := rand.Intn(10)
        return seed
    }

    getToken := func() {
        ticker := time.NewTicker(15 * time.Millisecond)
        for {
            select {
            case <-ticker.C:
                num := getRandom()

                for i := 0; i < num; i++ {
                    <-tokenBucket
                }
            }
        }

    }

    go fillToken()
    go getToken()

    time.Sleep(time.Hour)
}


惰性求值

我们来思考一下,令牌桶每隔一段固定的时间向桶中放令牌,如果我们记下上一次 放令牌的时间为 t1,和当时的令牌数k1,放令牌的时间间隔为ti,每次向令牌桶中 放x个令牌,令牌桶容量为cap。现在如果有人来调用 getToken来取n个令 牌,我们将这个时刻记为t2。在t2时刻,令牌桶中理论上应该有多少令牌呢?伪代码如下:

cur = k1 + ((t2 - t1)/ti) * x
cur = cur > cap ? cap : cur

我们用两个时间点的时间差,再结合其它的参数,理论上在取令牌之前就完全可以 知道桶里有多少令牌了。那劳心费力地像本小节前面向channel里填充token的操 作,理论上是没有必要的。只要在每次 Take 的时候,再对令牌桶中的token数进 行简单计算,就可以得到正确的令牌数。在得到正确的令牌数之后,再进行实际的 Take 操作就好,这个 Take 操作只需 要对令牌数进行简单的减法即可,记得加锁以保证并发安全。 github.com/juju/ratelimit这个库就是这样做的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值