通过对并发访问/请求进行限速,或者对一个时间窗口内的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务、排队或等待、降级等处理
说白了就是限制请求数量,或者是在某一段时间内限制总的请求数量
例如秒杀网站,限制22点5分 -- 22点10分 秒杀999份产品, 限制放行 5w 个请求,若在该段时间内,请求在第5w以后的请求,直接拒之门外, 也就是我们在进入网站的时候显示,系统繁忙
例如当系统的访问量突然剧增,大量的请求涌入过来,我们可能会知道会突然有一波高峰,这个时候如果服务器承受不了压力的话,就会崩溃,例如如下几类业务
我们在某宝或某东的热门节日上剁手,付款的时候,还是我们怀着焦灼的心等待着排队的人数一个一个下降的时候吗?
我们在疯狂抢购商品,由于点击太快,热情太高,导致多次弹出系统繁忙,请稍后再试,还记得吗?
更有甚者,在流量过大的时候,直接提示拒绝访问的,这些是不是都一一浮现在脑海呢?
根据如上场景,我们的限流思路会是这个样子的:
关于拒绝请求就相对简单粗暴,对于设置排队就会有多种排队方式了,咱们继续聊
除了限流还有什么方式可以解决或者缓解这种突然大量请求的情况呢?
还有熔断,降级,都可以有效的解决这样的问题
那啥是降级?
即服务降级,当我们的服务器压力剧增时,为了保证核心模块的高可用,这里指的是我们自身的系统出现了故障而降级,有如下2个**常用的解决方式
如图,某网站,当用户请求数猛增,服务器吃不消的时候,就可以选择把评论功能,修改密码等功能关闭,确保支付系统,数据系统等核心功能能够正常运行
哦?那熔断是啥?
与服务降级还是有区别的,这里指的是指依赖的外部接口出现故障的情况下,会设置断绝和外部接口的关系。
服务器A依赖于服务器B的对外接口,在某个时刻服务器B的接口出现异常,响应时间极其的慢,可是此接口会影响到服务器的整个运作,那么这个时候,服务器A就可以在请求服务器B该接口的时候,默认设置返回错误
我们来分享一个最常用的限流算法,大致分为以下 4 种
最简单的是 使用计数器来控制,设置固定的时间内,处理固定的请求数
上述图,固定时间窗口来做限制,1 s只能处理2个请求,红色请求则会被直接丢弃
能够去平滑一下处理的任务数量。滑动窗口计数器是通过将窗口再细分,并且按照时间滑动,这种算法避免了固定窗口算法带来的双倍突发请求,但时间区间精度越高,算法所需的空间容量越大
为了解决上述红色部分丢掉的问题,引入了 漏桶的方式进行限流,漏桶是有缓存的,有请求就会放到缓存中
漏桶,听起来有点像漏斗的样子,也是一滴一滴的滴下去的
如图,水滴即为请求的事件,如果漏桶可以缓存5000个事件,实际服务器1s处理1000个事件,那么在高峰期的时候,响应时间最多等5秒,但是不能一直是高峰期,否则,一直响应时间都是5s,就会是很慢的时间了,这个时间也是很影响体验的
如果桶满了,还有请求过来的话,则会被直接丢弃,这种做法,还是丢弃了请求
优势
劣势
通过动态控制令牌的数量,来更好的服务客户端的请求事情,令牌的生成数量和生产速率都是可以灵活控制的
如上,令牌桶和漏桶不同的地方在于
若发现一直是处于高峰期,可以考虑扩大令牌桶
如何在http 中间件中加入流控呢,目的是限流,每一个请求,都需要经过这个中间件,才有机会向后走,才有机会被处理
type middleWareHandler struct {
r *httprouter.Router
l *ConnLimiter
}
func NewMiddleWareHandler(r *httprouter.Router, cc int) http.Handler {
m := middleWareHandler{}
m.r = r
m.l = NewConnLimiter(cc) // 限制数量
return m
}
说完令牌桶,我们来说说限流器
限流器是后台服务中的非常重要的组件
它可以用做啥呢?
限流器的实现方法有很多种,基本上都是基于上述的限流算法来实现的
golang标准库中就自带了限流算法的实现,不需要我们自己造轮子
golang.org/x/time/rate
,直接用就好了,该限流器是基于Token Bucket(令牌桶)实现的
令牌桶就是我们上面说的桶,里面装令牌,系统会以恒定速率向桶中放令牌
桶满则暂时不放。用户请求就要向桶里面拿令牌
我们来看看限流器咋用
构造一个限流对象
limiter := NewLimiter(5, 1);
也就是说,其构造出的限流器是
我们当然也可以使用另外的设置方式,包中也有提供
limit := Every(500 * time.Millisecond);
limiter := NewLimiter(limit, 1);
可以用Every
方法来指定向Token桶中放置令牌的间隔,上面代码就表示每500 ms往桶中放一个令牌
也就说,上述代码是1 秒钟,产生2个令牌
Limiter
是支持可以调整速率和桶大小的,我们来看看源码
// 改变放入令牌的速率
SetLimit(Limit)
// 改变令牌桶大小
SetBurst(int)
我们来写一个案例看看:
package main
import (
"context"
"log"
"time"
"golang.org/x/time/rate"
)
func main() {
l := rate.NewLimiter(1, 2)
// limit表示每秒产生token数,buret最多存令牌数
log.Println(l.Limit(), l.Burst())
for i := 0; i < 50; i++ {
//这里是阻塞等待的,一直等到取到一个令牌为止
log.Println("... ... Wait")
c, _ := context.WithTimeout(context.Background(), time.Second*2)
// Wait阻塞等待
if err := l.Wait(c); err != nil {
log.Println("limiter wait error : " + err.Error())
}
log.Println("Wait ... ... ")
// Reserve返回等待时间,再去取令牌
// 返回需要等待多久才有新的令牌, 这样就可以等待指定时间执行任务
r := l.Reserve()
log.Println("reserve time :", r.Delay())
//判断当前是否可以取到令牌
// Allow判断当前是否可以取到令牌
a := l.Allow()
log.Println("Allow == ", a)
}
}
看到个数的案例,我们可以看到,包里面提供给我们使用的消费方法有3种
img
Wait , 等于 WaitN(ctx,1)
若此时桶内令牌数组不足(小于N
),那么Wait方法将会阻塞一段时间,直至令牌满足条件,否则就一直阻塞
若满足条件,则直接返回结果
Wait的context
参数。可以设置超时时间
看看函数原型
func (lim *Limiter) Allow() bool
func (lim *Limiter) AllowN(now time.Time, n int) bool
Allow
等于 AllowN(time.Now(),1)
, 当前取一个令牌,若满足,则为true,否则 false
AllowN
方法 指的是,截止到某一时刻,目前桶中令牌数目是否至少为N个,满足则返回true,同时从桶中消费N个令牌。反之返回不消费令牌,返回false
Reserve
, 等于 ReserveN(time.Now(), 1)
ReserveN
当调用完成后,无论令牌是否充足,都会返回一个Reservation*对象
我们可以调用该对象的Delay()
方法,有如下注意:
该方法返回了需要等待的时间
当然,若不想等待,你可以归还令牌,一个都不能少,调用该对象的Cancel()
方法即可
golang.org/x/time/rate
中,限流器的基本使用好了,本次就到这里,下一次 互联网协议介绍和分享,
技术是开放的,我们的心态,更应是开放的。拥抱变化,向阳而生,努力向前行。
我是小魔童哪吒,欢迎点赞关注收藏,下次见~