什么是韧性?
软件本身并不是目的:它支持您的业务流程并使客户满意。如果软件没有在生产中运行,它就无法产生价值。然而,生产性软件也必须是正确的、可靠的和可用的。
架构师的宝库,每天一篇,开拓你的视野和深度。分享企业架构,业务架构,应用架构,数据架构,技术架构,安全架构等。讨论架构框架,规划,治理,标准,落地。交流新兴的架构风格和模型。如微服务,事件驱动,微前端,大数据,数仓,物联网,人工智能架构。
当谈到软件设计中的弹性时,主要目标是构建健壮的组件,这些组件既可以容忍其范围内的故障,也可以容忍它们所依赖的其他组件的故障。虽然自动故障转移或冗余等技术可以使组件具有容错性,但如今几乎每个系统都是分布式的。即使是一个简单的 Web 应用程序也可以包含 Web 服务器、数据库、防火墙、代理、负载平衡器和缓存服务器。此外,网络基础设施本身由许多组件组成,因此总是会在某处发生故障。
除了完全失败的情况外,服务也可能需要更长的时间来响应。实际上,尽管他们的响应格式是正确的,但他们甚至可能以错误的方式回答语义。同样,系统拥有的组件越多,发生故障的可能性就越大。
可用性通常被认为是一个重要的质量属性。它表示一个组件实际可用的时间量,与该组件应该可用的时间量相比。可以用以下公式表示:
传统方法旨在增加正常运行时间,而现代方法旨在减少恢复时间,从而减少停机时间。这很有用,因为它允许我们处理故障,而不是不惜一切代价阻止它们,并且在它们发生时长时间不可用。Uwe Friedrichsen 将弹性设计模式分为四类:松散耦合、隔离、延迟控制和监督(Loose coupling, isolation, latency control, and supervision)。
在这篇博文中,我们想看看延迟控制类别中的四种模式:重试、回退、超时和断路器。在理论介绍之后,我们将看到如何使用 Eclipse Vert.x 在实践中应用这些模式。我们将通过讨论替代实现并总结调查结果来结束这篇文章。
模式
示例场景
为了说明模式的功能,我们将使用一个非常简单的示例用例。想象一下作为购物平台一部分的支付服务。当客户想要付款时,支付服务应确保没有欺诈意图。为此,它要求提供欺诈检查服务。
在这种情况下,我们的服务提供基于 HTTP 的接口。为了检查交易,支付服务向欺诈检查服务发送 HTTP 请求。如果一切正常,将会有一个 200 响应,其中的布尔值指示交易是否是欺诈性的。但是,如果欺诈检查服务没有回答怎么办?如果它返回内部服务器错误(500)怎么办?
现在让我们看一下解决可能的通信问题的四种具体模式。虽然这是一个具体示例,但您可以想象任何其他涉及通过不可靠信道与不可靠服务进行通信的星座。
重试
每当我们假设可以通过再次发送请求来修复意外响应(或没有响应)时,使用重试模式会有所帮助。这是一种非常简单的模式,失败的请求会在失败的情况下重试可配置的次数,然后才会将操作标记为失败。
下面的动画说明了支付服务试图发出欺诈支票。由于欺诈检查服务中的内部服务器错误,第一个请求失败。支付服务重试请求并收到交易不是欺诈的答案。
重试在以下情况下很有用
- 丢包等临时网络问题
- 目标服务的内部错误,例如由数据库中断引起
- 由于对目标服务的大量请求而没有响应或响应缓慢
但是请记住,如果问题是由目标服务过载引起的,重试可能会使这些问题变得更糟。为避免将弹性模式转变为拒绝服务攻击,可以将重试与其他技术结合使用,例如指数退避或断路器(见下文)。
倒退(Fallback)
回退模式使您的服务能够在对另一个服务的请求失败的情况下继续执行。我们不会因为缺少响应而中止计算,而是填写一个备用值。
下面的动画再次描绘了支付服务向欺诈检查服务发出请求。同样,欺诈检查服务返回内部服务器错误。然而,这一次,我们有一个备用方案,它假设交易不是欺诈性的。
备用值并不总是可行的,但如果小心使用,可以大大提高您的整体弹性。在上面的示例中,如果欺诈检查服务不可用,则回退到将交易视为非欺诈可能是危险的。它甚至为试图首先向服务发送垃圾邮件然后进行欺诈交易的欺诈交易打开了攻击面。
另一方面,如果后备是假设每笔交易都是欺诈性的,则不会进行任何付款,并且后备基本上是无用的。一个好的折衷方案可能是回退到一个简单的业务规则,例如简单地让相当少量的交易通过,以在风险和不失去客户之间取得良好的平衡。
Timeout(超时)
超时模式非常简单,许多 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)
在这个例子中,我们正在创建一个断路器,它在将其视为失败之前重试操作两次。在一次故障后,我们打开电路,该电路将在 5000 毫秒后再次半开。操作在 2000 毫秒后超时。如果指定了回退,则仅在开路的情况下才会调用它。也可以将断路器配置为在发生故障时调用回退,即使电路已关闭。
为了执行命令,我们需要提供一段异步代码来执行 Handler<Future<T>> 类型以及处理结果的 Handler<AsyncResult<T>> 类型的处理程序。返回 OK 并在之后打印它的最小示例如下所示:
circuitBreaker.executeCommand( Handler<Future<String>> { it.complete("OK") }, Handler { println(it) } )
在 Kotlin 中使用 Vert.x 时,您还可以将挂起函数作为参数传递,而不是使用处理程序。更多细节请参考 CoroutineHandlerFactory 类及其用法。除了这些基本功能之外,Vert.x 断路器模块还提供以下高级功能:
- 事件总线通知。断路器可以在每次状态更改时将事件发布到事件总线。如果您想以某种方式对这些事件做出反应,这很有用。
- 指标。断路器可以发布要由 Hystrix 仪表板使用的指标,以可视化断路器的状态。
- 状态更改回调。您可以配置在电路打开或关闭时调用的自定义处理程序。
替代实施方法
并非每个框架都支持开箱即用的弹性设计模式。Vert.x 也不支持所有可能的模式。有一些指定项目直接解决弹性主题,例如 Hystrix、resilience4j、failsafe和 Istio 的弹性特性。
Hystrix 已在许多应用程序中使用,但不再处于积极开发中。Hystrix、resilience4j 以及故障安全都是从应用程序源代码中直接调用的。例如,您可以通过实现接口或使用注释来集成它。
另一方面,Istio 是一个服务网格,因此是基础架构的一部分,而不是应用程序代码。它用于编排分布式服务系统并实现边车的概念。服务通信通过那个 sidecar 发生,这是一个与服务进程一起的专用进程。然后,sidecar 可以处理诸如重试之类的机制。
Sidecar 方法的优点是您不会将业务逻辑与弹性逻辑混为一谈。您可以在不涉及太多应用程序代码的情况下替换 sidecar 技术。此外,您可以轻松修改和调整 sidecar 配置,而无需重新部署服务。缺点在于无法使用特定模式,例如用于线程池隔离的隔板模式( bulkhead)。此外,后备值等模式在很大程度上取决于您的业务逻辑。扩展现有代码库也可能比添加新的基础架构组件更容易。
概括
在这篇文章中,我们看到了松散耦合、隔离、延迟控制和监督如何对系统弹性产生积极影响。重试模式可以处理可以通过多次尝试来纠正的通信错误。回退模式有助于在本地解决通信故障。超时模式提供了延迟的上限。断路器解决了在持续通信错误的情况下由于重试和快速回退而导致的意外拒绝服务攻击的问题。
像 Vert.x 这样的框架提供了一些开箱即用的弹性模式。还有可以与任何框架一起使用的专用弹性库。另一方面,服务网格作为在基础设施级别引入弹性模式的选项而存在。与往常一样,没有万能的解决方案,您的团队应该找出最适合他们的解决方案。