B站开源微服务限流,熔断

800 阅读17分钟

限流部分

对象

一般是服务提供方做限流,防止异常流量把服务打挂

原因

如果某接口请求量激增,服务机器资源耗尽,影响其他接口,恶性循环

场景

全局限流
单接口限流
调用方限流
接口+调用方限流
接口参数限流

粒度

一般微服务都做了负载均衡,所以单实例维度进行限流也是可以的,B栈的限流就是单实例限流

下面介绍b站目前两种限流器,一个是bbr,一个是令牌桶限流器 \

bbr

首先是bbr限流,采用滑动窗口统计数据,通过综合分析服务的 cpu 使用率、请求通过的 qps 和请求成功的 rt 来做自适应限流保护;针对的场景理论不限制,因为存储的是一个key,key可以按照接口,调用方等维度来传,不过在kratos里面,B栈的HTTP、grpc接口都是用的路径做的key \

下面介绍几个用到的包
git.bilibili.co/platform/go-common/library/container/group/group.go

Group结构体是一个懒加载的容器,容器内存储数据的是一个sync.Map结构,键值对中的value通过结构体的new方法生成,用一个读写锁保证new的读写安全,Group支持初始化,获取,重置,清除操作,之所以说是懒加载具体体现在没有set操作,get的时候如果不存在通过new生成一个value,这样的一个懒加载的工具会被作为 承载一组限流器的容器

// Package group provides a sample lazy load container.
// The group only creating a new object not until the object is needed by user.
// And it will cache all the objects to reduce the creation of object.
package group

import "sync"

// Group is a lazy load container.
type Group struct {
   new  func() interface{} //方法生成map的value
   objs sync.Map //存储数据
   sync.RWMutex //读写锁保证new的读写安全
}

// NewGroup news a group container.
初始的时候没有数据,需要赋值new方法
func NewGroup(new func() interface{}) *Group {
   if new == nil {
      panic("container.group: can't assign a nil to the new function")
   }
   return &Group{
      new: new,
   }
}

// Get gets the object by the given key. 
func (g *Group) Get(key string) interface{} {
   g.RLock()//读锁 安全取出new方法
   new := g.new
   g.RUnlock()
   obj, ok := g.objs.Load(key) //查询key是否存在
   if !ok {
      obj = new() //不存在通过方法生成对应value 并保存
      g.objs.Store(key, obj)
   }
   return obj
}

// Reset resets the new function and deletes all existing objects.
func (g *Group) Reset(new func() interface{}) {
   if new == nil {
      panic("container.group: can't assign a nil to the new function")
   }
   g.Lock()
   g.new = new //写锁 更新new方法 
   g.Unlock()
   g.Clear() //并且 删除所有map中存储元素
}

// Clear deletes all objects.
func (g *Group) Clear() {
   g.objs.Range(func(key, value interface{}) bool {
      g.objs.Delete(key)
      return true
   })
}

可以看出Group里面可以存储键值对,所以可以用来保存 同一空间下针对key的限流器
下面要介绍的是bbr包,里面是限流器的具体实现,BBR实现了一个限流器,用上面介绍的Group可以承载一组限流器

package bbr

import (
   "context"
   "math"
   "sync"
   "sync/atomic"
   "time"

   "go-common/library/container/group"
   "go-common/library/ecode"
   "go-common/library/log"
   "go-common/library/rate/limit"
   "go-common/library/stat/metric"
   cpustat "go-common/library/stat/sys/cpu"
)

var (
   cpu         int64
   decay       = 0.95
   //限流器的默认配置
   defaultConf = &Config{ 
      Window:       time.Second * 10,
      WinBucket:    100,
      CPUThreshold: 800,
   }
   cpuProcOnce sync.Once
)

type cpuGetter func() int64
//开启定时job 获取当前CPU
func startCPUProc() {
   cpustat.Init()
   go cpuproc()
}

func min(l, r uint64) uint64 {
   if l < r {
      return l
   }
   return r
}

