图解 Go 微服务熔断器

共 7892字,需浏览 16分钟

 ·

2021-08-10 00:33

  1. 原文中 Circuit Breakers 这里翻译为了熔断器,大家也可以理解叫为断路器

  2. fallback本文翻译为兜底,大家也可以理解为容错补救。

本文翻译自https://ioshellboy.medium.com/circuit-breakers-in-golang-1779da9b001,由于本人翻译水平有限,翻译不当之处烦请指出。希望大家看了这篇文章能有所帮助。感谢捧场。

什么是熔断器

当你看到 “熔断器” 这个术语时,你会想到什么呢?

从图片字面意思理解是使用一个锤子破坏了一个电路。

我们一般都会在自己家里安装熔断器,以阻止异常的电流从电网流向家里。在开始“微服务的熔断器”之前,让我们先看看它是如何工作的。

如上图所示,一个典型的熔断器装置有 2 个主要部件:

  1. 用火线紧紧包裹的软铁芯
  2. 触体。只要接触点能够形成一个连接点,电流就会从外部电源流向我们的房子。相反,如果连接断开,电流就停止流动。

当电流通过缠绕在软铁芯周围的导线时,软铁芯就像一块电磁铁,当流过它的电流高于预期的安培时,电磁铁就会变得强大到足以吸引邻近的触点,从而导致短路。

你一定在想,这与微服务架构有什么关系呢?在我看来,这是高度相关的,正如我们下面将要看到的!

微服务架构中的级联故障

微服务架构已经很好地取代了单体架构,但是为了使我们的系统具有高度的弹性,我们还需要解决一些关键问题。

微服务的一个问题是级联故障。举一个例子来更好地理解它。

在上图中,参与者调用我们的主服务,它依赖于上游服务——A,B,C。现在假定,服务 A 是一个读取量较大的系统,它依赖于数据库。这个数据库有其自身的局限性,并且在过载时,可能导致连接重置。这个问题不仅会影响服务 A 的性能,还会影响主服务。

这就是人们所说的“一块臭肉坏了整锅汤”,喝过这锅汤的人肯定会有同感。下面让我们用一个例子来验证这一点。

让我们构建一个 Netflixisc 应用程序。其中一个微服务负责提供feed页面的电影服务。此服务还依赖于推荐服务为用户提供适当的推荐。

// Recommendation Service
func main() {
 logGoroutines()
 http.HandleFunc("/recommendations", recoHandler)
 log.Fatal(http.ListenAndServe(":9090", nil))
}

func logGoroutines() {
 ticker := time.NewTicker(500 * time.Millisecond)
 done := make(chan bool)
 go func() {
  for {
   select {
   case <-done:
    return
   case t := <-ticker.C:
    fmt.Printf("\n%v - %v", t, runtime.NumGoroutine())
   }
  }
 }()
}

func recoHandler(w http.ResponseWriter, r *http.Request) {
 a := `{"movies": ["Few Angry Men""Pride & Prejudice"]}`
 w.Write([]byte(a))
}

推荐服务暴露一个路由接口 /recommendations,它返回一个推荐电影列表,同时每 500 毫秒打印一次 goroutine 的数量。

// Movies App
type MovieResponse struct {
 Feed           []string
 Recommendation []string
}

func main() {
 http.HandleFunc("/movies", fetchMoviesFeedHandler)
 log.Fatal(http.ListenAndServe(":8080", nil))
}

func fetchMoviesFeedHandler(w http.ResponseWriter, r *http.Request) {
 mr := MovieResponse{
  Feed: []string{"Transformers""Fault in our stars""The Old Boy"},
 }
 rms, err := fetchRecommendations()
 if err != nil {
  w.WriteHeader(500)
 }
 mr.Recommendation = rms
 bytes, err := json.Marshal(mr)
 if err != nil {
  w.WriteHeader(500)
 }
 w.Write(bytes)
}

func fetchRecommendations() ([]string, error) {
 resp, err := http.Get("http://localhost:9090/recommendations")
 if err != nil {
  return []string{}, err
 }
 defer resp.Body.Close()
 body, err := ioutil.ReadAll(resp.Body)
 if err != nil {
  return []string{}, err
 }
 var mvsr map[string]interface{}
 err = json.Unmarshal(body, &mvsr)
 if err != nil {
  return []string{}, err
 }
 mvsb, err := json.Marshal(mvsr["movies"])
 if err != nil {
  return []string{}, err
 }
 var mvs []string
 err = json.Unmarshal(mvsb, &mvs)
 if err != nil {
  return []string{}, err
 }
 return mvs, nil
}

电影服务暴露一个路由 /movies,它返回电影列表和推荐列表。为了获取推荐,它反过来调用上游的推荐服务

通过此设置,让我们以每秒 100 个请求的速率访问电影服务,持续 3 秒钟。在 99% 的毫秒范围内,我们可以获得 100% 的成功。这是预期的,因为只提供静态数据。

现在,假设推荐服务的响应时间过长,并在 recoHandler 添加20秒的等待时间,然后重新进行测试。成功率会下降,而响应时间也会开始受到影响。此外,在测试期间阻塞在推荐服务上的goroutine数量将急剧增加。

推荐服务的停工时间影响了终端用户,因为本来可以提供给他的电影feed列表都没有提供。这正是级联故障对我们的系统造成的影响。

熔断器救援

