SpringCloud - 服务容错保护组件Hystrix的使用详解7(请求合并)
作者:hangge | 2020-07-20 08:10
七、请求合并
1,什么是请求合并
(1)通常微服务架构中的依赖是通过远程调用实现,而远程调用中最常见的问题就是通信消耗与连接数占用。在高并发的情况下,因为通信次数的增加,总的通信时间消耗会变得不那个理想。同时,因为对依赖服务线程池资源有限,将出现排队等待与响应延迟的情况。
(2)为了优化这两个问题,Hystrix 提供了 HystrixCollapser 来实现请求的合并,以减少通信消耗和线程数的占用(默认合并 10ms 之内的请求,可自行设置)
2,请求合并的额外开销
(1)虽然请求合并可以减少请求数量以缓解依赖服务线程池的资源,但是在使用的过程中也需要注意他所带来的额外的性能开销:用于请求合并的延迟时间窗会使得依赖服务的请求延迟增高。
例如:某个请求在不通过请求合并器访问的平均耗时为 5ms,请求合并的延迟时间窗为 10ms(默认值),那么当该请求的设置了请求合并器之后,最坏情况下(在延迟时间窗结束时才发起请求)该请求需要 15ms 才能完成。
- 请求命令本身的延迟:如果依赖服务的请求命令本身是一个高延迟的命令,那么可以使用请求合并器,因为延迟时间窗的时间消耗就显得莫不足道了。
- 延迟时间窗内的并发量:延迟时间窗内的并发量。如果一个时间窗内只有 1 - 2 个请求,那么这样的依赖服务不适合使用请求合并器,这种情况下不但不能提升系统性能,反而会成为系统瓶颈,因为每个请求都需要多消耗一个时间窗才响应。相反,如果一个时间窗内具有很高的并发量,并且服务提供方也实现了批量处理接口,那么使用请求合并器可以有效的减少网络连接数量并极大地提升系统吞吐量,此时延迟时间窗所增加的消耗就可以忽略不计了。
3,请求合并的样例
(1)服务提供者 HELLO-SERVICE 准备一个获取 User 的接口:/users?ids={ids}(根据 ids 返回 User 对象列表的 Get 请求接口,其中 ids 为以逗号分隔的 id 集合)
@RestController public class HelloController { @GetMapping("/users") public List<User> users(@RequestParam("ids") List<Long> ids) { System.out.println("ids:" + ids.toString()); List<User> result = new ArrayList<>(); for (Long id: ids) { result.add(new User(id, "用户" + id)); } return result; } }
(2)而消费者这边我们可以使用注解的方式进行请求合并:
在获取单个对象的方法上通过 @HystrixCollapser 注解为其创建了合并请求器:
- 通过 batchMethod 属性指定了批量请求的实现方法为 findAll 方法(即请求 /users?ids={ids} 接口命令)
- 同时通过 collapserProperties 属性为合并请求器设置相关属性(这里将合并时间窗设置为 100 毫秒)
@Service public class UserService { @Autowired RestTemplate restTemplate; //指定批处理的方法,设置合并100ms之内的请求 //注意:单个请求的service返回的必须是Future对象 @HystrixCollapser(batchMethod = "findAll", collapserProperties = { @HystrixProperty(name = "timerDelayInMilliseconds", value = "100") }) public Future<User> find(Long id) { //不会进入这个方法体 return null; } @HystrixCommand public List<User> findAll(List<Long> ids) { System.out.println("合并的请求:"+ids.toString()); List<User> result = restTemplate.getForObject("http://HELLO-SERVICE/users?ids={1}", List.class, StringUtils.join(ids, ",")); System.out.println("合并后的结果:" + result); return result; } }
(3)下面创建一个 Controller 来调用这个 Service 试试:
@RestController public class ConsumerController { @Autowired UserService userService; @GetMapping("/hello-consumer") public void helloConsumer() throws ExecutionException, InterruptedException{ //开启上下文TheardLocal(重要) HystrixRequestContext context = HystrixRequestContext.initializeContext(); Future<User> future1 = userService.find(1L); Future<User> future2 = userService.find(2L); Future<User> future3 = userService.find(3L); System.out.println(future1.get()); System.out.println(future2.get()); System.out.println(future3.get()); context.close(); } }
(4)启动程序之后访问该接口,控制台输出如下,说明请求确实合并了:
![](http://hangge.com/blog_uploads/202001/2020010218112960319.png)
(5)而服务提供者控制台输出如下,说明只接收到一次请求:
![](http://hangge.com/blog_uploads/202001/2020010218121540956.png)
附:实现多次请求的多次服务调用合并合并成一次
(1)默认情况下,只会对一次请求的多次服务调用进行合并。假设我们将合并时间窗延长(比如 3 秒),当我们连续两次调用接口时(刷新两次页面),可以看到两次请求还是被合并成了两次服务调用:
![](http://hangge.com/blog_uploads/202001/2020010219004196547.png)
(2)如果想要实现两次请求,合并成一次服务调用,可以将 @HystrixCollapser 注解的 scope 属性设置为 GLOBAL 即可:
scope 属性可以设置合并作用域,它有如下可选值:
- Scope.REQUEST:只对一个 request 请求内的多次服务请求进行合并(默认值)
- Scope.GLOBAL:所有线程的请求中的多次服务请求进行合并。
@Service public class UserService { @Autowired RestTemplate restTemplate; //指定批处理的方法,设置合并3s之内的请求 //注意:单个请求的service返回的必须是Future对象 @HystrixCollapser(batchMethod = "findAll", scope = com.netflix.hystrix.HystrixCollapser.Scope.GLOBAL, collapserProperties = { @HystrixProperty(name = "timerDelayInMilliseconds", value = "3000") }) public Future<User> find(Long id) { //不会进入这个方法体 return null; } @HystrixCommand public List<User> findAll(List<Long> ids) { System.out.println("合并的请求:"+ids.toString()); List<User> result = restTemplate.getForObject("http://HELLO-SERVICE/users?ids={1}", List.class, StringUtils.join(ids, ",")); System.out.println("合并后的结果:" + result); return result; } }
(3)重启项目,当我们再次连续两次调用接口时(刷新两次页面),可以看到两次请求被合并成了一次服务调用:
![](http://hangge.com/blog_uploads/202001/202001021909477767.png)
(4)而服务提供者控制台输出如下,说明只接收到一次请求:
![](http://hangge.com/blog_uploads/202001/2020010219103086003.png)
全部评论(0)