首页  >  文章  >  后端开发  >  Go 中的断路器:阻止级联故障

Go 中的断路器:阻止级联故障

WBOY
WBOY原创
2024-07-17 17:41:11982浏览

Circuit Breakers in Go: Stop Cascading Failures

断路器

断路器检测故障并以防止故障不断重复的方式封装处理这些故障的逻辑。例如,它们在处理对外部服务、数据库或系统中可能暂时失败的任何部分的网络调用时非常有用。通过使用断路器,您可以防止级联故障、管理临时错误并在系统崩溃时保持稳定且响应迅速的系统。

级联故障

当系统某一部分的故障触发其他部分的故障时,就会发生级联故障,从而导致大范围的破坏。一个例子是,当分布式系统中的微服务变得无响应时,导致依赖的服务超时并最终失败。根据应用程序的规模,这些故障的影响可能是灾难性的,这会降低性能,甚至可能影响用户体验。

断路器模式

断路器本身是一种技术/模式,它运行三种不同的状态,我们将讨论它们:

  1. 关闭状态: 在关闭状态下,断路器允许所有请求正常传递到目标服务。如果请求成功,电路将保持关闭状态。然而,如果达到一定的故障阈值,电路就会转变为开路状态。将其视为一项完全可操作的服务,用户可以毫无问题地登录和访问数据。一切都进展顺利。

Circuit Breakers in Go: Stop Cascading Failures

2。 Open State :在打开状态下,断路器立即使所有传入请求失败,而不尝试联系目标服务。进入该状态是为了防止故障服务进一步过载并为其提供恢复时间。在预定的超时后,断路器进入半开状态。一个相关的例子是这样的;想象一下,一家在线商店突然遇到问题,每次购买尝试都失败。为了避免系统不堪重负,商店暂时停止接受任何新的购买请求。

Circuit Breakers in Go: Stop Cascading Failures

3。半开状态:在半开状态下,断路器允许(可配置的)有限数量的测试请求传递到目标服务。如果这些请求成功,电路将转换回关闭状态。如果它们失败,电路将返回到开路状态。在上面我给出的处于打开状态的在线商店的示例中,在线商店开始允许进行几次购买尝试,以查看问题是否已得到解决。如果这几次尝试成功,商店将全面重新开放服务以接受新的购买请求。

此图显示了断路器何时尝试查看对 服务 B 的请求是否成功,然后失败/中断:

Circuit Breakers in Go: Stop Cascading Failures

后续图显示了对 服务 B 的测试请求成功时,电路关闭,并且所有进一步的调用再次路由到 服务 B

Circuit Breakers in Go: Stop Cascading Failures

注意 :断路器的关键配置包括失败阈值(打开电路所需的失败次数)、打开状态的超时时间以及半开状态下的测试请求数量状态。

在 Go 中实现断路器

值得一提的是,需要具备 Go 的先验知识才能阅读本文。

与任何软件工程模式一样,断路器可以用多种语言实现。不过,本文将重点讨论 Golang 中的实现。虽然有几个库可用于此目的,例如 goresilience、go-resiliency 和 gobreaker,但我们将特别专注于使用 gobreaker 库。

专业提示:您可以查看 gobreaker 包的内部实现,请查看此处。

让我们考虑一个简单的 Golang 应用程序,其中实现了断路器来处理对外部 API 的调用。这个基本示例演示了如何使用断路器技术包装外部 API 调用:

让我们谈谈一些重要的事情:

  1. gobreaker.NewCircuitBreaker 函数使用我们的自定义设置初始化断路器
  2. cb.Execute方法包装HTTP请求,自动管理电路状态。
  3. MaximumRequests 是半开状态时允许通过的最大请求数
  4. 间隔是断路器闭合状态的循环周期,以清除内部计数
  5. 超时是从打开状态转换到半打开状态之前的持续时间。
  6. 每当请求在关闭状态下失败时,都会使用计数副本调用 ReadyToTrip。如果 ReadyToTrip 返回 true,断路器将进入断开状态。在我们的例子中,如果请求连续失败超过三次,它将返回 true。
  7. 每当断路器的状态发生变化时,都会调用 OnStateChange。您通常希望在此处收集状态更改的指标并向您选择的任何指标收集器报告。

