go 限速与限流

限速方式

  1. 漏桶算法: 讲究的是服务器匀速的去处理并发请求,但... 为达到目的居然采用sleep了。简单来说服务器匀速处理请求,超过桶容量会被舍弃
  2. 令牌桶算法:拿到令牌的请求被处理,否则被舍弃。在桶里面的令牌被拿光了的时候,此时就是一边生产令牌一边消耗令牌了,这种场景下也匀速了。存峰值,峰值为桶容量 + 消耗此容量所需时间产生的新token。

概念

  1. 熔断 与 限速 与 限流,过载 你得清楚它们的意思。
  2. 服务熔断:对上游服务的保护。打比方你的 A服务调用上游 B服务,并发来了,A发现B返回的数据不正常,A于是不再掉B 给它10S 缓冲期,那么不掉B的这10S发生的过程就是熔断。像保险丝
  3. 服务过载:指的是自身服务流量过大,这个时候需要考虑限速或者限流对自己进行保护
  4. 限速是限制流量流入的速度,才不会管你服务器 hang了没。 打比方你服务器正常情况下,10万qps,现在来了100万流量并将持续1小时,由于限速的作用并没有一下打死你的服务器,然后你的服务器任然以10万的qps提供服务,过30分钟后突然db出现慢查询了,请注意这个时候相当于你服务器每秒qps没有10万了,但是限速限制的还是10万,这个时候很可能你服务器即将gg。。。
  5. 限流着重是控制并发的最大流量。像资源池一样,并发到了设定的100个,那么不再接受请求,除非有请求处理完毕把资源放回了资源池。

基于 tollbooth 实现

如果要讲究开箱机即用,用这个开源组件去做http限速你只要按着demo稍微配置下。

  1. 可以配置只针对GET或POST 类型请求做限制
  2. 可以配置只针对ip 做限制
  3. 可以配置只针对请求头中带有某种特定标识的请求做限制(不重要的服务熔断可以采用它)
  4. 文档中有gin,echo等http的使用该组件的文档
  5. 可以开发成中间件
  6. 可以做到定时清理计数~
  7. 可以设置丢弃返回值
    ......
package main

import (
    "github.com/didip/tollbooth"
    "net/http"
)

func HelloHandler(w http.ResponseWriter, req *http.Request) {
    w.Write([]byte("Hello, World!"))
}

func main() {
    // Create a request limiter per handler.
    http.Handle("/", tollbooth.LimitFuncHandler(tollbooth.NewLimiter(1, nil), HelloHandler))
    http.ListenAndServe(":12345", nil)
}

探索 golang.org/x/time/rate

令牌桶这个算法


image.png

精简版:一个gorontinue定时往里面塞,所有的请求想要被响应必须先去channel取token,没取到的丢弃。
但感觉golang.org/x/time/rate 实现方式巧。它是直接通过计算的一个计算算法表达出token的过程。

使用demo


package main

import (
    "fmt"
    "golang.org/x/time/rate"
    "time"
)

const (
    speed = 1  //每秒执行的次数
    capacity = 10 //桶的容量大小
)

var gameScene = rate.NewLimiter(speed , capacity )

func main() {

    for i:=0;i<100;i++{
        k :=i
        if isGameSceneAllow(){
            fmt.Println("我是被接受的请求",time.Now().Unix(),k)
        }
    }


    //9秒钟sleep,忽略代码执行时间,那么将会产生9个
    time.Sleep(time.Second * 9)


    //以下打印9个,则证明限速起作用了
    for i:=0;i<100;i++{
        k :=i
        if isGameSceneAllow(){
            fmt.Println("我是被接受的请求2222",time.Now().Unix(),k)
        }
    }

    time.Sleep(time.Second * 9)

}

func isGameSceneAllow()(b bool){
    if gameScene.Allow() == false {
        return
    }
    b =true
    return
}

go-zero 结合redis+lua 做的分布式限速控制

文档地址:https://www.yuque.com/tal-tech/go-zero/gobn7v
github: https://github.com/tal-tech/go-zero

package main

