golang限速、接口限速、并发限速的常用方法

限速&限并发

速度qps = 并发*(1/平均处理时间)

区别

  • 限速-限流量
  • 限并发-限最大同时处理量
    (限速 餐馆受限服务员人力,每小时最多接待100人;限并发,餐馆受限餐桌数,无论何时最多接待20桌;)

如何选择

  • 大多数情况限那个都可以

  • 流量越大越适合限速 (如常见低耗时接口限速,如果接口耗时过长,更适合改成异步队列方式

  • 处理周期越长、资源绑定越高 越适合限并发 (如脚本任务队列处理时间长,依赖cpu核数

    (事实上餐馆主要限的就是并发)

限速方法

周期计数-sleep法

思路,处理了限速量时还没超过限速周期,则睡眠。伪代码如下

  qpsLimit=10
  t1=time.now()
  curProccessCount=0
  limitedFunc(){
    //do something
    curProccessCount++ // need atomic
    if curProccessCount>=qpsLimit {
        now=time.now()
        if (now-t1)<1s  sleep (t1+1s-now);
        curProccessCount=0
        t1=time.now()
    }   
  }  

周期内限速不均匀,更适合放在生产消费模型中生产端限速,在高限速下开销最小

计时限速器 time.tiker 、 rateLimiter

  • go time.ticker
var limiterTicker <-chan time.Time
func init() {
// qmsLimit:=50
    limiterTicker = time.Tick(time.Duration(int64(1000/50) * int64(time.Millisecond)))
}
func limitedFunc() {
    <-limiterTicker
    //do something
} 
  • ratelimiter 如 “go.uber.org/ratelimit”
// "go.uber.org/ratelimit"
var uberRatelimter ratelimit.Limiter

func init() {
    // qmsLimit:=50
    //ratelimit.WithSlack(10)  类似令牌桶中的桶容量,无slack,表示均匀每20ms一次, slack 10意思为 假如前半秒一个请求没来,那么后半秒则最多可处理 50/2+10个
    // ratelimter2 = ratelimit.New(50,ratelimit.WithoutSlack) 
    uberRatelimter = ratelimit.New(50,ratelimit.WithSlack(10))
}
func limitedFunc() {
    uberRatelimter.Take()
    //do something
}

ratelimit原理简化如下
同时uber/ratelimit 无法进行< 1q/s的限速,这里支持

type RateLimiter interface {
	Take() int64        //获取下一个时间戳
}
type myRateLimiter struct {
	lastTime   int64
	msInterval int64 // us
}
func NewRateLimiterByInterval(msInterval int64) (r RateLimiter) {
	mr := &myRateLimiter{
		msInterval: msInterval,
	}
	return mr
}
func (r *myRateLimiter) Take() (unixMsTimestamp int64) {
	t := time.Now().UnixNano() / 1e6
	if r.msInterval == 0 {
		return t
	}
	t2 := r.lastTime
	dec := t2 - t
	if dec <= 0 {
		r.lastTime = t + r.msInterval
		return t
	} else {
		atomic.AddInt64(&r.lastTime, r.msInterval)
		time.Sleep(time.Duration(dec * 1e6))
		return t2
	}
}

限并发方法

  • 用消费队列数量限制,消费者和队列一对一同步消费
  • 控制处理线程(协程)数量,单线程内同步处理
  • golang 有缓冲 chan
var limiterChan chan struct{}
func init() {
    // 最大并发数10
    limiterChan=make(chan struct{},10)
}
func limitedFunc() {
    limiterChan<- struct{}{}
    //do something
    <-limiterChan
}

线上环境限速,接口限速

  • 总限制/pod数(限速 限并发)

    假设服务某接口限速100,16个pod,那么 100/16 = 6 ,每个pod限速6即可;
    优点简单、缺点 限速受pod水平扩缩容影响,负载不均时无法打到最大限速 ;
    限并发情况负载不均问题在长周期任务特性下不明显,也可通过队列数量控制;

  • 令牌桶、漏桶服务 (限速)

    令牌桶以限速速度均匀向桶中放置token;
    处理限速操作时,先请求令牌桶服务、获得token正常处理,未获取则等待或者返回错误;
    令牌桶 漏桶区别是,令牌桶匀速向桶中放token,漏桶匀速漏token,由于桶有一定容量,令牌桶遇到请求量突增时会短暂超过实际限速。

  • 接口请求参数相关的限速(比如用户、用户行为相关)
    用redis这种单点来记录 ;
    例如对 用户user1、getData1 接口限速1分钟1次;
    我们可以构建一个key myservice:getData1:usr1 , 值存储最近访问时间 ;
    在请求到来时、 setNxByEx(myservice:limit:getData1:usr1,1min),若ok则继续,否则返回受到限制;
    那要想获取受限时间呢? 取出对应值-now ;
    那要是限制1分钟5次呢?存个长度为5的数组当队列,访问到来时,若队尾<now-1min 则push(now),否则受到限速;

  • redis 脚本(限速-限并发,不优先推荐, 低效)

    • 限速 key={限速器名字}_{时间戳秒}
      限速前,执行脚本 获取key值,若为空(setex 1s)或小于最大限速量则 执行INCR 返回正常、若大于最大限速则返回错误
      local key = "key1_time1"
      local threshold = 8 
      if redis.call("EXISTS", key) == 0 or tonumber(redis. call("GET", key)) < threshold then
        redis.call("INCR", key)
        return true
      else
      return false
      end
      
    • 限制并发 key={限速器名字}
      和限速比key没有时间成分、并发限制前操作同限速,完成并发操作后进行 DECR key处理
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
golang中,泛型是一种通用类型的编程方式,可以在代码中使用不特定具体类型来实现通用性。然而,目前的golang版本(截至2021年)并不直接支持泛型。 虽然golang没有原生的泛型支持,但可以通过接口方法来实现类似的效果。在接口的定义中,可以使用空接口类型(interface{})作为参数或返回类型。空接口类型可以接收任意类型的值。 在使用空接口类型作为接口方法的参数时,可以将任意类型值传递给该方法。在方法内部,可以使用类型断言将空接口类型转换成具体的类型,从而实现对不同类型的值的处理。这样可以实现一种类似于泛型的效果,通过接口方法来处理不同类型的值。 示例代码如下: ```go package main import "fmt" type Generic interface { Process(interface{}) interface{} } type StringContainer struct{} func (sc StringContainer) Process(item interface{}) interface{} { str, ok := item.(string) if !ok { return "error: not a string" } return "string: " + str } type IntContainer struct{} func (ic IntContainer) Process(item interface{}) interface{} { num, ok := item.(int) if !ok { return "error: not an int" } return "int: " + fmt.Sprintf("%d", num) } func main() { strContainer := StringContainer{} intContainer := IntContainer{} strResult := strContainer.Process("hello") intResult := intContainer.Process(123) fmt.Println(strResult) // output: string: hello fmt.Println(intResult) // output: int: 123 } ``` 在上述示例中,我们定义了一个`Generic`接口,该接口包含了一个`Process`方法,该方法接收一个空接口类型参数并返回一个空接口类型结果。然后我们定义了`StringContainer`和`IntContainer`两个结构体,并为它们实现了`Process`方法。在`Process`方法中,我们使用类型断言将参数转换为具体的类型,然后进行相应的处理。最后在`main`函数中,我们创建了`StringContainer`和`IntContainer`的实例,并调用其`Process`方法来处理不同类型的值。 通过接口方法的利用,我们可以实现类似于泛型的效果。当然,这种方式并不是完全等同于原生的泛型特性,而是一种在golang中模拟实现泛型的方法

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值