前言

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

超时模式

如下图在微服务架构中,当存在多个服务(A,B,C,D),服务 A 依赖于服务 B,而服务 B 依赖于服务 C,依次类推。由于网络可用性问题,导致终点服务(服务 D)不能即使返回,最终导致服务 A 作为调用方 线程一直处于阻塞状态。

「微服务设计之禅」超时模式_java

所以为了避免上图所述的线程阻塞的问题,我们建议在依赖服务之间通过设置调用超时来避免服务缓慢或者不可用的问题。

超时设置的好处
即使被调用方服务不可用,也能保证消费方服务始终正在运行
避免消费方服务无限期等待
避免阻塞当前线程

示例程序

架构说明

「微服务设计之禅」超时模式_java_02

  • 如上图所示,简单模拟电商支付下单逻辑


  1. 用户登录浏览商品 (商品库存模块)
  2. 扣减商品库存 (商品库存模块)
  3. 创建商品订单 (订单模块)
  4. 调用支付模块支付 (支付模块)

代码实现

├── timeout-demo
   ├── order-service        #订单服务  (8070)
   ├── pay-service          #支付服务  (8060)
   └── product-service      #商品库存服务  (8050)
  • 依赖说明。由于 hystrix 年久失修,这里使用 resilience4j 断路保护器做演示
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-spring-boot2</artifactId>
    <version>1.6.1</version>
</dependency>
  • 消费方定义超时策略
# 超时参数配置
resilience4j:
  timelimiter:
    instances:
      createOrder: # 接口名称
        timeoutDuration: 5s #超时时间
      pay: # 接口名称
        timeoutDuration: 3s #超时时间
  • 消费方接口。product-service 8050
@GetMapping("/buy")
public String buy() {
  log.info("--> 开始调用 ");
  // 模拟调用 订单服务下单
  orderService.createOrder()
          // 模拟调用 支付服务支付
          .thenApply(orderNo -> payService.pay()).get()
          .get();
  log.info("--> 结束调用 ");
  return "success";
}
/**
 * 创建订单
 * name: 指定接口超时配置名称
 * fallbackMethod: 超时后降级方法
 */

@TimeLimiter(name = "createOrder", fallbackMethod = "getError")
public CompletableFuture<String> createOrder() {
    return CompletableFuture.supplyAsync(() -> restTemplate.getForEntity("http://localhost:8070/createOrder"
            , String.class).getBody());
}

/**
 * 支付
 */

@TimeLimiter(name = "pay", fallbackMethod = "getError")
public CompletableFuture<String> pay() {
    return CompletableFuture.supplyAsync(() -> restTemplate.getForEntity("http://localhost:8060/pay"
            , String.class).getBody());
}

/**
 * 超时后执行降级方法
 */

public CompletableFuture<String> getError(Throwable error) {
    log.warn("失败 {}", error.getMessage());
    return CompletableFuture.completedFuture("");
}
  • 服务提供方。order-service 8070 /pay-service 8060
@RestController
public class PayController {
    @SneakyThrows
    @GetMapping("/pay")
    public String pay(){
        // 模拟调用支付渠道耗时 10s
        Thread.sleep(10000);
        return "支付成功";
    }
}

@RestController
public class OrderController {
    @SneakyThrows
    @GetMapping("/createOrder")
    public String createOrder(){
        // 模拟创建订单耗时
        Thread.sleep(10000);
        return "创建订单服务";
    }
}

使用示例

通过以上代码,我们实现了一个简单的下单逻辑,通过调用商品服务接口,会自动调用支付、订单服务接口。订单、支付服务提供方 处理的耗时分别为 10S ,但服务消费方(商品服务) 针对 createOrder、pay 接口的最大超时时间为 5S、3S。所以当用户调用商品服务的 BUY 会在 8S 内获得返回。

测试如下

curl http://localhost:8050/buy

日志输出如下:8 秒超时按照降级方法返回。

2020-12-05 14:09:34.605  ProductController       : --> 开始调用
2020-12-05 14:09:39.626  OrderService   : 创建订单失败了 TimeLimiter 'createOrder' recorded a timeout exception.
2020-12-05 14:09:42.644  PayService   : 支付订单失败 TimeLimiter 'pay' recorded a timeout exception.
2020-12-05 14:09:42.645ProductController       : --> 结束调用

总结

  • 通过引入 resilience4j 包装接口,实现对指定调用超时设置,保证线程不会被阻塞

  • 核心服务不会因为下游服务超时而被影响造成性能问题

  • 保证应用返回时间在固定时间窗内

「微服务设计之禅」超时模式_java_03

问题分析:

  • 下游服务不可用的状态下,线程仍需要阻塞,当并发请求很多时,也会造成性能瓶颈

「微服务设计之禅」超时模式_java_04

  • 如上问题可以通过隔离模式来实现影响最小化,下篇再来讲解。