让我们编写一些单元测试来验证我们的断路器实现。我只会解释最关键的单元测试以供理解。您可以在此处查看完整代码。

  1. 我们将编写一个测试来模拟连续失败的请求并检查断路器是否跳至打开状态。本质上,在 3 次故障之后,当第四次故障发生时,我们预计断路器会跳闸(断开),因为我们的条件表示 counts.ConsecutiveFailures > 3.测试如下:
 t.Run("FailedRequests", func(t *testing.T) {
         // Override callExternalAPI to simulate failure
         callExternalAPI = func() (int, error) {
             return 0, errors.New("simulated failure")
         }

         for i := 0; i < 4; i++ {
             _, err := cb.Execute(func() (interface{}, error) {
                 return callExternalAPI()
             })
             if err == nil {
                 t.Fatalf("expected error, got none")
             }
         }

         if cb.State() != gobreaker.StateOpen {
             t.Fatalf("expected circuit breaker to be open, got %v", cb.State())
         }
     })
  1. 我们将测试开放> - 打开> 关闭状态。但我们首先会模拟开路并调用超时。超时后,我们需要至少发出一次成功请求,以使电路转换为半开状态。在半开状态之后,我们需要再次成功请求电路再次完全关闭。如果出于某种原因,案例中没有成功请求的记录,它将恢复为开放状态。测试如下所示:
 //Simulates the circuit breaker being open, 
 //wait for the defined timeout, 
 //then check if it closes again after a successful request.
     t.Run("RetryAfterTimeout", func(t *testing.T) {
         // Simulate circuit breaker opening
         callExternalAPI = func() (int, error) {
             return 0, errors.New("simulated failure")
         }

         for i := 0; i < 4; i++ {
             _, err := cb.Execute(func() (interface{}, error) {
                 return callExternalAPI()
             })
             if err == nil {
                 t.Fatalf("expected error, got none")
             }
         }

         if cb.State() != gobreaker.StateOpen {
             t.Fatalf("expected circuit breaker to be open, got %v", cb.State())
         }

         // Wait for timeout duration
         time.Sleep(settings.Timeout + 1*time.Second)

         //We expect that after the timeout period, 
         //the circuit breaker should transition to the half-open state. 

         // Restore original callExternalAPI to simulate success
         callExternalAPI = func() (int, error) {
             resp, err := http.Get(server.URL)
             if err != nil {
                 return 0, err
             }
             defer resp.Body.Close()
             return resp.StatusCode, nil
         }

         _, err := cb.Execute(func() (interface{}, error) {
             return callExternalAPI()
         })
         if err != nil {
             t.Fatalf("expected no error, got %v", err)
         }

         if cb.State() != gobreaker.StateHalfOpen {
             t.Fatalf("expected circuit breaker to be half-open, got %v", cb.State())
         }

         //After verifying the half-open state, another successful request is simulated to ensure the circuit breaker transitions back to the closed state.
         for i := 0; i < int(settings.MaxRequests); i++ {
             _, err = cb.Execute(func() (interface{}, error) {
                 return callExternalAPI()
             })
             if err != nil {
                 t.Fatalf("expected no error, got %v", err)
             }
         }

         if cb.State() != gobreaker.StateClosed {
             t.Fatalf("expected circuit breaker to be closed, got %v", cb.State())
         }
     })
  1. 让我们测试一下在连续 2 次失败请求后触发的 ReadyToTrip 条件。我们将有一个变量来跟踪连续失败。 ReadyToTrip 回调已更新,以检查断路器是否在 2 次故障后跳闸(counts.ConsecutiveFailures > 2)。我们将编写一个测试来模拟故障并验证计数以及断路器在指定的故障次数后是否转换为打开状态。
 t.Run("ReadyToTrip", func(t *testing.T) {
         failures := 0
         settings.ReadyToTrip = func(counts gobreaker.Counts) bool {
             failures = int(counts.ConsecutiveFailures)
             return counts.ConsecutiveFailures > 2 // Trip after 2 failures
         }

         cb = gobreaker.NewCircuitBreaker(settings)

         // Simulate failures
         callExternalAPI = func() (int, error) {
             return 0, errors.New("simulated failure")
         }
         for i := 0; i < 3; i++ {
             _, err := cb.Execute(func() (interface{}, error) {
                 return callExternalAPI()
             })
             if err == nil {
                 t.Fatalf("expected error, got none")
             }
         }

         if failures != 3 {
             t.Fatalf("expected 3 consecutive failures, got %d", failures)
         }
         if cb.State() != gobreaker.StateOpen {
             t.Fatalf("expected circuit breaker to be open, got %v", cb.State())
         }
     })

