发布于 2026-01-06 5 阅读
0

弹性设计模式:重试、回退、超时、熔断器 什么是弹性?这些模式在 Vert.x 中的实现 其他实现方法 总结

弹性设计模式:重试、回退、超时、熔断器

什么是韧性?

模式

Vert.x 中的实现

替代实施方案

概括

什么是韧性?

软件本身并非目的:它支持您的业务流程,并提升客户满意度。如果软件未在生产环境中运行,则无法创造价值。然而,用于生产的软件还必须具备正确性、可靠性和可用性。

在软件设计中,韧性的主要目标是构建健壮的组件,使其不仅能够容忍自身范围内的故障,还能应对所依赖的其他组件的故障。虽然自动故障转移或冗余等技术可以提高组件的容错能力,但如今几乎所有系统都是分布式的。即使是一个简单的 Web 应用程序也可能包含 Web 服务器、数据库、防火墙、代理、负载均衡器和缓存服务器。此外,网络基础设施本身也由许多组件构成,因此总会有某个地方发生故障。

除了完全失效的情况外,服务响应时间也可能延长。实际上,即使响应格式正确,服务也可能出现语义错误的情况。此外,系统组件越多,出现故障的可能性就越大。

可用性通常被认为是一项重要的质量属性。它表示组件实际可用时间与组件预期可用时间的比值。可用率可以用以下公式表示:

传统方法旨在提高正常运行时间,而现代方法则旨在缩短恢复时间,从而减少停机时间。这样做的好处在于,它使我们能够应对故障,而不是不惜一切代价预防故障,导致一旦发生故障就长时间无法使用。Uwe Friedrichsen 将弹性设计模式分为四类:松耦合隔离延迟控制监督

在本篇博文中,我们将探讨延迟控制类别中的四种模式:重试回退超时熔断器。在进行理论介绍之后,我们将了解如何使用 Eclipse Vert.x 在实践中应用这些模式。最后,我们将讨论其他实现方案并总结研究结果。

模式

示例场景

为了说明这些模式的功能,我们将使用一个非常简单的示例用例。假设购物平台中包含一个支付服务。当客户想要付款时,支付服务需要确保不存在欺诈意图。为此,它会请求反欺诈服务进行验证。

在这种情况下,我们的服务提供基于 HTTP 的接口。为了验证交易,支付服务会向反欺诈服务发送 HTTP 请求。如果一切正常,将会收到一个 200 响应,其中包含一个布尔值,指示该交易是否为欺诈交易。但如果反欺诈服务没有响应呢?如果它返回内部服务器错误(500)呢?

现在让我们来看看解决可能出现的通信问题的四种具体模式。虽然这是一个具体的例子,但你可以想象任何其他涉及通过不可靠的信道与不可靠的服务进行通信的情况。

重试

当我们假设可以通过重新发送请求来修复意外响应(或者根本没有响应)时,使用重试机制会很有帮助。这是一种非常简单的机制,它会在操作失败后,按照可配置的次数重试失败的请求,直到该操作被标记为失败为止。

以下动画演示了支付服务尝试发出欺诈检查的过程。由于欺诈检查服务内部服务器错误,第一次请求失败。支付服务重试该请求,并收到交易并非欺诈的回复。

在某些情况下,重试可能很有用。

  • 临时性网络问题,例如丢包
  • 目标服务的内部错误,例如由数据库中断引起的错误。
  • 由于对目标服务的请求数量庞大,导致无响应或响应缓慢。

但请注意,如果问题是由目标服务过载引起的,重试可能会使问题更加严重。为了避免将弹性恢复模式变成拒绝服务攻击,可以将重试与其他技术结合使用,例如指数退避或熔断器(见下文)。

倒退

回退模式允许你的服务在向另一个服务发出请求失败时继续执行。我们不会因为缺少响应而中止计算,而是填充一个备用值。

以下动画再次展示了支付服务向反欺诈服务发出请求的过程。反欺诈服务再次返回内部服务器错误。但这次我们设置了备用方案,假定该交易并非欺诈交易。

备用值并非总是可行,但如果谨慎使用,可以显著提高整体弹性。例如,如果欺诈检查服务不可用,则将交易视为非欺诈交易可能会很危险。这甚至会给欺诈交易者带来攻击面,他们可能先向该服务发送大量垃圾邮件,然后再进行欺诈交易。

另一方面,如果默认所有交易都是欺诈交易,那么所有付款都将无法完成,这种备选方案实际上毫无用处。一个比较好的折衷方案是采用简单的业务规则,例如允许金额较小的交易通过,从而在风险和避免客户流失之间取得良好的平衡。

暂停

超时机制非常简单,许多 HTTP 客户端都配置了默认超时时间。其目的是避免无限期地等待响应,从而避免将超时时间内未收到响应的请求全部视为失败。

下面的动画显示了支付服务等待欺诈检查服务的响应,并在超时后中止操作。

几乎所有应用程序都会使用超时机制来避免请求无限期地阻塞。然而,处理超时并非易事。试想一下,如果网店的订单提交超时,您无法确定是订单已成功提交但响应超时,还是订单创建仍在进行中,又或者请求根本没有被处理。如果将超时机制与重试机制结合使用,可能会导致重复订单。如果将订单标记为失败,客户可能会认为订单没有成功,但实际上订单可能已经成功,他们最终会被收取费用。

此外,你还需要设置足够长的超时时间,以便允许较慢的响应到达,但又要足够短,以避免等待永远不会到达的响应。

断路器

