前言
微服务本质上分布式架构,当我们使用分布式系统时任何不可预知的问题都会发生(例如网络可用性问题、服务可用性问题、中间件可用性问题)。一个系统的问题可能会直接影响另外一个系统的使用或性能。所以在系统设计过程既要保证自身运行的弹性需求,也要避免对下游服务级联故障。
重试模式
在微服务技术架构中,当有多个服务(A,B,C ,D)时,一个服务(A)可能依赖于另一服务(B),而另一服务(B)又可能依赖于 C,依此类推。有时由于某些问题,服务 D 可能无法按预期响应。服务 D 可能引发了某些异常,例如 OutOfMemory Error 或 Internal Server Error。此类异常会影响下游服务,这可能导致用户体验较差,如下所示。
1607306376
类似于网络通信异常等,大部分情况通过刷新再次请求服务接口即可解决。在微服务架构中,生产环境针对服务 D 部署了多个实例,并通过负载均衡等实现服务 D 高可用。如果其中一个实例网络请求有问题,无法响应期望结果,我们可以通过重试该请求,通过负载均衡器将请求发送至运行状态良好的实例获取正确结果。因此通过“重试”模式,可以使得应用可用性得到保证。
1607306392
示例程序
架构图
1607306406
如上图所示,简单模拟电商下单逻辑
- 用户登录浏览商品 (商品库存模块)
- 扣减商品库存 (商品库存模块)
- 创建商品订单 (订单模块)
product-service 通过调用 order-service 服务下单
代码实现
├── retry-demo
├── order-service #订单服务 (8070)
└── product-service #商品库存服务 (8050)
├── retry-demo
├── order-service #订单服务 (8070)
└── product-service #商品库存服务 (8050)
- 依赖说明。由于 hystrix 年久失修,这里使用 resilience4j 断路保护器做演示
<dependency>
<groupId>io.github.resilience4jgroupId>
<artifactId>resilience4j-spring-boot2artifactId>
<version>1.6.1version>
dependency>
<dependency>
<groupId>io.github.resilience4jgroupId>
<artifactId>resilience4j-spring-boot2artifactId>
<version>1.6.1version>
dependency>
- 针对接口定义重试策略
resilience4j.retry:
instances:
ratingService:
maxRetryAttempts: 1 #重试策略
retryExceptions: #针对哪些异常进行重试
- org.springframework.web.client.HttpServerErrorException
resilience4j.retry:
instances:
ratingService:
maxRetryAttempts: 1 #重试策略
retryExceptions: #针对哪些异常进行重试
- org.springframework.web.client.HttpServerErrorException
- 消费方接口。product-service 8050
/**
* 用户点击购买
*/
@SneakyThrows
@GetMapping("/order")
public String buy() {
// 模拟调用 订单服务下单
orderService.createOrder().get();
return "success";
}
/**
* 用户点击购买
*/
@SneakyThrows
@GetMapping("/order")
public String buy() {
// 模拟调用 订单服务下单
orderService.createOrder().get();
return "success";
}
- 定义远程调用类使用 Retry 包装
/**
* 创建订单
* name: 指定接口重试配置名称
* fallbackMethod: 重试后降级方法
*/
@Bulkhead(name = "createOrder", fallbackMethod = "getError")
public CompletableFuture createOrder() {
return CompletableFuture.supplyAsync(() -> restTemplate.getForEntity("http://localhost:8070/createOrder"
, String.class).getBody());
}
public CompletableFuture getError(Throwable error) {
log.warn("创建订单失败了 {}", error.getMessage());
return CompletableFuture.completedFuture("");
}
/**
* 创建订单
* name: 指定接口重试配置名称
* fallbackMethod: 重试后降级方法
*/
@Bulkhead(name = "createOrder", fallbackMethod = "getError")
public CompletableFuture createOrder() {
return CompletableFuture.supplyAsync(() -> restTemplate.getForEntity("http://localhost:8070/createOrder"
, String.class).getBody());
}
public CompletableFuture getError(Throwable error) {
log.warn("创建订单失败了 {}", error.getMessage());
return CompletableFuture.completedFuture("");
}
- 服务提供方。order-service 8070
@RestController
public class PayController {
private static int num = 0;
@SneakyThrows
@GetMapping("/createOrder")
public String createOrder() {
log.info("开始请求创建订单接口 {}", ++num);
// 请求次数奇数模拟创建订单异常
if (num % 2 != 0) {
throw new RuntimeException();
}
return "创建订单服务";
}
}
@RestController
public class PayController {
private static int num = 0;
@SneakyThrows
@GetMapping("/createOrder")
public String createOrder() {
log.info("开始请求创建订单接口 {}", ++num);
// 请求次数奇数模拟创建订单异常
if (num % 2 != 0) {
throw new RuntimeException();
}
return "创建订单服务";
}
}
开始测试
- 请求商品服务,返回成功
curl http://localhost:8050/order
success⏎
curl http://localhost:8050/order
success⏎
- 订单服务日志,由于是第一次请求触发异常,然后服务调用方自动重试产生第二次调用。
2020-12-06 15:50:07.664 INFO 25846 --- [nio-8070-exec-1] c.e.o.controller.OrderController : 开始请求创建订单接口 1
2020-12-06 15:50:07.686 ERROR 25846 --- [nio-8070-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.RuntimeException] with root cause
... 异常日志 ...
2020-12-06 15:50:08.271 INFO 25846 --- [nio-8070-exec-2] c.e.o.controller.OrderController : 开始请求创建订单接口 2
2020-12-06 15:50:07.664 INFO 25846 --- [nio-8070-exec-1] c.e.o.controller.OrderController : 开始请求创建订单接口 1
2020-12-06 15:50:07.686 ERROR 25846 --- [nio-8070-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.RuntimeException] with root cause
... 异常日志 ...
2020-12-06 15:50:08.271 INFO 25846 --- [nio-8070-exec-2] c.e.o.controller.OrderController : 开始请求创建订单接口 2
总结
重试模式可以通过编码的形式自动发起重试,避免终端用户手动刷新体验问题。但重试模式的缺点是会造成整体的响应时间,部分业务逻辑不能适用(比如幂等接口[1]等)