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 会订阅它。所以,别搞错了,订阅是存在的,你可能看不到它。
这种惰性是重试的秘密。在下面的顺序图中,你可以看到请求在收到订阅者时发送。
这有一个有趣的后果,那就是如果你订阅两次,你就会发出两次请求,从而得到两次响应。
但让我们回到重试。什么是重试?重试是对失败的反应,所以你会这样写:uni.onFailure().retry()
。你还需要指明你想重试多久。
Uni<String> uni = invokePickyService(client)
.onFailure().retry().indefinitely();
uni.subscribe().with(
resp -> System.out.println("Success: " + resp)
);
在这个片段中,我们无限期重试,直到获得成功的结果。
但是,它在内部是如何工作的呢?很简单。如果遇到失败,它就会重新订阅。
它会一直重新订阅,直到获得成功的响应。换句话说,重试就是重新订阅。
我应该重试多少次?
这总是一个好问题。我应该无限期重试吗?最有可能不会。无限期可能非常长,如果被调用的服务持续失败,它可能永远不会结束。
你可以使用 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
次尝试。如果我们在这多次重新订阅后仍然遇到失败,则最后一次失败将被发送到订阅者。
你也可以使用 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.java
来jbang脚本,或者直接运行
jbang https://gist.github.com/cescoffier/e9abce907a1c3d05d70bea3dae6dc3d5
被调用的服务 50% 的时间会失败(嗯,它使用随机数,所以统计上是 50% 的时间)。
这篇博文到此结束。再次强调,在使用重试之前,请验证你的系统是否支持它。重试就是重新订阅。你可以配置重试的持续时间、次数以及何时进行尝试。Mutiny 提供了许多其他选项,例如 when
或在使用指数退避时使用截止日期(expireIn
和 expireAt
)。你可以使用提供的沙盒环境尝试所有这些选项。
敬请期待!我们还有很多其他精彩内容将在博客中讨论!