高级策略

我们可以更进一步,在断路器实现中添加指数退避策略。我们将通过演示指数退避策略的示例来保持本文的简单和简洁。然而,还有其他值得一提的高级断路器策略,例如减载、隔离、回退机制、上下文和取消。这些策略基本上增强了断路器的稳健性和功能性。这是使用指数退避策略的示例:

指数退避

具有指数退避功能的断路器

让我们澄清一些事情:

自定义退避函数:exponentialBackoff 函数实现带有抖动的指数退避策略。它基本上根据尝试次数计算退避时间,确保延迟随着每次重试尝试呈指数增长。

处理重试: 正如您在 /api 处理程序中看到的,逻辑现在包含一个循环,该循环尝试调用外部 API 最多指定的尝试次数( attempts := 5)。每次尝试失败后,我们都会等待由exponentialBackoff函数确定的持续时间,然后再重试。

断路器执行:断路器在循环内使用。如果外部API调用成功(err == nil),则循环中断,并返回成功结果。如果所有尝试均失败,则会返回 HTTP 503(服务不可用)错误。

在断路器实现中集成自定义退避策略确实旨在更优雅地处理瞬态错误。重试之间不断增加的延迟有助于减少失败服务的负载,让它们有时间恢复。正如上面的代码所示,我们引入了exponentialBackoff函数来在调用外部API时增加重试之间的延迟。

此外,我们可以集成指标和日志记录来使用 Prometheus 等工具来监控断路器状态变化,以进行实时监控和警报。这是一个简单的例子:

在 go 中使用高级策略实现断路器模式

如您所见,我们现在已经完成了以下操作:

  1. 在 L16-21 中,我们定义了一个 prometheus 计数器向量来跟踪请求的数量及其状态(成功、失败、断路器状态变化)。
  2. 在 L25-26 中,定义的指标在 init 函数中注册到 Prometheus。

专业提示:Go 中的 init 函数用于在执行 main 函数或包中的任何其他代码之前初始化包的状态。在本例中,init 函数向 Prometheus 注册 requestCount 指标。这本质上确保了 Prometheus 知道这个指标,并且可以在应用程序开始运行后立即开始收集数据。

  1. 我们使用自定义设置创建断路器,包括 ReadyToTrip 功能,该功能可增加故障计数器并确定何时使电路跳闸。

  2. OnStateChange 记录状态更改并增加相应的 prometheus 指标

  3. 我们在 /metrics 端点公开 Prometheus 指标

总结

作为本文的总结,我希望您看到断路器如何在构建有弹性且可靠的系统中发挥巨大作用。通过主动防止级联故障,它们增强了微服务和分布式系统的可靠性,即使在逆境中也能确保无缝的用户体验。

请记住,任何为可扩展性而设计的系统都必须采用策略来优雅地处理故障并快速恢复 — Oluwafemi2024

最初发布于 https://oluwafemiakinde.dev 于 2024 年 6 月 7 日。

以上是Go 中的断路器:阻止级联故障的详细内容。更多信息请关注PHP中文网其他相关文章!

声明:
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn