前言

微服务本质上分布式架构,当我们使用分布式系统时任何不可预知的问题都会发生(例如网络可用性问题、服务可用性问题、中间件可用性问题)。一个系统的问题可能会直接影响另外一个系统的使用或性能。所以在系统设计过程既要保证自身运行的弹性需求,也要避免对下游服务级联故障。

重试模式

在微服务技术架构中,当有多个服务(A,B,C ,D)时,一个服务(A)可能依赖于另一服务(B),而另一服务(B)又可能依赖于 C,依此类推。有时由于某些问题,服务 D 可能无法按预期响应。服务 D 可能引发了某些异常,例如 OutOfMemory Error 或 Internal Server Error。此类异常会影响下游服务,这可能导致用户体验较差,如下所示。



微服务登录到服务过程 微服务重试_github

1607306376

类似于网络通信异常等,大部分情况通过刷新再次请求服务接口即可解决。在微服务架构中,生产环境针对服务 D 部署了多个实例,并通过负载均衡等实现服务 D 高可用。如果其中一个实例网络请求有问题,无法响应期望结果,我们可以通过重试该请求,通过负载均衡器将请求发送至运行状态良好的实例获取正确结果。因此通过“重试”模式,可以使得应用可用性得到保证。



微服务登录到服务过程 微服务重试_参考资料_02

1607306392

示例程序

架构图



微服务登录到服务过程 微服务重试_微服务登录到服务过程_03

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]等)