SmallRye Fault Tolerance
微服务的分布式特性带来的一个挑战是与外部系统的通信本质上不可靠。这增加了对应用程序弹性的需求。为了简化构建更具弹性的应用程序,Quarkus 提供了 SmallRye Fault Tolerance,它是 MicroProfile Fault Tolerance 规范的实现。
在本文档中,我们演示了 MicroProfile 容错注解(例如 @Timeout
、@Fallback
、@Retry
、@CircuitBreaker
和 @RateLimit
)的用法。
- Prerequisites
- The Scenario
- Solution
- Creating the Maven Project
- Preparing an Application: REST Endpoint and CDI Bean
- Adding Resiliency: Retries
- Adding Resiliency: Timeouts
- Adding Resiliency: Fallbacks
- Adding Resiliency: Circuit Breaker
- Adding Resiliency: Rate Limits
- Runtime configuration
- Conclusion
- Additional resources
Prerequisites
如要完成本指南,您需要:
-
Roughly 15 minutes
-
An IDE
-
安装了 JDK 17+,已正确配置
JAVA_HOME
-
Apache Maven ${proposed-maven-version}
-
如果你想使用 Quarkus CLI, 则可以选择使用
-
如果你想构建一个本机可执行文件(或如果你使用本机容器构建,则使用 Docker),则可以选择安装 Mandrel 或 GraalVM 以及 configured appropriately
The Scenario
本文档中构建的应用程序模拟了一个美食咖啡电子商店的简单后端。它实现了一个 REST 端点,提供我们商店中咖啡样品的相关信息。
让我们想象一下,虽然没有像这样实现,但我们端点中的一些方法需要与外部服务(如数据库或外部微服务)进行通信,这引入了不可靠性因素。
Solution
我们建议您遵循接下来的部分中的说明,按部就班地创建应用程序。然而,您可以直接跳到完成的示例。
克隆 Git 存储库: git clone $${quickstarts-base-url}.git
,或下载 $${quickstarts-base-url}/archive/main.zip[存档]。
解决方案位于 microprofile-fault-tolerance-quickstart
directory 中。
Creating the Maven Project
首先,我们需要一个新项目。使用以下命令创建一个新项目:
quarkus create app {create-app-group-id}:{create-app-artifact-id} \
--no-code
cd {create-app-artifact-id}
要创建一个 Gradle 项目,添加 --gradle
或 --gradle-kotlin-dsl
选项。
有关如何安装和使用 Quarkus CLI 的详细信息,请参见 Quarkus CLI 指南。
mvn {quarkus-platform-groupid}:quarkus-maven-plugin:{quarkus-version}:create \
-DprojectGroupId={create-app-group-id} \
-DprojectArtifactId={create-app-artifact-id} \
-DnoCode
cd {create-app-artifact-id}
要创建一个 Gradle 项目,添加 -DbuildTool=gradle
或 -DbuildTool=gradle-kotlin-dsl
选项。
适用于 Windows 用户:
-
如果使用 cmd,(不要使用反斜杠
\
,并将所有内容放在同一行上) -
如果使用 Powershell,将
-D
参数用双引号引起来,例如"-DprojectArtifactId={create-app-artifact-id}"
此命令会生成一个项目,导入 Quarkus REST(以前称为 RESTEasy Reactive)/Jakarta REST 和 SmallRye Fault Tolerance 的扩展。
如果您已经配置了 Quarkus 项目,则可以通过在项目基础目录中运行以下命令,将 smallrye-fault-tolerance
扩展添加到您的项目中:
quarkus extension add {add-extension-extensions}
./mvnw quarkus:add-extension -Dextensions='{add-extension-extensions}'
./gradlew addExtension --extensions='{add-extension-extensions}'
这会将以下内容添加到构建文件中:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-fault-tolerance</artifactId>
</dependency>
implementation("io.quarkus:quarkus-smallrye-fault-tolerance")
Preparing an Application: REST Endpoint and CDI Bean
在本节中,我们将创建应用程序的框架,以便我们能够扩展它并在以后向其添加容错功能。
首先,创建表示商店中咖啡样品的简单实体:
package org.acme.microprofile.faulttolerance;
public class Coffee {
public Integer id;
public String name;
public String countryOfOrigin;
public Integer price;
public Coffee() {
}
public Coffee(Integer id, String name, String countryOfOrigin, Integer price) {
this.id = id;
this.name = name;
this.countryOfOrigin = countryOfOrigin;
this.price = price;
}
}
让我们继续使用一个简单的 CDI bean,它可以充当我们咖啡样品的存储库。
package org.acme.microprofile.faulttolerance;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class CoffeeRepositoryService {
private Map<Integer, Coffee> coffeeList = new HashMap<>();
public CoffeeRepositoryService() {
coffeeList.put(1, new Coffee(1, "Fernandez Espresso", "Colombia", 23));
coffeeList.put(2, new Coffee(2, "La Scala Whole Beans", "Bolivia", 18));
coffeeList.put(3, new Coffee(3, "Dak Lak Filter", "Vietnam", 25));
}
public List<Coffee> getAllCoffees() {
return new ArrayList<>(coffeeList.values());
}
public Coffee getCoffeeById(Integer id) {
return coffeeList.get(id);
}
public List<Coffee> getRecommendations(Integer id) {
if (id == null) {
return Collections.emptyList();
}
return coffeeList.values().stream()
.filter(coffee -> !id.equals(coffee.id))
.limit(2)
.collect(Collectors.toList());
}
}
最后,按照以下方式创建 org.acme.microprofile.faulttolerance.CoffeeResource
类:
package org.acme.microprofile.faulttolerance;
import java.util.List;
import java.util.Random;
import java.util.concurrent.atomic.AtomicLong;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import org.jboss.logging.Logger;
@Path("/coffee")
public class CoffeeResource {
private static final Logger LOGGER = Logger.getLogger(CoffeeResource.class);
@Inject
CoffeeRepositoryService coffeeRepository;
private AtomicLong counter = new AtomicLong(0);
@GET
public List<Coffee> coffees() {
final Long invocationNumber = counter.getAndIncrement();
maybeFail(String.format("CoffeeResource#coffees() invocation #%d failed", invocationNumber));
LOGGER.infof("CoffeeResource#coffees() invocation #%d returning successfully", invocationNumber);
return coffeeRepository.getAllCoffees();
}
private void maybeFail(String failureLogMessage) {
if (new Random().nextBoolean()) {
LOGGER.error(failureLogMessage);
throw new RuntimeException("Resource failure.");
}
}
}
此时,我们公开了单个 REST 方法,该方法将以 JSON 格式显示咖啡样品列表。请注意,我们在 CoffeeResource#maybeFail()
方法中引入了一些产生错误的代码,这将导致大约 50% 的请求在 CoffeeResource#coffees()
端点方法中发生故障。
何不检查一下我们的应用程序是否工作?使用以下命令运行 Quarkus 开发服务器:
quarkus dev
./mvnw quarkus:dev
./gradlew --console=plain quarkusDev
并在浏览器中打开 http://localhost:8080/coffee
。进行几个请求(请记住,其中一些请求可能会失败)。至少有些请求应该会向我们展示 JSON 中的咖啡样本列表,而其余的会以抛出的 RuntimeException
为 CoffeeResource#maybeFail()
失败。
恭喜,你刚刚制作了一个有效的(尽管有点不可靠)Quarkus 应用程序!
Adding Resiliency: Retries
保持 Quarkus 开发服务器运行,并在 IDE 中将 @Retry
注释添加到 `CoffeeResource#coffees()`方法中,如下所示,然后保存文件:
import org.eclipse.microprofile.faulttolerance.Retry;
...
public class CoffeeResource {
...
@GET
@Retry(maxRetries = 4)
public List<Coffee> coffees() {
...
}
...
}
在浏览器中点击刷新。Quarkus 开发服务器将自动检测这些更改,并自动为你重新编译应用程序,因此无需重新启动它。
你可以再点击刷新几次。实际上,几乎所有的请求现在都应该成功了。CoffeeResource#coffees()
方法实际上在 50% 的时间内仍然失败,但每次发生这种情况时,该平台都会自动重试调用!
若要查看仍然出现的故障,请查看开发服务器的输出。日志消息应类似于以下内容:
2019-03-06 12:17:41,725 INFO [org.acm.fau.CoffeeResource] (XNIO-1 task-1) CoffeeResource#coffees() invocation #5 returning successfully
2019-03-06 12:17:44,187 INFO [org.acm.fau.CoffeeResource] (XNIO-1 task-1) CoffeeResource#coffees() invocation #6 returning successfully
2019-03-06 12:17:45,166 ERROR [org.acm.fau.CoffeeResource] (XNIO-1 task-1) CoffeeResource#coffees() invocation #7 failed
2019-03-06 12:17:45,172 ERROR [org.acm.fau.CoffeeResource] (XNIO-1 task-1) CoffeeResource#coffees() invocation #8 failed
2019-03-06 12:17:45,176 INFO [org.acm.fau.CoffeeResource] (XNIO-1 task-1) CoffeeResource#coffees() invocation #9 returning successfully
你可以看到,每次调用失败后,会立即接收到另一个调用,直到成功。由于我们允许 4 次重试,因此为了实际向用户显示故障,它需要连续失败 5 次调用。这不太可能发生。
Adding Resiliency: Timeouts
那么,MicroProfile 容错中还有什么?我们来看看超时。
将以下两个方法添加到我们的 CoffeeResource
端点。同样,无需重新启动服务器,只需粘贴代码并保存文件即可。
import org.eclipse.microprofile.faulttolerance.Timeout;
...
public class CoffeeResource {
...
@GET
@Path("/{id}/recommendations")
@Timeout(250)
public List<Coffee> recommendations(int id) {
long started = System.currentTimeMillis();
final long invocationNumber = counter.getAndIncrement();
try {
randomDelay();
LOGGER.infof("CoffeeResource#recommendations() invocation #%d returning successfully", invocationNumber);
return coffeeRepository.getRecommendations(id);
} catch (InterruptedException e) {
LOGGER.errorf("CoffeeResource#recommendations() invocation #%d timed out after %d ms",
invocationNumber, System.currentTimeMillis() - started);
return null;
}
}
private void randomDelay() throws InterruptedException {
Thread.sleep(new Random().nextInt(500));
}
}
我们添加了一些新功能。我们希望能够根据用户当前正在查看的咖啡推荐一些相关的咖啡。这不是关键功能,而是锦上添花。当系统超载并且执行获取建议背后的逻辑花费的时间过长时,我们宁愿超时并渲染没有建议的 UI。
请注意,超时配置为 250 毫秒,并且在 CoffeeResource#recommendations()
方法中引入了 0 到 500 毫秒之间的随机人工延迟。
在浏览器中,转到 http://localhost:8080/coffee/2/recommendations
并点击刷新几次。
你应该会看到一些请求以 org.eclipse.microprofile.faulttolerance.exceptions.TimeoutException
超时。未超时的请求应以 JSON 格式显示两个推荐的咖啡样本。
Adding Resiliency: Fallbacks
让我们通过提供获取相关咖啡的备用(并且可能更快的)方式来让我们的推荐功能变得更好。
向 CoffeeResource
添加一个备用方法,并在 CoffeeResource#recommendations()
方法中添加一个 @Fallback
注释,如下所示:
import java.util.Collections;
import org.eclipse.microprofile.faulttolerance.Fallback;
...
public class CoffeeResource {
...
@Fallback(fallbackMethod = "fallbackRecommendations")
public List<Coffee> recommendations(int id) {
...
}
public List<Coffee> fallbackRecommendations(int id) {
LOGGER.info("Falling back to RecommendationResource#fallbackRecommendations()");
// safe bet, return something that everybody likes
return Collections.singletonList(coffeeRepository.getCoffeeById(1));
}
...
}
在 http://localhost:8080/coffee/2/recommendations
上点击刷新几次。TimeoutException
应该不再出现了。相反,在超时的情况下,页面将显示我们在备用方法 fallbackRecommendations()
中硬编码的单个建议,而不是原始方法返回的两个建议。
检查服务器输出以查看是否真的发生了备用:
2020-01-09 13:21:34,250 INFO [org.acm.fau.CoffeeResource] (executor-thread-1) CoffeeResource#recommendations() invocation #1 returning successfully
2020-01-09 13:21:36,354 ERROR [org.acm.fau.CoffeeResource] (executor-thread-1) CoffeeResource#recommendations() invocation #2 timed out after 250 ms
2020-01-09 13:21:36,355 INFO [org.acm.fau.CoffeeResource] (executor-thread-1) Falling back to RecommendationResource#fallbackRecommendations()
备用方法需要具备与原始方法相同的参数。 |
Adding Resiliency: Circuit Breaker
当系统的一部分暂时不稳定时,断路器对于限制系统中发生的故障数量非常有用。断路器会记录方法的成功调用和失败调用,当失败调用的比率达到指定阈值时,断路器会 opens 并阻止所有进一步调用该方法一段时间。
将以下代码添加到 CoffeeRepositoryService
中,以便演示断路器在操作中的情况:
import java.util.concurrent.atomic.AtomicLong;
import org.eclipse.microprofile.faulttolerance.CircuitBreaker;
...
public class CoffeeRepositoryService {
...
private AtomicLong counter = new AtomicLong(0);
@CircuitBreaker(requestVolumeThreshold = 4)
public Integer getAvailability(Coffee coffee) {
maybeFail();
return new Random().nextInt(30);
}
private void maybeFail() {
// introduce some artificial failures
final Long invocationNumber = counter.getAndIncrement();
if (invocationNumber % 4 > 1) { // alternate 2 successful and 2 failing invocations
throw new RuntimeException("Service failed.");
}
}
}
并将下面的代码插入到 CoffeeResource
终端:
public class CoffeeResource {
...
@Path("/{id}/availability")
@GET
public Response availability(int id) {
final Long invocationNumber = counter.getAndIncrement();
Coffee coffee = coffeeRepository.getCoffeeById(id);
// check that coffee with given id exists, return 404 if not
if (coffee == null) {
return Response.status(Response.Status.NOT_FOUND).build();
}
try {
Integer availability = coffeeRepository.getAvailability(coffee);
LOGGER.infof("CoffeeResource#availability() invocation #%d returning successfully", invocationNumber);
return Response.ok(availability).build();
} catch (RuntimeException e) {
String message = e.getClass().getSimpleName() + ": " + e.getMessage();
LOGGER.errorf("CoffeeResource#availability() invocation #%d failed: %s", invocationNumber, message);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(message)
.type(MediaType.TEXT_PLAIN_TYPE)
.build();
}
}
...
}
我们添加了另一项功能——应用程序可以在我们的商店中返回指定咖啡的剩余包装数量(只是一个随机数)。
这次在 CDI bean 中引入了一个人为的故障: CoffeeRepositoryService#getAvailability()
方法将在两次成功的调用和两次失败的调用之间进行交替。
我们还添加了一个带有 requestVolumeThreshold = 4
的 @CircuitBreaker
注解。 CircuitBreaker.failureRatio
默认为 0.5,而 CircuitBreaker.delay
默认为 5 秒。这意味着当最近 4 次调用中 2 次失败时,断路器将打开,且将保持打开状态 5 秒钟。
要进行测试,请执行以下操作:
-
在浏览器中转到
http://localhost:8080/coffee/2/availability
。你应该会看到返回一个数字。 -
点击刷新,此二次请求也应成功并返回一个数字。
-
再刷新两次。这两次你都应该看到文本“RuntimeException:服务失败”,这是
CoffeeRepositoryService#getAvailability()
抛出的异常。 -
再刷新几次。除非等待时间过长,否则你应该会再次看到异常,但这一次是“CircuitBreakerOpenException:getAvailability”。此异常表明断路器已经打开,且
CoffeeRepositoryService#getAvailability()
方法不再被调用。 -
让它保持 5 秒钟,在此期间断路器应关闭,而你应能够再次发出两次成功的请求。
Adding Resiliency: Rate Limits
这是 SmallRye Fault Tolerance 的一个附加功能,并未由 MicroProfile 容错机制指定。
可以使用 rate limit 来防止某个操作被执行得太频繁。速率限制会在一段时间内强制执行允许调用的最大次数。例如,通过速率限制,你可以确保某个方法每分钟只可以调用 50 次。超过此限制的调用将被拒绝,并抛出 RateLimitException
类型的异常。
此外,可以在调用之间定义最小间隔。例如,对于 1 秒的最小间隔,如果在第一个调用后 500 毫秒发生第二个调用,则会拒绝该调用,即使尚未超过限制。
速率限制表面上与隔离仓(并发限制)类似,但实际上有很大不同。隔离仓限制在任何时间点并发发生的执行次数。速率限制限制一段时间内执行的次数,而不考虑并发性。
速率限制需要在调用之间保持某种状态:最近调用的次数、上次调用的时间戳,等等。此状态是一个单例,与使用 @RateLimit
注解的 bean 的生命周期无关。
更具体地说,速率限制状态由 bean 类 (java.lang.Class
) 和表示受保护方法的方法对象 (java.lang.reflect.Method
) 的组合唯一标识。
让 Quarkus 开发模式运行,并在你的 IDE 中将 @RateLimit
注解添加到 CoffeeResource#coffees()
方法,如下所示并保存文件:
import java.time.temporal.ChronoUnit;
import io.smallrye.faulttolerance.api.RateLimit;
...
public class CoffeeResource {
...
@GET
@RateLimit(value = 2, window = 10, windowUnit = ChronoUnit.SECONDS)
public List<Coffee> coffees() {
...
}
...
}
点击浏览器中的刷新。 Quarkus 开发服务器将自动检测更改并重新编译应用程序,因此无需重新启动它。
可以再点击几次刷新。在 10 秒间隔内发出 2 个请求后,你应该会开始在日志中看到错误,类似于以下错误:
INFO [org.acm.mic.fau.CoffeeResource] (executor-thread-1) CoffeeResource#coffees() invocation #1 returning successfully
ERROR [io.qua.ver.htt.run.QuarkusErrorHandler] (executor-thread-1) HTTP Request to /coffee failed, error id: d3e59090-fd45-4c67-844e-80a8f7fa6ee0-4: io.smallrye.faulttolerance.api.RateLimitException: org.acme.microprofile.faulttolerance.CoffeeResource#coffees rate limit exceeded
at io.smallrye.faulttolerance.core.rate.limit.RateLimit.doApply(RateLimit.java:58)
at io.smallrye.faulttolerance.core.rate.limit.RateLimit.apply(RateLimit.java:44)
at io.smallrye.faulttolerance.FaultToleranceInterceptor.syncFlow(FaultToleranceInterceptor.java:255)
at io.smallrye.faulttolerance.FaultToleranceInterceptor.intercept(FaultToleranceInterceptor.java:182)
at io.smallrye.faulttolerance.FaultToleranceInterceptor_Bean.intercept(Unknown Source)
at io.quarkus.arc.impl.InterceptorInvocation.invoke(InterceptorInvocation.java:42)
at io.quarkus.arc.impl.AroundInvokeInvocationContext.perform(AroundInvokeInvocationContext.java:30)
at io.quarkus.arc.impl.InvocationContexts.performAroundInvoke(InvocationContexts.java:27)
at org.acme.microprofile.faulttolerance.CoffeeResource_Subclass.coffees(Unknown Source)
at org.acme.microprofile.faulttolerance.CoffeeResource$quarkusrestinvoker$coffees_73d7590ab944adfa130e4ad57c30282f825b2d18.invoke(Unknown Source)
at org.jboss.resteasy.reactive.server.handlers.InvocationHandler.handle(InvocationHandler.java:29)
at io.quarkus.resteasy.reactive.server.runtime.QuarkusResteasyReactiveRequestContext.invokeHandler(QuarkusResteasyReactiveRequestContext.java:141)
at org.jboss.resteasy.reactive.common.core.AbstractResteasyReactiveContext.run(AbstractResteasyReactiveContext.java:147)
at io.quarkus.vertx.core.runtime.VertxCoreRecorder$14.runWith(VertxCoreRecorder.java:599)
at org.jboss.threads.EnhancedQueueExecutor$Task.doRunWith(EnhancedQueueExecutor.java:2516)
at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2495)
at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1521)
at org.jboss.threads.DelegatingRunnable.run(DelegatingRunnable.java:11)
at org.jboss.threads.ThreadLocalResettingRunnable.run(ThreadLocalResettingRunnable.java:11)
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
at java.base/java.lang.Thread.run(Thread.java:1583)
如果 @Fallback
与 @RateLimit
结合使用,则当抛出 RateLimitException
时,可能会调用后备方法或处理程序,具体取决于后备配置。
如果 @Retry
与 @RateLimit
结合使用,那么限速会将每次重试尝试视为独立的调用来处理。如果抛出 RateLimitException
,则可能会重试执行,具体取决于如何配置重试。
如果 @CircuitBreaker
与 @RateLimit
结合使用,那么在强制执行限速前会检查熔断器。如果限速导致 RateLimitException
,这可能会被计算为失败,具体取决于如何配置熔断器。
Runtime configuration
可以在 application.properties
文件内在运行时覆盖注释参数。
如果采用我们已经看过的重试示例:
package org.acme;
import org.eclipse.microprofile.faulttolerance.Retry;
...
public class CoffeeResource {
...
@GET
@Retry(maxRetries = 4)
public List<Coffee> coffees() {
...
}
...
}
可以通过以下配置项,将 maxRetries
参数从 4 次重试覆盖为 6 次重试:
org.acme.CoffeeResource/coffees/Retry/maxRetries=6
格式为 |
Conclusion
SmallRye Fault Tolerance 允许改善应用程序的弹性,而不会影响业务逻辑的复杂性。
在 Quarkus 中启用容错功能所需的一切是:
-
使用
quarkus-maven-plugin
将smallrye-fault-tolerance
Quarkus 扩展添加到项目中:include::./_includes/devtools/extension-add.adoc[] -
或只需添加以下 Maven 依赖项:[source, xml] .pom.xml
<dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-smallrye-fault-tolerance</artifactId> </dependency>
implementation("io.quarkus:quarkus-smallrye-fault-tolerance")
Additional resources
SmallRye Fault Tolerance 具有此处显示的更多功能。请查看 SmallRye Fault Tolerance documentation 以了解它们。
在 Quarkus 中,你可以开箱即用地使用 SmallRye Fault Tolerance 可选功能。
支持 Mutiny,因此除了 CompletionStage
之外异步方法还可以返回 Uni
。
MicroProfile Context Propagation 已与 Fault Tolerance 集成,因此现有上下文会自动传播到异步方法。
这也适用于 CDI 请求上下文:如果它在原始线程上处于活动状态,则它将传播到新线程,但如果不是,那么新线程也不会拥有它。这与 MicroProfile Fault Tolerance 规范相反,该规范指出请求上下文必须在 |
默认情况下启用非兼容模式。这意味着,返回 CompletionStage
(或 Uni
)的方法已应用了无任何 @Asynchronous
、 @AsynchronousNonBlocking
、 @Blocking
或 @NonBlocking
注解的异步容错。这也意味着断路器、后备和重试功能会自动检查异常原因链,如果异常本身不足以决策后续操作。
此模式与 MicroProfile 容错规范不兼容,但这种不兼容性很小。若要恢复完全兼容性,请添加此配置属性:
|
存在 programmatic API ,包括 Mutiny 支持,并且与基于声明和基于注释的 API 相集成。你可以开箱即用地使用 FaultTolerance
和 MutinyFaultTolerance
API。
目前支持 Kotlin(假定你使用 Kotlin 的 Quarkus 扩展),因此你可以使用容错注解保护 suspend
函数。
度量标准会自动发现并集成。如果你的应用程序使用 Micrometer 的 Quarkus 扩展,则 SmallRye 容错会向 Micrometer 发送度量标准。如果你的应用程序使用 SmallRye 度量标准的 Quarkus 扩展,则 SmallRye 容错会向 MicroProfile 度量标准发送度量标准。否则,将禁用 SmallRye 容错度量标准。