高可用高可靠系统设计中的重试机制

重试机制是许多现代软件系统的关键组件。它允许我们的系统自动重试失败的操作,以从暂时性错误或网络中断中恢复。通过自动重试失败的操作,重试机制可以帮助软件系统从意外故障中恢复并继续正常运行。

今天,我们就来看看这些话题:

  1. 什么是重试模式?
    •  它的用途是什么?为什么我们需要在我们的系统中实现它?
  • 何时重试您的请求
    • 仅应重试某些请求。重要的是要了解可以重试下游服务中的哪些类型的错误,以避免出现业务逻辑问题。
  • 重试退避期
    • 当我们向下游服务重试请求时,失败后应该等待多长时间才能再次发送请求?
  • 如何重试
    • 我们将研究从基本到更复杂的重试方法。


    什么是重试模式?
    重试是当​​向下游服务的请求失败时发送相同请求的行为。通过使用重试模式,您将提高系统的下游弹性方面。当调用下游服务发生错误时,我们的系统会尝试再次调用它,而不是向上游服务返回错误。

    那么,究竟为什么我们需要这样做呢?近几十年来,微服务架构越来越受欢迎。虽然这种方法有很多好处,但微服务架构的缺点之一是在服务之间引入网络通信。当服务相互通信时,额外的网络通信可能会导致网络中出现错误(请阅读分布式计算的谬误)。每次调用其他服务都有可能出现这些错误。

    此外,无论您使用的是单体架构还是微服务架构,您很有可能仍然需要调用不在公司内部网络内的其他服务。在不同网络内调用服务意味着您的请求将经过更多网络层并且失败的可能性更大。

    除了网络错误之外,您还可能遇到系统错误,例如速率限制错误、服务停机和处理超时。您收到的错误可能适合重试,也可能不适合重试。让我们进入下一节来更详细地探讨它。

    何时重试您的请求
    尽管在系统中添加重试机制通常是一个好主意,但并非对下游服务的每个请求都应该重试。作为一个简单的基准,当您想要重试时应该考虑以下事项:

    这是暂时性错误吗?
    您需要考虑您收到的错误类型是否是暂时的(暂时的)。例如,您可以重试连接超时错误,因为它通常只是暂时的,但不是错误的请求错误,因为您需要更改请求。

    是系统错误吗?
    当您从下游服务收到错误消息时,它可以归类为系统错误或应用程序错误。系统错误一般是可以重试的,因为你的请求还没有被下游服务处理。另一方面,应用程序错误通常意味着您的请求有问题,您不应该重试。例如,如果您从下游服务收到错误请求错误,则无论您重试多少次,您都将始终收到相同的错误。

    幂等性
    即使您从下游服务收到错误,它仍然有可能处理您的请求。下游服务可以在处理完主进程后发送错误,但另一个子进程会导致错误。幂等API意味着即使API两次收到相同的请求,它也只会处理第一个请求。我们可以通过在请求中添加一些该请求唯一的 ID 来实现,以便下游服务可以确定是否应该处理该请求。通常,您可以通过方法来区分Request。GET、DELETE、 和PUT通常是幂等的,但POST不是。但是,您需要向服务所有者确认API的幂等性。

    重试的成本
    当您重试向下游服务发出请求时,将会有额外的资源使用。额外的资源使用可以是额外的 CPU 使用、阻塞线程、额外的内存使用、额外的带宽使用等形式。您需要考虑这一点,特别是如果您的服务预计有大流量。

    重试机制的实施成本
    许多编程语言已经有一个实现重试机制的库,但您仍然需要确定重试哪个请求。如果您愿意,您还可以创建您的重试机制或每个系统,但是当然,这意味着重试机制的实现成本很高。

    整理了一些常见错误以及它们是否适合重试: 

    • 连接超时:您的应用无法连接下游服务;因此,下游服务不知道您的请求,您可以重试。
    • 读取超时:下游应用已处理您的请求,但长时间未返回任何响应。
    • 断路器跳闸:如果您在服务中使用断路器,则会出现错误。您可以重试此类错误,因为您的服务尚未将其请求发送到下游服务。
    • 400 - 错误请求:此错误意味着您对下游服务的请求在验证后被标记为错误请求。您不应该重试此错误,因为如果请求相同,它总是会返回相同的错误。
    • 401 - 未经授权:您需要在发送请求之前授权。是否可以重试此错误将取决于身份验证方法和错误。但一般来说,如果您的请求相同,您总是会得到相同的错误。
    • 429 - 请求过多:您的请求受到下游服务的速率限制。您可以重试此错误,但您应该与下游服务的所有者确认您的请求将受到速率限制的时间。
    • 500 - 内部服务器错误:这意味着下游服务已开始处理您的请求,但在中间失败。通常,重试此错误就可以了。
    • 503 - 服务不可用:下游服务因停机而不可用。遇到这种错误重试一下就可以了。

    重试退避期
    当您的请求无法到达下游服务时,您的系统将需要等待一段时间才能重试。这段时间称为重试退避期。
    一般来说,调用之间的等待时间有三种策略:固定退避、指数退避和随机退避。他们三个都有各自的优点和缺点。您使用哪一种应取决于您的 API 和服务用例。

    1、固定退避
    固定退避意味着每次重试请求时,请求之间的延迟始终相同。例如,如果您以 5 秒的退避时间重试两次,那么如果第一次调用失败,则将在 5 秒后发送第二个请求。如果再次失败,则会在失败后5秒发送第三次调用。

    固定的退避期适合直接来自用户且需要快速响应的请求。如果请求很重要并且您需要它尽快返回,那么您可以将退避期设置为无或接近0。

    2、指数退避
    当下游服务出现问题时,并不总是能很快恢复。当下游服务尝试恢复时,您不想做的就是在短时间内多次命中它。指数退避的工作原理是在每次我们的服务尝试调用下游服务时添加一些额外的退避时间。

    例如,我们可以将重试机制配置为 5 秒初始退避,并在每次尝试时添加 2 作为乘数。这意味着当我们对下游服务的第一次调用失败时,我们的服务将在下一次调用之前等待 5 秒。如果第二次调用再次失败,服务将在下一次调用之前等待 10 秒而不是 5 秒。

    由于其较长的间隔性质,指数退避不适合重试用户请求。但它非常适合通知、发送电子邮件或 Webhook 系统等后台进程。

    3、随机退避
    随机退避 是一种在退避间隔计算中引入随机性的退避策略。假设您的服务流量激增。然后,您的服务会为每个请求调用下游服务,然后您会从中收到错误,因为下游服务会被您的请求淹没。您的服务实现了重试机制,并将在 5 秒内重试请求。但有一个问题:当需要重试请求时,所有请求都会立即重试,您可能会再次从下游服务收到错误。利用随机退避机制引入的随机性,您可以避免这种情况。

    随机退避策略将通过引入随机重试值来帮助您的服务将请求平衡到下游服务。假设您将重试机制配置为 5 秒间隔和两次重试。如果第一次调用失败,可以在 500ms 后尝试第二次调用;如果再次失败,则可以在 3.8 秒后尝试第三次。如果许多请求使下游服务失败,则不会同时重试它们。

    在哪里存储重试状态
    进行重试时,您需要将重试的状态存储在某处。状态包括已进行的重试次数、要重试的请求以及要保存的其他元数据。一般来说,可以使用三个地方来存储重试状态,如下所示:

    1、线程
    线程是存储重试状态最常见的地方。如果您使用具有内置重试机制的库,它很可能会使用 来Thread存储状态。最简单的方法就是让线程sleep。

    让我们看一个 Java 中的例子:

    int retryCount = 0;

    while (retryCount < 3) {
        try {
            thirdPartyOutboundService.getData();
        } catch (Exception e) {
            retryCount += 1;
            Thread.sleep(3000);
        }
    }

    上面的代码基本上是在出现异常时sleep线程,然后再次调用进程。这种方法虽然简单,但缺点是会阻塞线程,导致其他进程无法使用该线程。这种方法适用于间隔时间较短的固定延迟策略,如直接响应用户并需要尽快响应的进程。

    消息传递
    我们可以使用 RabbitMQ(延迟队列)等流行的消息传递代理来保存重试状态。从上游收到请求后,如果处理失败(可能是下游服务的原因,也可能不是),可以将消息发布到延迟队列,以便稍后(根据延迟情况)再处理。

    使用消息传递保存重试状态适用于后台进程请求,因为上游服务无法直接获取重试进程的响应。使用这种方法的好处是通常很容易实现,因为代理/库已经支持重试功能。消息传递作为重试状态的存储系统,在分布式系统中也能很好地发挥作用。可能发生的一个问题是,你的服务在等待下一次重试时突然出现停机等问题。通过将重试状态保存在消息代理中,您的服务可以在问题解决后继续重试。


    数据库
    数据库是存储重试状态的最可定制的解决方案,可以使用持久性存储或内存 KV 存储(如 Redis)。当下游服务请求失败时,可以将数据保存在数据库中,并使用 cron 作业每秒或每分钟检查数据库,重试失败的消息。

    虽然这是最可定制的解决方案,但实施成本会很高,因为你需要实现重试机制。您可以在服务中创建该机制,但在重试时会牺牲一些性能,或者为重试目的创建一个全新的服务。

    总结
    本文探讨了什么是重试模式以及在实施重试模式时应考虑哪些方面。

    您需要了解什么请求以及如何重试。如果重试机制实施得当,将有助于改善用户体验和减少所构建服务的运行。但是,如果重试机制不正确,就有可能导致用户体验恶化和业务错误。您需要了解请求何时可以重试以及如何重试,这样才能正确实施该机制。

    在本文中,我们介绍了重试模式。这种模式提高了系统的下游弹性,但下游弹性的意义远不止于此。我们可以将重试模式与超时(我们在本文中探讨过)和断路器结合起来,使我们的系统对下游故障更具弹性。