熔断器是一个非常简单但相当重要的概念,因为它可以让我们保持服务的高可用性。熔断器有三种状态:

  • Closed State 关闭状态

关闭状态是指数据通过的时候连接处关闭的状态。这是我们的理想状态,其中上游服务正如预期的那样工作。

  • Open State 开放状态

打开状态指的是由于上游服务未按预期响应而导致电路短路的状态。这种短路可以避免上游服务在已经挣扎的情况下不堪重负。此外,下游服务的业务逻辑可以更快地获得上游可用性状态的反馈,而无需等待上游的响应。

  • Half Open State 半开状态

如果熔断器是打开状态,我们希望它在上游服务再次可用时立即关闭它。虽然你可以通过手动干预来实现,但首选的方法应该是在电路最后一次打开,让一些请求延迟之后通过电路,

如果这些请求请求上游服务成功,我们就可以安全地接通整个链路。

另一方面,如果这些请求失败,熔断器仍然处于打开状态。

熔断器的状态图如下:

  1. 初始状态下熔断器是关闭的,当故障超过配置的阈值,则会打开
  2. 熔断器是打开状态,在经过一段熔断时间后,部分会打开
  3. 如果熔断器是半开的,它可以
    • 再次打开,如果允许通过的请求也失败了
    • 关闭,如果允许通过的请求成功响应

熔断器在 Golang 的应用

虽然有多个库可供选择,但最常用的是 hystrix[1]。正如文档建议的那样,hystrix 是 Netflix 设计的一个延迟和容错库,用于隔离远程系统、服务和第三方库的访问,阻止级联故障,并在不可避免的故障发生的复杂分布式系统中实现恢复能力。

Hystrix 熔断器的实现取决于以下配置:

  1. 超时 ー 上游服务响应的等待时间
  2. 最大并发请求 ー 上游服务允许调用的最大并发
  3. 请求容量阈值 ー 在熔断之前的请求数,断路器在需要更改状态时无法评估的请求数量
  4. 睡眠窗口 ー 开放状态与半开放状态之间的延迟时间
  5. 误差百分比阈值ー熔断器短路时的误差百分比阈值

接下来让我们在电影和推荐示例中使用它,并在获取推荐时实现熔断器模式。

var downstreamErrCount int
var circuitOpenErrCount int

func main() {
 downstreamErrCount = 0
 circuitOpenErrCount = 0
 hystrix.ConfigureCommand("recommendation", hystrix.CommandConfig{
  Timeout: 100,
  RequestVolumeThreshold: 25,
  ErrorPercentThreshold:  5,
  SleepWindow:            1000,
 })
 http.HandleFunc("/movies", fetchMoviesFeedHandlerWithCircuitBreaker)
 log.Fatal(http.ListenAndServe(":8080", nil))
}

func fetchMoviesFeedHandlerWithCircuitBreaker(w http.ResponseWriter, r *http.Request) {
 mr := MovieResponse{
  Feed: []string{"Transformers""Fault in our stars""The Old Boy"},
 }
 
 output := make(chan bool, 1)
 errors := hystrix.Go("recommendation", func() error {
  // talk to other services
  rms, err := fetchRecommendations()
  if err != nil {
   return err
  }
  mr.Recommendation = rms
  output <- true
  return nil
 }, func(err error) error {
     // 写你的fallback(兜底)逻辑
  return nil
 })

 select {
 case err := <-errors:
  if err == hystrix.ErrCircuitOpen {
   circuitOpenErrCount = circuitOpenErrCount + 1
  } else {
   downstreamErrCount = downstreamErrCount + 1
  }

 case _ = <-output:

 }

 bytes, err := json.Marshal(mr)
 if err != nil {
  w.WriteHeader(500)
 }
 fmt.Printf("\ndownstreamErrCount=%d, circuitOpenErrCount=%d", downstreamErrCount, circuitOpenErrCount)
 w.Write(bytes)
}

使用 Hystrix,您还可以在熔断器打开时实现兜底逻辑。这种逻辑可能因情况而异。如果熔断器打开,则从缓存中获取。

使用这个更新的逻辑,让我们尝试以每秒100个请求的速率重新攻击 3 秒钟。

哇! !100% 的成功率,在打开的情况下,我们只提供 Feed 和返回 0个推荐。此外,由于每当熔断器熔断,我们不再调用上游服务,因此推荐服务不会不堪重负,阻塞的 goroutine 数量不会像以前那么多。

扩展阅读

我的建议:

  1. 关于 Netflix Hystrix[2]
  2. Hystrix 是怎样工作的?[3]
  3. Hystrix bucketing[4]


参考资料

[1] 

hystrix: https://github.com/afex/hystrix-go/hystrix

[2] 

关于 Netflix Hystrix: https://github.com/Netflix/Hystrix/wiki

[3] 

Hystrix 是怎样工作的?: https://github.com/Netflix/Hystrix/wiki/How-it-Works

[4] 

Hystrix bucketing: https://raw.githubusercontent.com/wiki/Netflix/Hystrix/images/circuit-breaker-1280.png



推荐阅读


福利

我为大家整理了一份从入门到进阶的Go学习资料礼包,包含学习建议:入门看什么,进阶看什么。关注公众号 「polarisxu」,回复 ebook 获取;还可以回复「进群」,和数万 Gopher 交流学习。

浏览 43
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报