限速方式
- 漏桶算法: 讲究的是服务器匀速的去处理并发请求,但... 为达到目的居然采用sleep了。简单来说服务器匀速处理请求,超过桶容量会被舍弃
- 令牌桶算法:拿到令牌的请求被处理,否则被舍弃。在桶里面的令牌被拿光了的时候,此时就是一边生产令牌一边消耗令牌了,这种场景下也匀速了。存峰值,峰值为桶容量 + 消耗此容量所需时间产生的新token。
概念
- 熔断 与 限速 与 限流,过载 你得清楚它们的意思。
- 服务熔断:对上游服务的保护。打比方你的 A服务调用上游 B服务,并发来了,A发现B返回的数据不正常,A于是不再掉B 给它10S 缓冲期,那么不掉B的这10S发生的过程就是熔断。像保险丝
- 服务过载:指的是自身服务流量过大,这个时候需要考虑限速或者限流对自己进行保护
- 限速是限制流量流入的速度,才不会管你服务器 hang了没。 打比方你服务器正常情况下,10万qps,现在来了100万流量并将持续1小时,由于限速的作用并没有一下打死你的服务器,然后你的服务器任然以10万的qps提供服务,过30分钟后突然db出现慢查询了,
请注意这个时候相当于你服务器每秒qps没有10万了
,但是限速限制的还是10万,这个时候很可能你服务器即将gg。。。 - 限流着重是控制并发的最大流量。像资源池一样,并发到了设定的100个,那么不再接受请求,除非有请求处理完毕把资源放回了资源池。
基于 tollbooth 实现
如果要讲究开箱机即用,用这个开源组件去做http限速你只要按着demo稍微配置下。
- 可以配置只针对GET或POST 类型请求做限制
- 可以配置只针对ip 做限制
- 可以配置只针对请求头中带有某种特定标识的请求做限制(不重要的服务熔断可以采用它)
- 文档中有gin,echo等http的使用该组件的文档
- 可以开发成中间件
- 可以做到定时清理计数~
- 可以设置丢弃返回值
......
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
令牌桶这个算法
精简版:一个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漏铜
限速