Mutiny - retry... 重试是如何工作的?

上周,Quarkus 用户 David 向我询问了重试异步操作的问题。David 有一个挑剔的远程 HTTP 服务,有时会表现不佳。答案很简单!只需执行:uni.onFailure().retry().atMost(x)。但是,David 好奇心很强,又问了我第二个问题:重试是如何工作的?嗯,这很简单;它只是重试……正如你所能想象的,这并没有满足 David 的好奇心。

在回答 David 的过程中,我意识到重试并非那么简单,值得进一步解释。这正是在这篇博文中我们将要探讨的内容。

关于重试的免责声明

好的,如果你是那种会跳过服务条款的读者,你可以跳到下一节。但对于其他人,我需要警告你关于重试。重试可能不是你问题的解决方案,因为它可能会带来糟糕的结果。只有当你的系统能够处理重复的请求或消息时,才能进行重试。换句话说,只有当你的系统是幂等的,即多次发送请求或消息不会改变整体状态时,你才能进行重试。如有疑问,在实现重试之前请先检查,因为它可能会损害你的系统。

免责声明已说明!让我们看看重试的内部机制。

重试就是重新订阅

让我们想象一下,你有一个 Uni 来表示你的异步操作,就像 David 的情况一样,调用远程服务。

Uni<String> uni = invokePickyService(client);

Uni 是惰性的。在有人订阅它之前,什么都不会发生。在我们的例子中,只有当有人订阅 uni 时,请求才会被发送到远程服务。所以要执行请求,我们需要订阅。

Uni<String> uni = invokePickyService(client);
uni.subscribe().with(
    resp -> System.out.println("Success: " + resp),
    failure -> System.out.println("Failed: " + failure.getMessage())
);

在 Quarkus 中,大多数时候,你返回 Uni,Quarkus 会订阅它。所以,别搞错了,订阅是存在的,你可能看不到它。

这种惰性是重试的秘密。在下面的顺序图中,你可以看到请求在收到订阅者时发送。

subscription

这有一个有趣的后果,那就是如果你订阅两次,你就会发出两次请求,从而得到两次响应。

double subscription

但让我们回到重试。什么是重试?重试是对失败的反应,所以你会这样写:uni.onFailure().retry()。你还需要指明你想重试多久。

Uni<String> uni = invokePickyService(client)
    .onFailure().retry().indefinitely();
uni.subscribe().with(
        resp -> System.out.println("Success: " + resp)
);

在这个片段中,我们无限期重试,直到获得成功的结果。

但是,它在内部是如何工作的呢?很简单。如果遇到失败,它就会重新订阅。

retry

它会一直重新订阅,直到获得成功的响应。换句话说,重试就是重新订阅。

我应该重试多少次?

这总是一个好问题。我应该无限期重试吗?最有可能不会。无限期可能非常长,如果被调用的服务持续失败,它可能永远不会结束。

你可以使用 atMost 配置重试次数。

 Uni<String> uni = invokePickyService(client)
    .onFailure().retry().atMost(2);
uni.subscribe().with(
        resp -> System.out.println("Success: " + resp),
        failure -> System.out.println("Failure: " + failure.getMessage())
);

atMost 表示在失败之前最多进行 n 次尝试。如果我们在这多次重新订阅后仍然遇到失败,则最后一次失败将被发送到订阅者。

retry failed

你也可以使用 until 并通过查看收到的失败来决定是否重试。

Uni<String> uni = invokePickyService(client)
    .onFailure().retry().until(failure -> ! (failure instanceof TooManyRequestsException));

奖励:指数退避

到目前为止,我们的重试是立即进行的。这可能不明智,并且稍微区分我们的重试可能会带来更好的结果,尤其是在面对由于负载或其他外部原因导致的间歇性故障时。使用指数退避可以提供合理的折衷。使用指数退避重试

  • 在初始延迟后重试,

  • 每次失败时,它都会使之前的延迟加倍,可以选择设置最大值,

  • 它可以使用的抖动(jitter)为延迟添加一个随机持续时间,

  • 如果需要,它还可以具有最大延迟,

  • 它仍然受到 atMost 的限制。

Uni<String> uni = invokePickyService(client)
    .onFailure().retry()
        .withBackOff(Duration.ofSeconds(1)).withJitter(0.2).atMost(10);

最后一个片段配置了带指数退避的重试。第一次重试发生在 1 秒后,然后每次都翻倍,没有上限。延迟应用了随机抖动。如果,不幸的是,在十次尝试后仍然失败,则该失败将被发送到订阅者。

演示时间!

好了,说得够多了。我构建了一个简单的沙盒环境,你可以在其中调整和尝试各种重试策略:https://gist.github.com/cescoffier/e9abce907a1c3d05d70bea3dae6dc3d5。你可以通过下载脚本并运行 jbang Retry.javajbang脚本,或者直接运行

jbang https://gist.github.com/cescoffier/e9abce907a1c3d05d70bea3dae6dc3d5

被调用的服务 50% 的时间会失败(嗯,它使用随机数,所以统计上是 50% 的时间)。

这篇博文到此结束。再次强调,在使用重试之前,请验证你的系统是否支持它。重试就是重新订阅。你可以配置重试的持续时间、次数以及何时进行尝试。Mutiny 提供了许多其他选项,例如 when 或在使用指数退避时使用截止日期(expireInexpireAt)。你可以使用提供的沙盒环境尝试所有这些选项。

敬请期待!我们还有很多其他精彩内容将在博客中讨论!