高并发系统的三大利器:
缓存
提升系统访问速度和增大处理容量,为相应业务增加缓存
降级
当服务器压力剧增时,根据业务逻辑选择降级,以此释放服务器资源,保证主要业务正常运行
限流
通过对并发限速,以达到拒绝服务、排队等待、降级等的处理
每次请求时计算桶流量,当流量超过阈值时,则触发降级请求
每次请求时从令牌桶取令牌,取不到则触发降级请求
time/rate是Golang 标准库中自带的限流算法,是基于 Token Bucket(令牌桶) 实现的
创建方法
rate.NewLimiter(limit,burst)
limit表示每秒产生的token数,burst表示令牌桶中最多存储的token数
使用方法(这是三个方法都会去取token)
func Test_RateLimiter(t *testing.T) {
l := rate.NewLimiter(1, 5)
log.Println(l.Limit(), l.Burst())
for i := 0; i < 100; i++ {
//阻塞等待直到,取到一个token
log.Println("before Wait")
c, _ := context.WithTimeout(context.Background(), time.Second*2)
if err := l.Wait(c); err != nil {
log.Println("limiter wait err:" + err.Error())
}
log.Println("after Wait")
//返回需要等待多久才有新的token,这样就可以等待指定时间执行任务
r := l.Reserve()
log.Println("reserve Delay:", r.Delay())
//判断当前是否可以取到token
a := l.Allow()
log.Println("Allow:", a)
log.Println("------------------------")
}
}
前五次都可以成功取得token,第六次则因超过token上限,所以获取失败
若是固定地向令牌桶中放入令牌,用户从桶中获取令牌,这么做固然是令牌桶的一种实现方式,但是效率过低,不仅需要多维护一个放入令牌的操作和一个令牌桶,还浪费了一些不必要的内存
所以在Golang的timer/rate中,并没有采用这种方式,而是采用了lazyload方式,知道每次消费之前,才根据时间差更新Token数目,而且也不使用令牌桶的方式来存放token,而是仅仅通过计数的方式
概要过程:
结构体分析
type Limiter struct {
limit Limit //每秒token数
burst int //令牌桶的最大数
mu sync.Mutex
tokens float64 //令牌数
// last is the last time the limiter's tokens field was updated
last time.Time //最后一次令牌更新时间
// lastEvent is the latest time of a rate-limited event (past or future)
lastEvent time.Time//最后一次限速时间
}
三种使用方法
在内部实现中,三种消费方式最终都调用了reserveN 函数来生成和消费token
因此,核心代码,直接来吧
reserveN方法的实现源码 看看一次请求中消费令牌的过程
func (lim *Limiter) reserveN(now time.Time, n int, maxFutureReserve time.Duration) Reservation {
lim.mu.Lock()
//没有设置limit就直接返回
if lim.limit == Inf {
lim.mu.Unlock()
return Reservation{
ok: true,
lim: lim,
tokens: n,
timeToAct: now,
}
}
//根据时间推移 计算桶中的数目返回限流的结果
//该方法用于计算 由于时间推移而导致的限流结果
now, last, tokens := lim.advance(now)
这里暂停一下,进去看看advance方法的实现
func (lim *Limiter) advance(now time.Time) (newNow time.Time, newLast time.Time, newTokens float64) {
last := lim.last
// 如果now比last还早,则last使用更早的时间
if now.Before(last) {
last = now
}
//1、时间容错处理,暂时不看
//当last是过期数据时,应避免delta德尔塔δ 移除
maxElapsed := lim.limit.durationFromTokens(float64(lim.burst) - lim.tokens)
elapsed := now.Sub(last)
if elapsed > maxElapsed {
elapsed = maxElapsed
}
//2、计算时间差内可以生成的token数量 delta德尔塔δ
delta := lim.limit.tokensFromDuration(elapsed)
//将之前的token数与时间差内的token数求和
tokens := lim.tokens + delta
//最大令牌数限制
if burst := float64(lim.burst); tokens > burst {
tokens = burst
}
return now, last, tokens
}
紧接着回到reserveN方法,获取了限流返回结果(now, last, tokens)之后
//计算剩余token,因为每次请求当然要token减一啦
tokens -= float64(n)
// 当token不够时,计算等待时间
var waitDuration time.Duration
if tokens < 0 {
waitDuration = lim.limit.durationFromTokens(-tokens)
}
// Decide result
ok := n <= lim.burst && waitDuration <= maxFutureReserve
// Prepare reservation
r := Reservation{
ok: ok,
lim: lim,
limit: lim.limit,
}
if ok {
r.tokens = n
r.timeToAct = now.Add(waitDuration)
}
// 更新状态 关锁返回即可
if ok {
lim.last = now
lim.tokens = tokens
lim.lastEvent = r.timeToAct
} else {
lim.last = last
}
lim.mu.Unlock()
return r
}
搞清楚了time/rate限速器使用和源码逻辑后,就可以看到我们的主线:微服务网关的中间件上了
拿HTTP的限流中间件举例:
http_flow_limit.go
func HTTPFlowLimitMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
serviceInterface, ok := c.Get("service")
if !ok {
middleware.ResponseError(c, 2001, errors.New("service not fount"))
c.Abort()
return
}
serviceDetail := serviceInterface.(*dao.ServiceDetail)
//服务端限流操作
if serviceDetail.AccessControl.ServiceFlowLimit != 0 {
//1、根据限流器创建返回的对应服务限流器实例
serviceLimiter, err := public.FlowLimiterHandler.GetLimiter(
public.FlowServicePrefix+serviceDetail.Info.ServiceName,
float64(serviceDetail.AccessControl.ServiceFlowLimit),
)
if err != nil {
middleware.ResponseError(c, 5001, err)
}
//2、对应服务限流器实例的请求消费令牌
if !serviceLimiter.Allow() {
middleware.ResponseError(c, 5002, errors.New(fmt.Sprintf("service flow limit %v", serviceDetail.AccessControl.ServiceFlowLimit)))
c.Abort()
return
}
}
if serviceDetail.AccessControl.ClientIPFlowLimit != 0 {
//客户端相同限流操作
//1、创建限流器实例
clientLimit, err := public.FlowLimiterHandler.GetLimiter(
public.FlowServicePrefix+serviceDetail.Info.ServiceName+"_"+c.ClientIP(),
float64(serviceDetail.AccessControl.ClientIPFlowLimit),
)
if err != nil {
middleware.ResponseError(c, 5003, err)
c.Abort()
return
}
//2、请求消费令牌
if !clientLimit.Allow() {
middleware.ResponseError(c, 5002, errors.New(fmt.Sprintf("%v flow limit %v", c.ClientIP(), serviceDetail.AccessControl.ClientIPFlowLimit)))
c.Abort()
return
}
}
c.Next()
}
}
在中间件中获取限流器并消费令牌只有这么几步而已,接下来看public中封装好的限流器获取实例方法
flow_limit_handler.go
这里我们使用单例模式进行封装
具体单例模式的结构已经写了很多遍了,这里就不一一赘述了
直接看向核心方法GetLimiter()
看过上面的time/rate限速器使用和源码分析后,核心代码就很通俗易懂了
根据服务信息中的qps,调用rate的构造方法**rate.NewLimiter(limit,burst)**即可得到限速器实例,limit是服务信息中的QPS,这里设置令牌桶的最大令牌数为QPS*3
然后!就没咯!!!就这么简单
var FlowLimiterHandler *FlowLimiter
type FlowLimiter struct {
FlowLimiterMap map[string]*FlowLimiterItem
FlowLimiterSlice []*FlowLimiterItem
Locker sync.RWMutex
}
type FlowLimiterItem struct {
ServiceName string
Limiter *rate.Limiter
}
func NewFlowLimiter() *FlowLimiter {
return &FlowLimiter{
FlowLimiterMap: map[string]*FlowLimiterItem{},
FlowLimiterSlice: []*FlowLimiterItem{},
Locker: sync.RWMutex{},
}
}
func init() {
FlowLimiterHandler = NewFlowLimiter()
}
func (counter *FlowLimiter) GetLimiter(serviceName string, qps float64) (*rate.Limiter, error) {
for _, item := range counter.FlowLimiterSlice {
if item.ServiceName == serviceName {
return item.Limiter, nil
}
}
//创建限流器(每秒令牌数,桶中最大令牌数)
newLimiter := rate.NewLimiter(rate.Limit(qps), int(qps*3))
item := &FlowLimiterItem{
ServiceName: serviceName,
Limiter: newLimiter,
}
counter.FlowLimiterSlice = append(counter.FlowLimiterSlice, item)
counter.Locker.Lock()
defer counter.Locker.Unlock()
counter.FlowLimiterMap[serviceName] = item
return newLimiter, nil
}