通常微服务架构中的依赖都是通过远程调用来实现的,而远程调用中最常见的问题就是通信消耗和连接数占用。在高并发的情况下,由于通信次数的增加,总的通信耗时会变得不那么理想。同时,由于依赖服务的线程池资源有限,会出现队列等待和响应延迟。为了优化这两个问题,Hystrix提供了HystrixCollapser来实现请求的合并,减少通信消耗和线程数。HystrixCollapser实现了在HystrixCommand之前放置一个合并处理器的功能,它会在很短的时间窗口(默认10毫秒)内整合同一个依赖服务的多个请求,并批量发起请求(服务提供者还需要提供相应的批量实现接口).通过HystrixCollapser的封装,开发者无需关注线程合并的具体过程,只需要关注批量服务和处理即可。我们从HystrixCollapser的使用例子来看合并请求的过程。Hystrix的请求合并示例publicabstractclassHystrixCollapserimplementsHystrixExecutable,HystrixObservable{...publicabstractRequestArgumentTypegetRequestArgument();protectedabstractHystrixCommandcreateCommand(Collection>requests);protectedabstractvoidmapResponseToRequests(BatchReturnTypebatchResponse,Collection>requests);...}从HystrixCollapser抽象类的定义可以看出,它指定了三种不同的类型:BatchReturnType:合并批请求的返回类型ResponseType:Single请求返回的类型RequestArgumentType:请求参数类型,这三种类型的使用可以看它的三个抽象方法:RequestArgumentTypegetRequestArgument():该函数用于定义获取请求参数的方法。HystrixCommandcreateCommand(Collection>requests):合并请求生成批处理命令的具体实现方法。mapResponseToRequests(BatchReturnTypebatchResponse,Collection>requests):批处理命令结果返回后的处理。这里,需要实现将批处理结果拆分并传递给各个原子请求命令再进行合并的逻辑。下面我们通过一个简单的例子来直观的了解实现请求合并的过程。假设当前微服务USER-SERVICE提供了两个获取User的接口:/users/{id}:根据id返回User对象的GET请求接口。/users?ids={ids}:GET请求接口,根据ids参数返回User对象列表,其中ids是用逗号分隔的id集合。在服务消费者端,通过RestTemplate实现了对这两个远程接口的简单调用,如下:1}",User.class,id);}@OverridepublicListfindAll(Listids){returnrestTemplate.getForObject("http://USER-SERVICE/users?ids={1}",列表。class,StringUtils.join(ids,","));}}下面我们来实现短时间内合并多个获取单个User对象的请求命令的实现:Step1:Mergefortherequest实现实现准备一个批量请求命令,如下:publicclassUserBatchCommandextendsHystrixCommand>{UserServiceuserService;ListuserIds;publicUserBatchCommand(UserServiceuserService,ListuserIds){super(Setter.withGroupKey(asKey("userServiceCommand")));this.userIds=userIds;this.userService=userService;}@OverrideprotectedListrun()throwsException{returnuserService.findAll(userIds);}}批量请求命令其实就是一个简单的HystrixCommand实现,从上面的实现可以看出,它通过调用userService.findAll方法访问/users?ids={ids}接口,返回User的列表结果第二步,通过继承HystrixCollapser实现请求合并器:publicclassUserCollapseCommandextendsHystrixCollapser,User,Long>{privateUserServiceuserService;privateLonguserId;publicUserCollapseCommand(UserServiceuserService,LonguserId){super(Setter.withCollapserKey(HystrixCollapserKey.Factory.asKey("userCollapseCommand")).andCollapserPropertiesDefaults(HystrixCollapserProperties.Setter().withTimerDelayInMilliseconds(100)));this.userService=userService;this.userId=userId;}@OverridepublicLonggetRequestArgument(){returnuserId;}@OverrideprotectedHystrixCommand>createCommand(Collection>collapsedRequests){ListuserIds=newArrayList<>(collapsedRequests.size());userIds.addAll(collapsedRequests.stream().map(CollapsedRequest::getArgument).collect(Collectors.toList()));returnnewUserBatchCommand(userService,userIds);}@OverrideprotectedvoidmapResponseToRequests(List>collapsedRequests){intcount=0;for(CollapsedRequestcollapsedRequest:collapsedRequests){Useruser=batchResponse.get(count++);collapsedRequest.setResponse(user);}在上面的构造函数中,我们为请求组合器设置了时间延迟属性。合并器将收集此时间窗口内单个用户的请求,并在时间窗口结束时将它们合并以形成单个批处理请求。下面的getRequestArgument方法返回给定的单个请求参数userId,createCommand和mapResponseToRequests是请求组合器的两个核心:createCommand:该方法的collapsedRequests参数存储了延迟时间窗口内收集的所有获取单个User的请求。通过获取这些请求的参数来组织我们上面准备的批量请求命令UserBatchCommand实例。mapResponseToRequests:批量命令UserBatchCommand实例被触发执行后,该方法开始执行,其中batchResponse参数保存了createCommand中组织的批量请求命令的返回结果,collapsedRequests参数代表每次合并的请求。这里我们遍历批量结果batchResponse对象,在collapsedRequests中设置合并前每个单次请求的返回结果,从而完成批量结果到单次请求结果的转换。请求合并原理分析下图是未使用HystrixCollapser请求合并器之前的线程使用情况。可以看出,当一个服务消费者同时向USER-SERVICE的/users/{id}接口发起5个请求时,会从依赖服务的独立线程池中申请5个线程来完成各自的请求操作。使用HystrixCollapserrequestcombiner后,同样情况下的线程占用情况如下图所示。由于同时发生的5个请求都在requestcombiner的一个时间窗口内,所以这些对/users/{id}接口的请求被requestcombiner拦截并在combiner中进行合并,然后将这些请求合并发送一个向USER-SERVICE的批接口/users?ids={ids}请求。得到批量请求结果后,通过requestcombiner将批量结果拆分分配给各个合并后的请求。从图中我们可以看出,requestcombiner的使用有效的降低了线程池中的资源占用。因此,当资源可用,短时间内会产生高并发请求时,为避免连接不足造成的延迟,可以考虑使用requestcombiner进行处理优化。使用注解实现requestconsolidator在快速入门的例子中,我们使用@HystrixCommand注解优雅的实现了HystrixCommand的定义,那么requestconsolidator是不是也可以通过注解来定义呢?答案是肯定的!以上面实现的requestcombiner为例,也可以通过如下方式实现:@ServicepublicclassUserService{@AutowiredprivateRestTemplaterestTemplate;@HystrixCollapser(batchMethod="findAll",collapserProperties={@HystrixProperty(name="timerDelayInMilliseconds",value="100")})publicUserfind(Longid){returnnull;}@HystrixCommandpublicListfindAll(Listids){returnrestTemplate.getForObject("http://USER-SERVICE/users?ids={1}",List.class,StringUtils.join(ids,","));}}@HystrixCommand我们之前介绍过,可以看到这里定义了两个Hystrix命令,一个是用来请求/users/{id}接口,另一个用于请求/users?ids={ids}接口。在请求/users/{id}接口的方法上,通过@HystrixCollapser注解为其创建一个mergerequester,通过batchMethod属性指定批量请求的实现方法为findAll方法(即:request/users?ids={ids}接口命令),通过collazerProperties属性为合并请求者设置相关属性。这里,@HystrixProperty(name="timerDelayInMilliseconds",value="100")用于设置合并时间窗口为100毫秒。这样就通过@HystrixCollapser注解简单优雅地在/users/{id}依赖服务之前设置了一个批量请求combiner。请求合并的额外开销虽然请求合并可以减少请求的数量来缓解对服务线程池的资源依赖,但是在使用时也需要注意它带来的额外开销:请求的延迟时间窗口合并将使依赖关系增加服务的请求延迟。例如:访问一个没有requestcombiner的request的平均耗时是5ms,requestmerging的延迟时间窗口是10ms(默认值),那么当请求设置了requestcombiner时,在最坏情况下(in请求在延迟时间窗口结束时发起)请求需要15ms才能完成。由于requestcombiner的延迟时间窗口会带来额外的开销,所以我们是否使用requestcombiner需要根据依赖服务调用的实际情况来选择,主要考虑以下两个方面:请求命令本身的延迟。如果依赖服务的请求命令本身是高延迟命令,那么可以使用请求组合器,因为延迟时间窗口的时间消耗是微不足道的。延迟窗口内的并发量。如果一个时间窗口内只有1-2个请求,那么这样的依赖服务不适合使用requestcombiner。在这种情况下,不仅不能提高系统性能,反而会成为系统瓶颈,因为每个请求都需要一个额外的时间窗口来响应。相反,如果一个时间窗口内并发量很高,而且服务提供者也实现了批处理接口,那么使用requestcombiner可以有效减少网络连接数,大大提高系统吞吐量。此时,延迟时间窗口增加的消耗可以忽略不计。【本文为专栏作家“翟永超”原创稿件,转载请联系作者获得授权】点此查看该作者更多好文