// cpu = cpuᵗ⁻¹ * decay + cpuᵗ * (1 - decay)
func cpuproc() {
   defer func() {
      if err := recover(); err != nil {
         log.Error("rate.limit.cpuproc() err(%+v)", err)
         //可以看出即使panic了,再起一个协程仍然继续获取CPU,可以学习这种job处理方式
         go cpuproc() 
      }
   }()
   ticker := time.NewTicker(time.Millisecond * 250)
   // EMA algorithm: https://blog.csdn.net/m0_38106113/article/details/81542863
   for range ticker.C {
      stat := &cpustat.Stat{}
      cpustat.ReadStat(stat)
      stat.Usage = min(stat.Usage, 1000)
      prevCPU := atomic.LoadInt64(&cpu)
      // 涉及深度学习里面的某种算法,公式在方法上一行注释
      curCPU := int64(float64(prevCPU)*decay + float64(stat.Usage)*(1.0-decay))
      atomic.StoreInt64(&cpu, curCPU)
   }
}

// Stat contains the metrics's snapshot of bbr.
type Stat struct {
   CPU         int64
   InFlight    int64
   MaxInFlight int64
   MinRt       int64
   MaxPass     int64
}

// BBR implements bbr-like limiter.
// It is inspired by sentinel.
// https://github.com/alibaba/Sentinel/wiki/%E7%B3%BB%E7%BB%9F%E8%87%AA%E9%80%82%E5%BA%94%E9%99%90%E6%B5%81
type BBR struct {
   cpu             cpuGetter
   passStat        metric.RollingCounter //统计请求通过数
   rtStat          metric.RollingCounter //统计接口响应时间
   inFlight        int64
   winBucketPerSec int64 // time.Second / conf.Window / conf.WinBucket,WinBucket表示一个Window包括多少个Bucket
   conf            *Config  //限流器配置
   prevDrop        atomic.Value  //上一次丢掉请求的时间
}

// Config contains configs of bbr limiter. 限流器的配置
type Config struct {
   Enabled      bool
   Window       time.Duration
   WinBucket    int
   Rule         string
   Debug        bool
   CPUThreshold int64
}

//当前Window中统计每个Bucket中的请求未被限流的个数,取最大值
func (l *BBR) maxPASS() int64 {
   val := int64(l.passStat.Reduce(func(iterator metric.Iterator) float64 {
      var result = 1.0
      for iterator.Next() { //对bucket循环
         bucket := iterator.Bucket() //取出bucket
         count := 0.0
         for _, p := range bucket.Points { //对bucket内 通过请求 求和
            count += p
         }
         result = math.Max(result, count) //取最大值
      }
      return result
   }))
   if val == 0 {
      return 1
   }
   return val
}

//当前Window中统计每个Bucket中的请求平均响应时间,取最小值
func (l *BBR) minRT() int64 {
   val := l.rtStat.Reduce(func(iterator metric.Iterator) float64 {
      var result = math.MaxFloat64
      for iterator.Next() {
         bucket := iterator.Bucket()
         if len(bucket.Points) == 0 {
            continue
         }
         total := 0.0
         for _, p := range bucket.Points {
            total += p
         }
         avg := total / float64(bucket.Count) //应该是总响应时间/响应个数
         result = math.Min(result, avg)
      }
      return result
   })
   return int64(math.Ceil(val))
}

func (l *BBR) maxFlight() int64 {
   return int64(math.Floor(float64(l.maxPASS()*l.minRT()*l.winBucketPerSec)/1000.0 + 0.5))
}

//判断是否丢掉这次请求 判断cpu 上次限流时间 当前系统容量
func (l *BBR) shouldDrop() bool {
   if l.cpu() < l.conf.CPUThreshold {
      prevDrop, ok := l.prevDrop.Load().(time.Time)
      if !ok {
         return false
      }
      if time.Since(prevDrop) <= time.Second {
         inFlight := atomic.LoadInt64(&l.inFlight)
         return inFlight > 1 && inFlight > l.maxFlight()
      }
      return false
   }
   inFlight := atomic.LoadInt64(&l.inFlight)
   drop := inFlight > 1 && inFlight > l.maxFlight()
   if drop {
      l.prevDrop.Store(time.Now())
   }
   return drop
}

// 方法判断是否限流
func (l *BBR) Allow(ctx context.Context, opts ...limit.AllowOption) (func(info limit.DoneInfo), error) {
   allowOpts := limit.DefaultAllowOpts()
   for _, opt := range opts {
      opt.Apply(&allowOpts)
   }
   if l.shouldDrop() {
      return nil, ecode.LimitExceed //返回限流错误码
   }
   atomic.AddInt64(&l.inFlight, 1)
   stime := time.Now()
   return func(do limit.DoneInfo) {
      rt := int64(math.Ceil(float64(time.Since(stime)) / float64(time.Millisecond)))
      l.rtStat.Add(rt)
      atomic.AddInt64(&l.inFlight, -1)
      switch do.Op {
      case limit.Success:
         l.passStat.Add(1) //没有限流 就通过统计+1
         return
      default:
         return
      }
   }, nil
}