import (
    "flag"
    "fmt"
    "log"
    "runtime"
    "strconv"
    "sync"
    "sync/atomic"
    "time"

    "github.com/tal-tech/go-zero/core/limit"
    "github.com/tal-tech/go-zero/core/stores/redis"
)

const seconds = 5

var (
    rdx     = flag.String("redis", "localhost:6379", "the redis, default localhost:6379")
    rdxType = flag.String("redisType", "node", "the redis type, default node")
    rdxPass = flag.String("redisPass", "", "the redis password")
    rdxKey  = flag.String("redisKey", "rate", "the redis key, default rate")
    threads = flag.Int("threads", runtime.NumCPU(), "the concurrent threads, default to cores")
)

func main() {
    flag.Parse()

    store := redis.NewRedis(*rdx, *rdxType, *rdxPass)
    fmt.Println(store.Ping())
    lmt := limit.NewPeriodLimit(seconds, 5, store, *rdxKey)
    timer := time.NewTimer(time.Second * seconds)
    quit := make(chan struct{})
    defer timer.Stop()
    go func() {
        <-timer.C
        close(quit)
    }()

    var allowed, denied int32
    var wait sync.WaitGroup
    for i := 0; i < *threads; i++ {
        wait.Add(1)
        go func() {
            for {
                select {
                case <-quit:
                    wait.Done()
                    return
                default:
                    if v, err := lmt.Take(strconv.FormatInt(int64(i), 10)); err == nil && v == limit.Allowed {
                        atomic.AddInt32(&allowed, 1)
                    } else if err != nil {
                        log.Fatal(err)
                    } else {
                        atomic.AddInt32(&denied, 1)
                    }
                }
            }
        }()
    }

    wait.Wait()
    fmt.Printf("allowed: %d, denied: %d, qps: %d\n", allowed, denied, (allowed+denied)/seconds)
}

限流

着重点是去限制你的服务器并发处理请求的能力。打比方你的服务器最多同时处理1万个请求,它的出现就是同时处理1万个请求,请求处理完毕资源就会被释放,就可以让新的流量进入。

package main

import (
    "log"
    "net/http"
    "text/template"
    "time"

    "github.com/julienschmidt/httprouter"
)

type middleWareHandler struct {
    r *httprouter.Router
    l *ConnLimiter
}

//NewMiddleWareHandler ...
func NewMiddleWareHandler(r *httprouter.Router, cc int) http.Handler {
    m := middleWareHandler{}
    m.r = r
    m.l = NewConnLimiter(cc)
    return m
}

func (m middleWareHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if !m.l.GetConn() {
        defer func() { recover() }()
        log.Panicln("Too many requests")
        return
    }
    m.r.ServeHTTP(w, r)
    defer m.l.ReleaseConn()
}

//RegisterHandlers ...
func RegisterHandlers() *httprouter.Router {
    router := httprouter.New()
    router.GET("/ce", ce)
    return router
}

func ce(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
    //为了演示效果这块设置了等待
    time.Sleep(time.Second * 100)
    t, _ := template.ParseFiles("./videos/ce.html")
    t.Execute(w, nil)
}

func main() {
    r := RegisterHandlers()
    //里面的参数2为设置的最大流量
    mh := NewMiddleWareHandler(r, 2)
    http.ListenAndServe(":9000", mh)
}


//ConnLimiter 定义一个结构体
type ConnLimiter struct {
    concurrentConn int
    bucket         chan int
}

//NewConnLimiter ...
func NewConnLimiter(cc int) *ConnLimiter {
    return &ConnLimiter{
        concurrentConn: cc,
        bucket:         make(chan int, cc),
    }
}

//GetConn 获取通道里面的值
func (cl *ConnLimiter) GetConn() bool {
    if len(cl.bucket) >= cl.concurrentConn {
        log.Printf("Reached the rate limitation.")
        return false
    }

    cl.bucket <- 1
    return true
}

//ReleaseConn 释放通道里面的值
func (cl *ConnLimiter) ReleaseConn() {
    c := <-cl.bucket
    log.Printf("New connction coming: %d", c)
}

文献

golang版本实现限速参考
算法介绍
tollbooth 一个开箱即用的限速项目
uber漏铜
限速

你可能感兴趣的:(go 限速与限流)