「微服务设计之禅」隔离模式

JAVA架构日记

共 2586字,需浏览 6分钟

 ·

2020-12-17 12:26

前言

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

隔离模式

9dad4a8f788e945c6b5ad18a04c55d21.webp

如上图,如果我们仔细观察船的结构时会发现,使用了多个隔板将船分为了多个小的隔间。隔板用于密封船体的各个部分,避免由于某个部位泄漏导致整个船淹没。当我们进行软件设计时,应该将整个应用拆分为多个组件,并且一个组件的故障不会影响到全局,这就所谓的隔离模式。

例如:假设有两个服务 A 和 B,服务 A 只能同时处理 5 个并发请求。服务 A 里面有两个接口

  • /a/b 依赖于服务 B (耗时比较多)

  • /a      本地服务

dee63e6581ace714cc03d7888901e71b.webp

当有 10 个并发请求 A 服务接口, 其中 5 个请求 /a/b接口 ,5 个请求 /a由于/a/b 需要调用服务 B 耗时比较多,可能会造成 5 个线程阻塞应用无法处理 /a 虽然只是本地处理。由于一个接口耗时占用了整个应用的资源导致其他接口无法使用,导致用户体验很差。

43cec0b9851b6efdc38ee378f4645186.webp

通过隔离模式分配特定接口的资源限制,避免资源瓶颈。如上图,将a/b 接口最大处理线程设置为 2,这样应用始终会有剩余线程处理其他接口请求。

示例程序

架构图

bfe79e3060058bb82c5764e5a320776a.webp

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

  • 用户登录浏览商品 (商品库存模块)
  • 扣减商品库存 (商品库存模块)
  • 创建商品订单 (订单模块)

product-service 提供两个接口


  1. /products 查询全部商品展示

  1. /order 调用 order 服务下单

代码实现

├── bulkhead-demo
   ├── order-service        #订单服务  (8070)
   └── product-service      #商品库存服务  (8050)
  • 依赖说明。由于 hystrix 年久失修,这里使用 resilience4j 断路保护器做演示
<dependency>
    <groupId>io.github.resilience4jgroupId>
    <artifactId>resilience4j-spring-boot2artifactId>
    <version>1.6.1version>
dependency>
  • 消费方定义隔离策略
server:
  tomcat:
    threads:
      max: 5 #限制应用只能处理5 个并发,方便测试

resilience4j.bulkhead:
  instances:
    createOrder:  # 限制此接口只能使用两个
      maxConcurrentCalls: 2
  • 消费方接口。product-service 8050
  /**
   * 用户点击购买
   */

  @SneakyThrows
  @GetMapping("/order")
  public String buy() {
      // 模拟调用 订单服务下单
      orderService.createOrder().get();
      return "success";
  }

  /***
   * 获取全部商品接口
   */

  @SneakyThrows
  @GetMapping("/products")
  public String products() {
      // 模拟查询DB 耗时
      Thread.sleep(100);
      return "products";
  }
  • 定义远程调用类使用 Bulkhead 包装
/**
 * 创建订单
 * 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 {
    @SneakyThrows
    @GetMapping("/pay")
    public String pay(){
        // 模拟调用支付渠道耗时 10s
        Thread.sleep(10000);
        return "支付成功";
    }
}

测试

  • Jmeter 10 个线程请求 /order 接口, 10 个线程请求 /products ,30S 请求服务性能如下

测试使用资源隔离

资源隔离下,30S 共计执行样本 1750 次,通过率 43.5/每秒

f8dc5efd1e3f410e7c20f704eab1c67f.webp1607306553

测试不使用资源隔离(去掉@Bulkhead 逻辑)

由于未做资源隔离,30S 共计执行样本 50 次,通过率 49.8/每分钟

2c9e5a959d95709b850f1bf29789af0b.webp

总结

在应用资源紧张的前提下,通过设置资源隔离策略能大大提升整个应用的性能。所谓资源紧张即请求并发大于容器的处理前最大值,如上边压测 tomcat 限制只能处理 5 个并发,但是我们测试是 20 个并发。

源码:https://github.com/lltx/microservices-pattern

参考资料和部分图片来源 https://www.vinsguru.com

往期推荐

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

「微服务设计之禅」重试模式

「2020封箱」Spring Boot 2.4.1 发布

Spring Boot 2.4.0 正式发布,全面拥抱云原生




浏览 38
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报