「微服务设计之禅」隔离模式
前言
微服务本质上分布式架构,当我们使用分布式系统时任何不可预知的问题都会发生(例如网络可用性问题、服务可用性问题、中间件可用性问题)。一个系统的问题可能会直接影响另外一个系统的使用或性能。所以在系统设计过程既要保证自身运行的弹性需求,也要避免对下游服务级联故障。
隔离模式
如上图,如果我们仔细观察船的结构时会发现,使用了多个隔板将船分为了多个小的隔间。隔板用于密封船体的各个部分,避免由于某个部位泄漏导致整个船淹没。当我们进行软件设计时,应该将整个应用拆分为多个组件,并且一个组件的故障不会影响到全局,这就所谓的隔离模式。
例如:假设有两个服务 A 和 B,服务 A 只能同时处理 5 个并发请求。服务 A 里面有两个接口
/a/b
依赖于服务 B (耗时比较多)/a
本地服务
当有 10 个并发请求 A 服务接口, 其中 5 个请求 /a/b
接口 ,5 个请求 /a
由于/a/b
需要调用服务 B 耗时比较多,可能会造成 5 个线程阻塞应用无法处理 /a
虽然只是本地处理。由于一个接口耗时占用了整个应用的资源导致其他接口无法使用,导致用户体验很差。
通过隔离模式分配特定接口的资源限制,避免资源瓶颈。如上图,将a/b
接口最大处理线程设置为 2,这样应用始终会有剩余线程处理其他接口请求。
示例程序
架构图
如上图所示,简单模拟电商下单逻辑
- 用户登录浏览商品 (商品库存模块)
- 扣减商品库存 (商品库存模块)
- 创建商品订单 (订单模块)
product-service 提供两个接口
- /products 查询全部商品展示
- /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/每秒
1607306553测试不使用资源隔离(去掉@Bulkhead 逻辑)
由于未做资源隔离,30S 共计执行样本 50 次,通过率 49.8/每分钟
总结
在应用资源紧张的前提下,通过设置资源隔离策略能大大提升整个应用的性能。所谓资源紧张即请求并发大于容器的处理前最大值,如上边压测 tomcat 限制只能处理 5 个并发,但是我们测试是 20 个并发。
源码:https://github.com/lltx/microservices-pattern
参考资料和部分图片来源 https://www.vinsguru.com
往期推荐
Spring Boot 2.4.0 正式发布,全面拥抱云原生