在电子学中,断路器是一种开关,用于保护元件免受过载损坏。在软件领域,断路器则用于保护服务免受垃圾邮件的侵扰,尤其是在服务因高负载而部分不可用的情况下。

断路器模式由 Martin Fowler 提出。它可以实现为一个有状态的软件组件,该组件在三种状态之间切换:关闭(请求可以自由流动)、打开(请求被拒绝,不会提交到远程资源)和半打开(允许一次探测请求来决定是否再次闭合断路器)。下面的动画演示了断路器的工作原理。

支付服务向反欺诈服务发出的请求会经过熔断器。如果出现两次内部服务器错误,熔断器会打开,后续请求将被阻止。经过一段时间的等待后,熔断器会进入半开状态。在此状态下,熔断器会允许一个请求通过,如果该请求失败,则熔断器会恢复到完全打开状态;如果请求成功,则熔断器会关闭。下一个请求成功后,熔断器会再次关闭。

断路器是一个非常有用的工具,尤其是在与重试机制、超时机制和回退机制结合使用时。回退机制不仅可以在发生故障时使用,也可以在断路器断开时使用。下一节我们将来看一个使用 Kotlin 编写的 Vert.x 代码示例。

Vert.x 中的实现

上一节我们从理论角度探讨了不同的弹性模式。现在让我们看看如何实现它们。示例的源代码已上传至 GitHub。本次演示将使用 Vert.x 和 Kotlin。其他方案将在下一节讨论。

Vert.x 提供了CircuitBreaker一个强大的装饰器类,它支持重试、回退、超时和断路器配置的任意组合。您可以使用该类配置断路器,CircuitBreakerOptions如下所示。

val vertx = Vertx.vertx()
val options = circuitBreakerOptionsOf(
    fallbackOnFailure = false,
    maxFailures = 1,
    maxRetries = 2,
    resetTimeout = 5000,
    timeout = 2000
)
val circuitBreaker = CircuitBreaker.create("my-circuit-breaker", vertx, options)
Enter fullscreen mode Exit fullscreen mode

在这个例子中,我们创建了一个断路器,它会在操作失败前重试两次。第一次失败后,我们会断开电路,电路会在 5000 毫秒后再次半开。操作超时时间为 2000 毫秒。如果指定了备用方案,则仅在电路断开时调用该方案。也可以配置断路器,使其即使电路闭合,在发生故障时也调用备用方案。

为了执行命令,我们需要提供一段异步执行的代码(类型为 `executable`)Handler<Future<T>>以及一个Handler<AsyncResult<T>>处理结果的处理程序(类型为 `handler`)。一个返回结果OK并随后打印结果的最小示例如下所示:

circuitBreaker.executeCommand(
    Handler<Future<String>> {
        it.complete("OK")
    },
    Handler {
        println(it)
    }
)
Enter fullscreen mode Exit fullscreen mode

在 Kotlin 中使用 Vert.x 时,您还可以将挂起函数作为参数传递,而无需使用处理程序。有关更多详细信息,请参阅该类CoroutineHandlerFactory及其用法。除了这些基本功能外,Vert.x 断路器模块还提供以下高级功能:

  • 事件总线通知。断路器可以在每次状态改变时向事件总线发布事件。如果您希望以某种方式响应这些事件,这将非常有用。
  • 指标。断路器可以发布指标,供 Hystrix 控制面板使用,以可视化断路器的状态。
  • 状态变更回调。您可以配置自定义处理程序,以便在电路打开或关闭时调用。

替代实施方案

并非所有框架都开箱即用地支持弹性设计模式。Vert.x 也并非支持所有可能的模式。有一些专门的项目直接致力于弹性主题,例如Hystrixresilience4jfailsafe以及Istio的弹性功能

Hystrix 曾被广泛应用于各种应用程序中,但目前已停止积极开发。Hystrix、resilience4j 以及 failsafe 均可直接从应用程序源代码中调用。您可以通过实现接口或使用注解等方式进行集成。

另一方面,Istio 是一个服务网格,因此它是基础设施的一部分,而非应用程序代码的一部分。它用于编排分布式服务系统,并实现了sidecar的概念。服务之间的通信通过 sidecar 进行,sidecar 是一个与服务进程并行运行的专用进程。sidecar 可以处理诸如重试之类的机制。

Sidecar 架构的优势在于它不会将业务逻辑与弹性逻辑混淆。您可以替换 Sidecar 技术,而无需对应用程序代码进行过多修改。此外,您可以轻松地修改和调整 Sidecar 配置,而无需重新部署服务。其缺点在于无法实现某些特定模式,例如用于线程池隔离的隔板模式。此外,诸如回退值之类的模式严重依赖于您的业务逻辑。扩展现有代码库可能比添加新的基础设施组件更容易。

概括

本文探讨了松耦合、隔离、延迟控制和监控如何有效提升系统弹性。重试模式能够处理可通过多次尝试纠正的通信错误。回退模式有助于在本地解决通信故障。超时模式为延迟设定了上限。熔断器则解决了因持续通信错误而导致的重试和快速回退,进而引发意外拒绝服务攻击的问题。

像 Vert.x 这样的框架提供了一些开箱即用的弹性模式。此外,还有一些专用的弹性库可以与任何框架配合使用。另一方面,服务网格也是一种在基础设施层面引入弹性模式的选择。一如既往,没有万能的解决方案,您的团队应该找到最适合他们的方案。


如果你喜欢这篇文章,可以在 ko-fi 上支持我

文章来源:https://dev.to/frosnerd/resilience-design-patterns-retry-fallback-timeout-Circuit-breaker-2870