//返回一个接口Limiter,实际动态类型是实现了接口的bbr结构体
这个方法作为group里面的new方法返回key对应的限制器的作用,可以看下面的NewGroup方法
func newLimiter(conf *Config) limit.Limiter {
   if conf == nil {
      conf = defaultConf
   }
   size := conf.WinBucket
   bucketDuration := conf.Window / time.Duration(conf.WinBucket)
   passStat := metric.NewRollingCounter(metric.RollingCounterOpts{Size: size, BucketDuration: bucketDuration})
   rtStat := metric.NewRollingCounter(metric.RollingCounterOpts{Size: size, BucketDuration: bucketDuration})
   cpu := func() int64 {
      return atomic.LoadInt64(&cpu)
   }
   limiter := &BBR{
      cpu:             cpu,
      conf:            conf,
      passStat:        passStat,
      rtStat:          rtStat,
      winBucketPerSec: int64(time.Second) / (int64(conf.Window) / int64(conf.WinBucket)),
   }
   return limiter
}


//Group用来装在一组bbr限制器,里面是上面介绍的懒加载的Group结构体
type Group struct {
   group *group.Group
}

func NewGroup(conf *Config) *Group {
   cpuProcOnce.Do(startCPUProc) //先开启CPU加载的job,限制器工作的时候需要
   if conf == nil {
      conf = defaultConf
   }
   group := group.NewGroup(func() interface{} {
      return newLimiter(conf)  //给Group new方法 ,方法返回一个限制器
   })
   return &Group{
      group: group,
   }
}

//group有get方法,内部就是调用结构体Group的get方法获取限制器
func (g *Group) Get(key string) limit.Limiter {
   limiter := g.group.Get(key)
   return limiter.(limit.Limiter)
}

接下来就在 HTTP中注入限流器的中间件
结构体RateLimiter 里面包含了一个bbr.Group,通过NewRateLimiter方法 初始化,最重要的是Limit()方法返回一个中间件handler,返回的方法里面调用了路径对应的限流器的allow方法判断是否限流
git.bilibili.co/platform/go-common/library/net/http/blademaster/ratelimit.go

package blademaster

import (
   "sync/atomic"
   "time"

   "go-common/library/log"
   "go-common/library/rate/limit"
   "go-common/library/rate/limit/bbr"
   "go-common/library/stat/metric"
)

var (
   _metricServerBBR = metric.NewCounterVec(&metric.CounterVecOpts{
      Namespace: serverNamespace,
      Subsystem: "",
      Name:      "bbr_total",
      Help:      "http server bbr total.",
      Labels:    []string{"url"},
   })
)

// RateLimiter bbr middleware.
type RateLimiter struct {
   group   *bbr.Group
   logTime int64
}

// New return a ratelimit middleware.
func NewRateLimiter(conf *bbr.Config) (s *RateLimiter) {
   return &RateLimiter{
      group:   bbr.NewGroup(conf),
      logTime: time.Now().UnixNano(),
   }
}


// Limit return a bm handler func.
func (b *RateLimiter) Limit() HandlerFunc {
   return func(c *Context) {
      limiter := b.group.Get(c.RoutePath) //针对路由路径做限流
      done, err := limiter.Allow(c) //调用allow方法判断是否限流
      b.printStats(c.RoutePath, limiter, err == nil)
      if err != nil {
         routePath := c.RoutePath
         if routePath == "" {
            routePath = "/"
         }
         _metricServerBBR.Inc(routePath[1:])
         c.JSON(nil, err)
         c.Abort()
         return
      }
      defer func() {
         done(limit.DoneInfo{Op: limit.Success})
      }()
      c.Next()
   }
}

在HTTP的newServer方法里面将限流器加入到了中间件中 git.bilibili.co/platform/go-common/library/net/http/blademaster/server.go

