断路器检测故障并以防止故障不断重复的方式封装处理这些故障的逻辑。例如,它们在处理对外部服务、数据库或系统中可能暂时失败的任何部分的网络调用时非常有用。通过使用断路器,您可以防止级联故障、管理临时错误并在系统崩溃时保持稳定且响应迅速的系统。
当系统某一部分的故障触发其他部分的故障时,就会发生级联故障,从而导致大范围的破坏。一个例子是,当分布式系统中的微服务变得无响应时,导致依赖的服务超时并最终失败。根据应用程序的规模,这些故障的影响可能是灾难性的,这会降低性能,甚至可能影响用户体验。
断路器本身是一种技术/模式,它运行三种不同的状态,我们将讨论它们:
2。 Open State :在打开状态下,断路器立即使所有传入请求失败,而不尝试联系目标服务。进入该状态是为了防止故障服务进一步过载并为其提供恢复时间。在预定的超时后,断路器进入半开状态。一个相关的例子是这样的;想象一下,一家在线商店突然遇到问题,每次购买尝试都失败。为了避免系统不堪重负,商店暂时停止接受任何新的购买请求。
3。半开状态:在半开状态下,断路器允许(可配置的)有限数量的测试请求传递到目标服务。如果这些请求成功,电路将转换回关闭状态。如果它们失败,电路将返回到开路状态。在上面我给出的处于打开状态的在线商店的示例中,在线商店开始允许进行几次购买尝试,以查看问题是否已得到解决。如果这几次尝试成功,商店将全面重新开放服务以接受新的购买请求。
此图显示了断路器何时尝试查看对 服务 B 的请求是否成功,然后失败/中断:
后续图显示了对 服务 B 的测试请求成功时,电路关闭,并且所有进一步的调用再次路由到 服务 B:
注意 :断路器的关键配置包括失败阈值(打开电路所需的失败次数)、打开状态的超时时间以及半开状态下的测试请求数量状态。
值得一提的是,需要具备 Go 的先验知识才能阅读本文。
与任何软件工程模式一样,断路器可以用多种语言实现。不过,本文将重点讨论 Golang 中的实现。虽然有几个库可用于此目的,例如 goresilience、go-resiliency 和 gobreaker,但我们将特别专注于使用 gobreaker 库。
专业提示:您可以查看 gobreaker 包的内部实现,请查看此处。
让我们考虑一个简单的 Golang 应用程序,其中实现了断路器来处理对外部 API 的调用。这个基本示例演示了如何使用断路器技术包装外部 API 调用:
让我们谈谈一些重要的事情:
让我们编写一些单元测试来验证我们的断路器实现。我只会解释最关键的单元测试以供理解。您可以在此处查看完整代码。
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()) } })
//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()) } })
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 中使用高级策略实现断路器模式
如您所见,我们现在已经完成了以下操作:
专业提示:Go 中的 init 函数用于在执行 main 函数或包中的任何其他代码之前初始化包的状态。在本例中,init 函数向 Prometheus 注册 requestCount 指标。这本质上确保了 Prometheus 知道这个指标,并且可以在应用程序开始运行后立即开始收集数据。
我们使用自定义设置创建断路器,包括 ReadyToTrip 功能,该功能可增加故障计数器并确定何时使电路跳闸。
OnStateChange 记录状态更改并增加相应的 prometheus 指标
我们在 /metrics 端点公开 Prometheus 指标
作为本文的总结,我希望您看到断路器如何在构建有弹性且可靠的系统中发挥巨大作用。通过主动防止级联故障,它们增强了微服务和分布式系统的可靠性,即使在逆境中也能确保无缝的用户体验。
请记住,任何为可扩展性而设计的系统都必须采用策略来优雅地处理故障并快速恢复 — Oluwafemi,2024
最初发布于 https://oluwafemiakinde.dev 于 2024 年 6 月 7 日。
以上是Go 中的断路器:阻止级联故障的详细内容。更多信息请关注PHP中文网其他相关文章!