go-zero 断路器

296 阅读4分钟

参考:

为什么需要熔断

微服务集群中,每个应用基本都会依赖一定数量的外部服务。但是随时都可能遇到网络连接慢、超时、依赖服务过载、服务不可用的情况,在高并发场景下如果调用方不做任何处理,继续持续请求故障服务的话很容易引起整个微服务集群雪崩。

缓存雪崩

image.png 上图的数据流程是这样的:1. 从缓存服务拿数据,如果有本地缓存,则返回结果,否则去 Redis 拿数据,如果没有则请求源服务

雪崩发生:

  • Redis集群因为不可抗拒的原因挂掉了
  • 大量的请求到达缓存服务,缓存服务请求 Redis,但是因为 Redis 挂掉了,缓存服务会等待、重试之类,导致延迟增加,比如从 5ms 变为 500ms
  • 这个时候就会出现大量的请求hang住,占用大量的资源
  • 缓存服务就会并发请求源服务,源服务则会请求MySQL,这个时候的并发量可能非常大
  • MySQL会占用大量资源,导致性能急剧下降,最后甚至可能挂掉
  • 然后源服务跟 MySQL 之间的延迟也会变高,然后就会有大量的请求在等待源服务返回结果(hang 住了)
  • 缓存服务大量的资源都耗费在了访问 Redis、源服务无果,最后自己被拖死,无法提供服务
  • 网站崩溃

集群雪崩

image.png

  • 首先,服务的处理能力开始出现过载
  • 然后服务由于资源耗尽而不可用
  • 最后由于服务内部出现严重的过载,导致响应严重超时,服务的调用方同样会出现大量请求的积压使资源耗尽

如果在系统过载的情况下,不进行任何控制,异常情况就会急剧扩散,导致雪崩情况出现。所以,想要避免系统雪崩,要么通过快速减少系统负载,即熔断、降级、限流等快速失败和降级机制;要么通过快速增加系统的服务能力来避免雪崩的发生,即弹性扩容机制。

利用熔断避免雪崩

image.png

熔断器一般有三个状态:

  • 关闭

    • 默认状态,请求能被到达目标服务,同时统计在窗口时间成功和失败次数,如果达到错误率阈值,则会进入断开状态
  • 断开

    • 在该状态下,发起请求时会立即返回错误,也可以返回一个降级的结果
  • 半断开

    • 进行断开状态会维护一个超时时间,到达超时时间开始进入 半断开 状态,尝试允许一部分请求正常通过并统计成功数量,如果请求正常则认为此时目标服务已恢复进入 关闭 状态,否则进入 断开 状态。半断开 状态存在的目的在于实现了自我修复,同时防止正在恢复的服务再次被大量打垮。

自适应熔断

这里就可以直接看作者自己的分析的源码了,不献丑了 juejin.cn/post/703099…

补充一下,这里列一下demo的逻辑注释


const (
   // 服务的运行时间
   duration = time.Minute * 5

   // 休息状态时的随机数范围
   breakRange = 20

   // 工作状态时的随机数范围
   workRange = 50

   // 请求的时间间隔,设置为毫秒级。
   requestInterval = time.Millisecond

   // 状态因子,用于在绘图中使状态更可见。
   stateFator = float64(time.Second/requestInterval) / 2
)

type (
   // 服务器结构体,包含一个状态标志。
   server struct {
      state int32
   }

   // 计量结构体,记录调用次数。
   metric struct {
      calls int64
   }
)

func (m *metric) addCall() {
   atomic.AddInt64(&m.calls, 1)
}

func (m *metric) reset() int64 {
   return atomic.SwapInt64(&m.calls, 0)
}

func newServer() *server {
   return &server{}
}

func (s *server) serve(m *metric) bool {
   m.addCall()
   return atomic.LoadInt32(&s.state) == 1
}

// 启动一个goroutine,该goroutine会定期改变服务器的状态。
func (s *server) start() {
   go func() {
      r := rand.New(rand.NewSource(time.Now().UnixNano()))
      var state int32
      for {
         var v int32
         if state == 0 {
            v = r.Int31n(breakRange)
         } else {
            v = r.Int31n(workRange)
         }
         time.Sleep(time.Second * time.Duration(v+1))
         state ^= 1
         atomic.StoreInt32(&s.state, state)
      }
   }()
}

// 接受一个服务器实例、断路器实例、运行时间、以及一个计量实例。
// 定期尝试执行断路器的任务,如果服务器服务可用,则执行任务;否则,返回breaker.ErrServiceUnavailable。
// 在一段时间(duration)后停止任务。
func runBreaker(s *server, br breaker.Breaker, duration time.Duration, m *metric) {
   ticker := time.NewTicker(requestInterval)
   defer ticker.Stop()
   done := make(chan lang.PlaceholderType)

   go func() {
      time.Sleep(duration)
      close(done)
   }()

   for {
      select {
      case <-ticker.C:
         _ = br.Do(func() error {
            if s.serve(m) {
               return nil
            } else {
               return breaker.ErrServiceUnavailable
            }
         })
      case <-done:
         return
      }
   }
}

func main() {
   srv := newServer()
   srv.start()

   gb := breaker.NewBreaker()
   fp, err := os.Create("result.csv")
   logx.Must(err)
   defer fp.Close()
   fmt.Fprintln(fp, "seconds,state,googleCalls,netflixCalls")

   var gm, nm metric
   go func() {
      // 用于定期记录服务器状态和调用次数到CSV文件
      ticker := time.NewTicker(time.Second)
      defer ticker.Stop()
      var seconds int
      for range ticker.C {
         seconds++
         gcalls := gm.reset()
         ncalls := nm.reset()
         fmt.Fprintf(fp, "%d,%.2f,%d,%d\n",
            seconds, float64(atomic.LoadInt32(&srv.state))*stateFator, gcalls, ncalls)
      }
   }()

   var waitGroup sync.WaitGroup
   waitGroup.Add(1)
   go func() {
      // 运行断路器任务
      runBreaker(srv, gb, duration, &gm)
      waitGroup.Done()
   }()

   go func() {
      // 进度条
      bar := pb.New(int(duration / time.Second)).Start()
      ticker := time.NewTicker(time.Second)
      defer ticker.Stop()
      for range ticker.C {
         bar.Increment()
      }
      bar.Finish()
   }()

   waitGroup.Wait()
}