// DefaultServer returns an Engine instance with the Recovery, Logger and CSRF middleware already attached.
func DefaultServer(conf *ServerConfig, options ...ServerOption) *Engine {
   engine := NewServer(conf, options...)
   engine.Use(Recovery(), Trace(), Logger(), CSRF(), Mobile(), AuroraHandler())
   if !_httpDisableRateLimit {
      engine.Use(NewRateLimiter(nil).Limit()) //返回handler,里面是限流逻辑
   }
   engine.Use(Mirror())
   log.Info("blademaster: rate limit is enabled: %t", !_httpDisableRateLimit)
   return engine
}

grpc 的限流,可以看到RateLimiter结构体里面也是有bbr.Group 来做限流逻辑,它的 Limit() 方法也是返回grpc的一个截断器,截断器里面的逻辑执行了对应url的限流器的allow方法判断是否需要限流
git.bilibili.co/platform/go-common/library/net/rpc/warden/ratelimiter/bbr/ratelimiter.go

package bbr

import (
   "context"
   "sync/atomic"
   "time"

   "go-common/library/log"
   "go-common/library/rate/limit"
   "go-common/library/rate/limit/bbr"
   "go-common/library/stat/metric"
   "google.golang.org/grpc"
)

var (
   _metricServerBBR = metric.NewCounterVec(&metric.CounterVecOpts{
      Namespace: "grpc_server",
      Subsystem: "",
      Name:      "bbr_total",
      Help:      "grpc server bbr total.",
      Labels:    []string{"url"},
   })
)

// RateLimiter bbr middleware.
type RateLimiter struct {
   group   *bbr.Group
   logTime int64
}

// New return a ratelimit middleware.
func New(conf *bbr.Config) (s *RateLimiter) {
   return &RateLimiter{
      group:   bbr.NewGroup(conf),
      logTime: time.Now().UnixNano(),
   }
}

// Limit is a server interceptor that detects and rejects overloaded traffic.
func (b *RateLimiter) Limit() grpc.UnaryServerInterceptor {
   return func(ctx context.Context, req interface{}, args *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
      uri := args.FullMethod
      limiter := b.group.Get(uri) //grpc的请求方法路径作为key做限流
      done, err := limiter.Allow(ctx)  //是否限流,
      b.printStats(uri, limiter, err == nil)
      if err != nil { //触发限流
         _metricServerBBR.Inc(uri)
         return
      }
      defer func() {
         done(limit.DoneInfo{Op: limit.Success})
      }()
      resp, err = handler(ctx, req)
      return
   }
}

在grpc 的newServer 方法中,添加了实现限流逻辑的拦截器 git.bilibili.co/platform/go-common/library/net/rpc/warden/server.go

// NewServer returns a new blank Server instance with a default server interceptor.
func NewServer(conf *ServerConfig, opt ...grpc.ServerOption) (s *Server) {
   if conf == nil {
     
   s = new(Server)
   ......
   opt = append(opt, keepParam, grpc.UnaryInterceptor(s.interceptor))
   s.server = grpc.NewServer(opt...)
 
   if !_grpcDisableRateLimit {
      s.Use(bbr.New(nil).Limit())  //这里添加了限流器功能的拦截器
   }
   log.Info("warden: rate limit is enabled: %t", !_grpcDisableRateLimit)
   s.Use(mirrorhandler.WardenServerInterceptor())
   s.Use(auroragrpc.ServerInterceptor())
   return
}

这是HTTP的限流器实现大概图解,可以看出最外层是ratelimiter结构,里面有Group结构,group里面有键值对承载一组限流器,限流器有allow方法处理限流逻辑

image.png

HTTP 和 grpc 里面限流结构体是一样的,对比代码很多东西是相同的

image.png 但是limit返回的不一样,grpc是截断器 HTTP返回的是中间件

image.png

总结

其实bbr也可以实现 接口,调用方或者其他维度的限流,这个都是可以扩展的,但是在kratos上层 的http ,grpc应用都是只采用了针对接口的限流,不同接口在数据上区别体现在于每一个bbr结构底层参数都不一样,性能比较好的滑动窗口里面统计的响应时间更短,处理更快,在执行接口对应的allow方法中计算公式的参数值是不一样的

令牌桶

也是单实例限流组件,针对rule对应的key限制频次,应该是不考虑cpu等参数的,单纯的限制QPS
git.bilibili.co/platform/go-common/library/net/rpc/warden/ratelimiter/tokenbucket/limiter.go \

在服务newserver的时候使用下面包提供的方法返回相关中间件 new的时候传入配置,配置里面说明限制类型和限制针对的key,阈值等

// Limiter controls how frequently events are allowed to happen.
type Limiter struct {
   rbs map[string]*ruleBucket
}

// Config limitter  conf.
type Config []*Limit

// Limit limit conf.
type Limit struct {
   Rule  string //限制类型
   Key   string //针对的key 比如接口名
   Limit rate.Limit
   Burst int
}

// New return Limiter.  自已自定义rule传入
func New(conf Config, rs ...Rule) (l *Limiter) {
   // 默认内置 rule
   rs = append(rs, RuleCaller{}, RuleMethod{}, RuleMethodCaller{})

   rbs := make(map[string]*ruleBucket, len(rs))
   for _, r := range rs {
      if _, ok := rbs[r.Name()]; ok {
         continue
      }

      rb := newRuleBucket(r)
      rbs[r.Name()] = rb
   }

   l = &Limiter{rbs}
   l.Reload(conf)
   return
}
// Limit is a server interceptor that detects and rejects overloaded traffic.
func (l *Limiter) Limit() grpc.UnaryServerInterceptor {
   return func(ctx context.Context, req interface{}, args *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
      ok, name, key := l.Allow(ctx, req, args)
      if !ok {
         _metricServerTB.Inc(name, key)
         err = ecode.LimitExceed
         log.Warn("warden tokenbucket limit name(%s) key(%s)", name, key)
         return
      }
      resp, err = handler(ctx, req)
      return
   }
}
func (rb *ruleBucket) Reload(conf Config) {
   olm, _ := rb.limMap.Load().(limiterMap)

   lm := make(limiterMap, len(conf))
   for _, v := range conf {
      k := v.Key

      if l, ok := olm[k]; ok && l.Limit() == v.Limit && l.Burst() == v.Burst {
         lm[k] = l
      } else {
         lm[k] = rate.NewLimiter(v.fix())
      }
   }

   rb.limMap.Store(lm)
}

具体使用如下

// New new grpc server
func New(svc *service.Service, rateConf rate.Config) (wsvr *warden.Server, err error) {


   rm := rate.New(rateConf)
   wsvr.Use(rm.Limit()) //use中间件

   // quota
   quotaLimit := quota.New(conf.Conf.Quota)
   wsvr.Use(quotaLimit.Limit())
//这里注册服务逻辑省略
   if wsvr, err = wsvr.Start(); err != nil {
      return
   }
   return
}

配置示例如下

`[[RateLimit]]`

`rule = ``"method_caller"`

`key = ``"/live.x.v1.Guard/Get|live.live.test"`  `# 用 | 分隔 method 和 caller`

`limit = 1000.0  ``# 注意一定要有小数点`

`burst = 1000`

 

`[[RateLimit]]`

`rule = ``"caller"`

`key = ``"live.live.test"`

`limit = 1000.0`

`burst = 1000`

 

`[[RateLimit]]`

`rule = ``"method"`

`key = ``"/live.x.v1.Guard/Get"`

`limit = 1000.0`

`burst = 1000`

可以看出一共提供三种限流,针对方法的,方法加caller的,caller的,这三种分别提供了三种结构体,提供LimitKey方法来解析请求参数中对应的key

// RuleCaller 以 caller 限流的规则
type RuleCaller struct{}

// Name 规则名
func (r RuleCaller) Name() string {
   return "caller"
}

// LimitKey 组合 key
func (r RuleCaller) LimitKey(ctx context.Context, req interface{}, args *grpc.UnaryServerInfo) string {
   caller := metadata.String(ctx, metadata.Caller)
   if caller == "" {
      caller = "no_user"
   }
   return caller
}

// RuleMethod 以 method 限流的规则
type RuleMethod struct{}

// Name 规则名
func (r RuleMethod) Name() string {
   return "method"
}

// LimitKey 组合 key
func (r RuleMethod) LimitKey(ctx context.Context, req interface{}, args *grpc.UnaryServerInfo) string {
   return args.FullMethod
}

// RuleMethodCaller 组合 method caller 限流的规则
type RuleMethodCaller struct {
   rc RuleCaller
   rm RuleMethod
}

// Name 规则名
func (r RuleMethodCaller) Name() string {
   return "method_caller"
}

// LimitKey 组合 key
func (r RuleMethodCaller) LimitKey(ctx context.Context, req interface{}, args *grpc.UnaryServerInfo) string {
   rmKey := r.rm.LimitKey(ctx, req, args)
   if rmKey == "" {
      return ""
   }

   return fmt.Sprintf("%s|%s", rmKey, r.rc.LimitKey(ctx, req, args))
}

最终找到key对应的limiter来判断是否限流

参数粒度的限流,用自定义rule

// Rule 定义限流规则
type Rule interface {
   // 返回规则名
   Name() string
   // 从请求中抽取 limiterMap 的 key,抽不出返回空字符串
   LimitKey(ctx context.Context, req interface{}, args *grpc.UnaryServerInfo) string
}

rule是一个接口,上面三个也是实现了这个接口 从而进行三种方式的限流,所以自定义一个类型的限流只要满足接口即可,这种能力下我们可以实现更细粒度的限流,比如某个接口的请求参数特定值来限流(比如接口请求传递了请求来源参数,我想要限制来源是1的请求),在LimitKey方法中通过类型断言提取出来源参数的值返回,配置文件里面的key填想要限流的参数,如果提取值和配置的一致就能拿到对应的限流器,就可以进行参数特定值粒度的限流了,这个粒度真的很细很定制化了

熔断部分

对象

可以是调用方也可以是服务方,调用方发现下游有异常节点,熔断防止雪崩,采用降级逻辑

原因

可以下游节点异常,继续调用可以回引起雪崩

场景

接口熔断

目前熔断是根据 请求的成功率来判断熔断与否
目前框架中集成的熔断有http熔断,grpc熔断,db熔断这3种,HTTP熔断器的粒度是每个URL一个熔断器;grpc熔断器的粒度是每个method一个熔断器;db熔断器的粒度是每个Addr一个熔断器,该Addr是通过DSN解析出来的Addr;用户可以自定义熔断器 目前kratos支持熔断的能力,用户可以自定义熔断器

git.bilibili.co/platform/go-common/library/net/netutil/breaker/breaker.go

这里也用到了上面说的懒加载容器Group来承载一组熔断器,NewGroup方法里面给group结构体添加了new方法,可以返回一个熔断器实例,newBreaker方法返回一个实现了接口breaker的结构体实例,这个实例具体实现了熔断逻辑,这里面的方法和上面差不多,不累述

package breaker

import (
   "sync"
   "time"

   "go-common/library/container/group"
   xtime "go-common/library/time"
)

// Config broker config.
type Config struct {
   SwitchOff bool // breaker switch,default off.

   // Hystrix
   Ratio float32
   Sleep xtime.Duration

   // Google
   K float64

   Window  xtime.Duration
   Bucket  int
   Request int64
}


// Breaker is a CircuitBreaker pattern.
// FIXME on int32 atomic.LoadInt32(&b.on) == _switchOn
type Breaker interface {
   Allow() error
   MarkSuccess()
   MarkFailed()
}

type Group struct {
   group *group.Group
}

const (
   // StateOpen when circuit breaker open, request not allowed, after sleep
   // some duration, allow one single request for testing the health, if ok
   // then state reset to closed, if not continue the step.
   StateOpen int32 = iota
   // StateClosed when circuit breaker closed, request allowed, the breaker
   // calc the succeed ratio, if request num greater request setting and
   // ratio lower than the setting ratio, then reset state to open.
   StateClosed
   // StateHalfopen when circuit breaker open, after slepp some duration, allow
   // one request, but not state closed.
   StateHalfopen

   //_switchOn int32 = iota
   // _switchOff
)

var (
   _mu   sync.RWMutex
   _conf = &Config{
      Window:  xtime.Duration(3 * time.Second),
      Bucket:  10,
      Request: 100,

      Sleep: xtime.Duration(500 * time.Millisecond),
      Ratio: 0.5,
      // Percentage of failures must be lower than 33.33%
      K: 1.5,

      // Pattern: "",
   }
   _group = NewGroup(_conf)
)


// newBreaker new a breaker.
func newBreaker(c *Config) (b Breaker) {
   // factory
   return newSRE(c)
}

func NewGroup(conf *Config) *Group {
   if conf == nil {
      conf = _conf
   } else {
      conf.fix()
   }
   group := group.NewGroup(func() interface{} {
      return newBreaker(conf)
   })
   return &Group{
      group: group,
   }
}

// Get get a breaker by a specified key, if breaker not exists then make a new one.
func (g *Group) Get(key string) Breaker {
   brk := g.group.Get(key)
   return brk.(Breaker)
}

// Reload reload the group by specified config, this may let all inner breaker
// reset to a new one.
func (g *Group) Reload(conf *Config) {
   if conf == nil {
      return
   }
   conf.fix()
   g.group.Reset(func() interface{} {
      return newBreaker(conf)
   })
}

具体实现breaker接口的结构体在 git.bilibili.co/platform/go-common/library/net/netutil/breaker/sre_breaker.go
sreBreaker结构体实现了上面的Breaker接口,他的allow 方法里面有具体的处理熔断逻辑, newSRE 方法返回一个动态类型是sreBreaker 的Breaker接口,重点在allow里面的逻辑

package breaker

import (
   "math"
   "math/rand"
   "sync"
   "sync/atomic"
   "time"

   "go-common/library/ecode"
   "go-common/library/log"
   "go-common/library/stat/metric"
)

// sreBreaker is a sre CircuitBreaker pattern.
type sreBreaker struct {
   stat metric.RollingCounter

   k       float64
   request int64

   state int32
   r     *rand.Rand
   rLock sync.Mutex
}

func newSRE(c *Config) Breaker {
   counterOpts := metric.RollingCounterOpts{
      Size:           c.Bucket,
      BucketDuration: time.Duration(int64(c.Window) / int64(c.Bucket)),
   }
   stat := metric.NewRollingCounter(counterOpts)
   return &sreBreaker{
      stat: stat,
      r:    rand.New(rand.NewSource(time.Now().UnixNano())),

      request: c.Request,
      k:       c.K,
      state:   StateClosed,
   }
}

func (b *sreBreaker) summary() (success int64, total int64) {
   b.stat.Reduce(func(iterator metric.Iterator) float64 {
      for iterator.Next() {
         bucket := iterator.Bucket()
         total += bucket.Count
         for _, p := range bucket.Points {
            success += int64(p)
         }
      }
      return 0
   })
   return
}

func (b *sreBreaker) Allow() error {
   success, total := b.summary()
   k := b.k * float64(success)
   if log.V(5) {
      log.Info("breaker: request: %d, succee: %d, fail: %d", total, success, total-success)
   }
   // check overflow requests = K * success 
   请求总数小于一定阈值 或者成功率大于某个阈值都不会熔断
   if total < b.request || float64(total) < k {
   //如果现在开关是开的,说明不允许请求通过,现在就要修改为关闭,允许通过
      if atomic.LoadInt32(&b.state) == StateOpen {
         atomic.CompareAndSwapInt32(&b.state, StateOpen, StateClosed)
      }
      return nil
   }
   //现在情况不允许请求通过,需要把开关打开
   if atomic.LoadInt32(&b.state) == StateClosed {
      atomic.CompareAndSwapInt32(&b.state, StateClosed, StateOpen)
   }
   dr := math.Max(0, (float64(total)-k)/float64(total+1))
   drop := b.trueOnProba(dr)
   if log.V(5) {
      log.Info("breaker: drop ratio: %f, drop: %v", dr, drop)
   }
   if drop {
      return ecode.ServiceUnavailable
   }
   //即使超过了阈值,通过上面的trueOnProba方法 还是会有一部分请求到这里,正常情况而不是返回熔断错误
   return nil
}

//应该是在请求结果返回的时候调用下面这两个函数,来统计成功和失败的情况
func (b *sreBreaker) MarkSuccess() {
   b.stat.Add(1)
}

func (b *sreBreaker) MarkFailed() {
   // NOTE: when client reject requets locally, continue add counter let the
   // drop ratio higher.
   b.stat.Add(0)
}

func (b *sreBreaker) trueOnProba(proba float64) (truth bool) {
   b.rLock.Lock()
   truth = b.r.Float64() < proba
   b.rLock.Unlock()
   return
}

熔断的具体使用是在 数据库连接有用到

// NewMySQL new db and retry connection when has error.
func NewMySQL(c *Config) (db *DB) {
   if c.QueryTimeout == 0 || c.ExecTimeout == 0 || c.TranTimeout == 0 {
      panic("mysql must be set query/execute/transction timeout")
   }
   db, err := Open(c)
   if err != nil {
      log.Error("open mysql error(%v)", err)
      panic(err)
   }
   return
}
// Open opens a database specified by its database driver name and a
// driver-specific data source name, usually consisting of at least a database
// name and connection information.
func Open(c *Config) (*DB, error) {
   if c.SlowLog == 0 {
      c.SlowLog = xtime.Duration(_slowLogDuration)
   }
   db := new(DB)
   d, err := connect(c, c.DSN)
   if err != nil {
      return nil, err
   }
   addr := parseDSNAddr(c.DSN)
   brkGroup := breaker.NewGroup(c.Breaker)
   brk := brkGroup.Get(addr) //给写连接 设置熔断器
   w := &conn{DB: d, breaker: brk, conf: c, addr: addr}
   rs := make([]*conn, 0, len(c.ReadDSN))
   for _, rd := range c.ReadDSN {
      d, err := connect(c, rd)
      if err != nil {
         return nil, err
      }
      addr = parseDSNAddr(rd)
      brk := brkGroup.Get(addr) //给每一个读连接也设置了熔断器
      r := &conn{DB: d, breaker: brk, conf: c, addr: addr}
      rs = append(rs, r)
   }
   db.write = w
   db.read = rs
   db.master = &DB{write: db.write}
   return db, nil
}

grpc的newclient里面也设置了熔断器

// NewClient xroom grpc client
func NewClient(cfg *warden.ClientConfig, opts ...grpc.DialOption) (*Client, error) {
   ...
   client := warden.NewClient(cfg, opts...)
   conn, err := client.Dial(context.Background(), "discovery://default/"+AppID)
   if err != nil {
      return nil, err
   }
   cli := &Client{}
   return cli, nil
}
上面的warden.NewClient 实现方法
func NewClient(conf *ClientConfig, opt ...grpc.DialOption) *Client {
   resolver.Register(discovery.Builder())
   c := new(Client)
   if err := c.SetConfig(conf); err != nil {
      panic(err)
   }
   return c
}
上面的c.SetConfig(conf) 实现
func (c *Client) SetConfig(conf *ClientConfig) (err error) {
  ...
   //这里设置了一个熔断器
   if c.breaker == nil {
      c.breaker = breaker.NewGroup(conf.Breaker)
   } else {
      c.breaker.Reload(conf.Breaker)  
   }
   c.mutex.Unlock()
   return nil
}

参考限流

https://github.com/alibaba/Sentinel/wiki/%E7%B3%BB%E7%BB%9F%E8%87%AA%E9%80%82%E5%BA%94%E9%99%90%E6%B5%81

限流的一些算法

一年后追加: 感觉下面解释不清楚,看这片文章

zhuanlan.zhihu.com/p/228412634

计数器算法

-种比较简单的限流实现算法,在指定周期内累加访问次数,当访问次数达到设定的阈值时,触发限流策略,当进入下一个时间周期时进行访问次数的清零。

image.png

滑动窗口算法

滑动窗口算法的原理是在固定窗口中分割出多个小时间窗口,分别在每个小时间窗口中记录访问次数,然后根据时间将窗口往前滑动并删除过期的小时间窗口。最终只需要统计滑动窗口范围内的所有小时间窗口总的计数即可

image.png

令牌桶限流算法

image.png

也是网络中流量整形或速率限制常用的一种算法。令牌桶算法以一个恒定的速率向桶里放入令牌,如果有新的请求进来希望进行处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。令牌桶算法限制的是流入速率(漏桶是流出速率),允许一定规模的突增流量,最大速率受限于桶的容量和令牌生成速率。可以支持一定程度的突发流量,更适合具有突发特性的流量(这个可以对比漏桶限流,速率是固定的就不适合突发,突发来了可能会被卡住)

漏桶限流算法

image.png

桶以固定的速率流出流量,无论水龙头进入的流量是多少,都不改变流出速率。上图中,水龙头处存在突发流量,一共进入 30Mb 数据,分布不均匀,对系统有冲击。经过漏桶算法处理,漏桶以 3 Mbps 速率持续流出数据,为系统做了很好的缓冲。

漏桶算法中,如果桶未满,可以持续接收流量;如果桶已满,流量溢出,后续的流量将无法入桶,会被丢弃。漏桶算法限制的是流出速率,无论流入是多少,都能保证后续系统的请求是平稳的。后续系统感知的流量相对固定,但可能在系统仍有能力处理更多流量的时候,也会被漏桶限制住

是网络中流量整形(Traffic Shaping)和速率限制(Rate Limiting)常用的一种算法。漏桶算法调控了访问流量,使得突发流量可以被整形、去毛刺,为系统提供稳定的访问流量