在第一篇中学习了搭建一个最基础的微服务架构,但在实际工作场景中服务调用关系是非常复杂的,一个http请求背后可能会形成一个微服务调用链,比如服务A调用服务B,服务B又调用服务C,服务D也调用服务B。假设一个场景:当服务B出现异常情况的时候,服务A调用服务B,经常出现访问超时,如果这个时候服务A继续调用服务B,可能会造成服务B进一步恶化,从而导致服务D调用服务B也出现异常。为了这些由于微服务调用关系链中某一节点故障导致整个系统出现问题,势必要引入保护机制-微服务容错保护,而在Spring Cloud提供了Hystrix这个组件去完成这件事,下面具体学习下Hystrix到底提供了哪里功能为微服务稳定性保驾护航的。
一、Spring Cloud Hystrix介绍
Hystrix这个组件有点像我们生活中电路保护装置,当发生短路时候,它能自动检查到并且跳闸,防止我们其它电器进一步损害。在微服务当中它主要提供了断路器来实现服务的降级和熔断、线程隔离和信号量等保护功能,另外还提供了像请求缓存、请求合并以及服务监控等这些辅助功能。关键是这些功能都提供了注解的方式,使用起来非常简单,我们一一学习下。
二、服务降级和熔断
断路器应该是Hystrix一个非常核心的功能,是实现服务降级和熔断基础。关于服务降级和熔断的概念这里有个比较通俗的讲解http://blog.didispace.com/fallback-and-circle-break/。按照我的理解,在Hystrix中出现服务调用异常,会触发fallback走到我们的服务降级逻辑里面去,而每次服务调用断路器根据被调用的服务健康情况决定是否要打开断路器,当断路器打开就意味着进行服务熔断的,默认情况下断路器打开后会经过一个休眠时间(该时间可通过circuitBreaker.sleepWindowInMilliseconds设置,默认5s),休眠时间后会再次关闭,当断路器关闭后服务又可以进行调用,如果还是调用异常,断路器会再次打开,通过这种健康检查判断+断路器设计防止故障蔓延。
下面通过代码学习下如何实现一个服务调用的降级和熔断逻辑,在上一篇随笔的基础上新增一个项目Hystrix。
HystrixController代码。
@RequestMapping("/get.do") public void get(@RequestParam("id") String id){ LOGGER.info("第一次请求time=" +hystrixServcie.get("11").getCreateTime().getTime()); LOGGER.info("第二次请求time="+ hystrixServcie.get("12").getCreateTime().getTime()); }
HystrixServcieImpl代码。HystrixServcieImpl里面的get方法会比HystrixController调用,我们只需要在get方法增加一个@HystrixCommand注解,然后在注解的fallbackMethod属性指明当发生异常时的回调方法就可以实现我们的降级逻辑了。当get方法发生异常,如调用SERVER-A服务超时或者不可用都会走到我们的降级逻辑里面去。
@Override @HystrixCommand(fallbackMethod = "getFallBack", commandKey = "hystrixKey") public User get(String id) { // 模拟发生异常 try { Thread.sleep(4000); } catch (InterruptedException e) { e.printStackTrace(); } return restTemplate.getForObject("http://SERVICE-A/hystrix/get.do?id=" +id, User.class); } private User getFallBack(String id, Throwable e){ LOGGER.info("降级处理,e=" +e); return null; }
二、线程隔离与信号量
Hystrix为每一个依赖服务创建了一个独立的线程池,通过这种方式依赖服务单独使用自己的线程池,当自身出现问题的时候也就不存在影响到其它依赖服务的可能性了。而且还带来了其它好处,例如我们可以通过判断某个依赖服务线程池使用情况去判断该服务的健康程度,并且可以实际情况动态调整这些线程池的配置。线程隔离是Hystrix默认的一种隔离策略,除此之外它还提供了信号量这种方式去控制调用依赖服务的并发,这2种方式的差异可以参考下http://www.coolxuewang.com/view/4介绍。我的理解是通常我们使用默认的线程隔离策略即可,除非有特殊要求才会考虑使用信号量这种方式。
三、请求缓存
Hystrix还提供了请求缓存这个功能,通过开启这个功能在同一http请求中,对同一服务,相同参数的调用的结果将会被缓存起来,这样一来多次用相同的参数调用同一服务,直接取缓存的结果,从而减少网络请求的消耗。这里有个注意的地方是,缓存的生命周期是在同一http请求中才有效的,当该http请求结束时,缓存也会被清空,这点务必理解。既然有缓存,那么当存在更新操作的时候,必然也有触发清空缓存操作,不过需要手动去做一些关联以便让Hystrix知道该更新操作是更新了哪个缓存的数据,Hystrix就会去删除该缓存的数据。
请求缓存这一块主要涉及到以下几个注解:
@CacheResult 开启缓存,必须与@HystrixCommand注解配合使用,其中可指定cacheKeyMethod属性定义缓存key生成规则。
@CacheRemove 该注解让缓存失效的。它有2个属性,第一个是cacheKeyMethod和@CacheResult一样作用。而commandKey属性就比较重要了,它是用来关联让哪一个缓存失效的。
@CacheKey 这个注解是作用在方法参数上的,用来标识方法哪几个参数用来做缓存key的,如果没有标注该注解默认就是使用所有参数啦。如果该方法同时还使用了@CacheResult或@CacheRemove的cacheKeyMethod属性的话,该注解就不起作用了,我感觉这个注解还是比较弱鸡的。。
Don't bb,Let you see my code。
要使用请求缓存的话首先我们要新增一个filter,不然会报错,从这个过滤器也就可以看出来为什么缓存的生命周期是在一个请求之内的了。
package com.pumpkin.filter; import com.netflix.hystrix.strategy.concurrency.HystrixRequestContext; import org.apache.log4j.Logger; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import java.io.IOException; @WebFilter(urlPatterns = "/*") public class RequestCacheFilter implements javax.servlet.Filter { private final Logger LOGGER = Logger.getLogger(getClass()); public void init(FilterConfig filterConfig) throws ServletException { LOGGER.info("初始化init filter..."); } public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { LOGGER.info("执行filter"); /** * 不初始化,会报如下错误 * Request caching is not available. Maybe you need to * initialize the HystrixRequestContext * 初始化是在filter中进行(官方建议),但是每一次初始化之前的缓存就失效了,所以要测缓存,就只能在controller中调用两次, * 才能看到缓存的结果是否相同 * 在同一用户请求的上下文中,相同依赖服务的返回数据始终保持一致 ---《spring cloud 微服务实战》有争论 */ HystrixRequestContext context = HystrixRequestContext.initializeContext(); try { chain.doFilter(request, response); } finally { context.shutdown(); } } public void destroy() { // TODO Auto-generated method stub } }
修改一下HystrixController
@RequestMapping("/get.do") public void get(@RequestParam("id") String id){ LOGGER.info("第一次请求time=" +hystrixServcie.get(id).getCreateTime().getTime()); LOGGER.info("第二次请求time="+ hystrixServcie.get(id).getCreateTime().getTime()); hystrixServcie.update(id);//测试删除缓存 LOGGER.info("第二次请求time="+ hystrixServcie.get(id).getCreateTime().getTime()); }
修改一下HystrixServiceImpl。认真看看注解里面一些属性的对应关系@CacheRemove(commandKey = "hystrixKey"),通过commandKey属性和@CacheResult开启缓存的方法关联起来了,这里我不演示cacheKeyMethod属性用法了,有兴趣的可以自己试下。
@Override @HystrixCommand(fallbackMethod = "getFallBack", commandKey = "hystrixKey") @CacheResult public User get(String id) { // 模拟发生异常 /* try { Thread.sleep(4000); } catch (InterruptedException e) { e.printStackTrace(); }*/ return restTemplate.getForObject("http://SERVICE-A/hystrix/get.do?id=" +id, User.class); } @Override @CacheRemove(commandKey = "hystrixKey") @HystrixCommand public void update(String id) { restTemplate.put("http://SERVICE-A/hystrix/update.do?id=" +id, null); } private User getFallBack(String id, Throwable e){ LOGGER.info("降级处理,e=" +e); return null; }
运行一下看看结果。可以看到第一次和第二次请求由于参数一样,返回的一样的结果。而第三次请求前,因为调用了hystrixServcie.update(id)进行更新操作,导致缓存被删除,而重新调用了一次SERVICE-A,获取新的结果。不过在我测试过程中发现,如果hystrixServcie.update(id)调用不是在同一个http请求中,缓存将不会删除,难道是有别的参数开启这种全局的缓存删除机制?还是设计如此?
四、请求合并
最后介绍下hystrix缓存合并这个功能。这个功能主要是应对一些高并发或者请求服务本身存在延迟的接口才建议使用,因为它的工作原理是在一个时间窗口内将多个单一的请求合并成批量请求,从而减少网络耗时。举个例子,如果存在一个根据id获取用户信息的http接口,该接口主要逻辑是通过调用SERVICE-A获取单一用户信息服务,那么我们是不是可以在SERVICE-A提供一个批量获取用户信息的服务,假如时间窗口为100ms,在100ms内把所有调用该http接口的用户id收集起来,然后调用一次SERVICE-A的批量获取用户信息服务就好了,而不用调用多次SERVICE-A的单一用户信息服务。弊端显然易见啊,就是每次请求该http接口都要等个100ms的时间窗口,因此适用场景开头就说了“这个功能主要是应对一些高并发或者请求服务本身存在延迟的接口才建议使用”。
HystrixController新增一个接口用来测试下请求合并功能
@RequestMapping("/get1.do") public String get1(){ try { Future<User> f1 = hystrixServcie.asyncGet(1+""); Future<User> f2 = hystrixServcie.asyncGet(2+""); Thread.sleep(300); Future<User> f3 = hystrixServcie.asyncGet(3+""); User user1 = f1.get(); User user2 = f2.get(); User user3 = f3.get(); LOGGER.info("user1=" +user1.toString()); LOGGER.info("user2=" +user2.toString()); LOGGER.info("user3=" +user3.toString()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } return "hello"; }
HystrixServiceImpl新增方法演示请求合并。首先我们新增了一个asyncGet方法,使用@HystrixCollapser即可轻松使用请求合并功能,其中batchMethod属性用于指定处理批量请求的方法,collapserProperties属性
用于设置有一个请求合并的属性,比如设置timerDelayInMilliseconds时间窗口为100ms。这里有个注意的是asyncGet方法的返回值需要用Future 包装,否则不会进行合并,之前测试一直不通过,就是被这个坑到了。
@HystrixCollapser(batchMethod = "batch", collapserProperties = {@HystrixProperty(name="timerDelayInMilliseconds", value = "100")}) public Future<User> asyncGet(String id) { return null; } @Override @HystrixCommand public List<User> batch(List<String> ids) { if (ids == null){ LOGGER.info("ids is null"); return null; } LOGGER.info("当前线程:" +Thread.currentThread().getName()); int idCount = ids.size(); LOGGER.info("ids size=" +idCount); for (int i = 0; i < idCount; i++){ LOGGER.info("ids[" +i+ "]=" +ids.get(i)); } User[] user1 = restTemplate.getForObject("http://SERVICE-A/hystrix/batch.do?ids={1}", User[].class, StringUtils.join(ids, ",")); LOGGER.info("res=" +user1.toString()); return Arrays.asList(user1); }
测试一下看看结果。可以看到我调用了3次该接口,其中第一次和第二次调用进行了请求合并,随后我Thread.sleep(300),由于我设置的时间窗口为100ms,因此第三次调用并没有和前两次调用合并起来。另外从结果中我们还能看出2次的合并请求处理的线程不是同一线程,侧面说明了hystrix默认的隔离测试时线程隔离而非信号量。
五、总结
时隔了好像2周才终于把这篇随笔逼出来,有点坑。主要是自己在写测试代码的时候遇到了一下坑,刚开始想直接上Feign+Hystrix来写这一篇随笔,谁知道Feign居然没有支持请求缓存和合并这些功能,只好分开来写。Hystrix这章的知识点也比较多,后面还会有两篇来介绍下Dashboard和Feign配合Hystrix一起用。总结一下这篇随笔,介绍了Hystrix的在微服务的作用是什么,以及它提供了哪些功能去对服务的容错进行保护,其中我认为服务降级和熔断是Hystrix的核心功能;线程隔离使得它锦上添花,对依赖服务调用调用使用隔离的线程池,保障了系统的稳定性;请求缓存和请求合并合理使用能提高我们系统的性能,减少不必要的网络IO。
六、参考资料
Spring Cloud微服务实战-翟永超。本系列的学习都是参考该书籍学习的,同时源码使用的Spring Boot和Spring Cloud的版本也与该书保持一致。
Hystrix两种隔离模式分析 http://www.coolxuewang.com/view/4
白话:服务降级与熔断的区别 http://blog.didispace.com/fallback-and-circle-break/
Hystrix:HystrixCollapser请求合并 https://juejin.im/post/5a22a88851882554bd50deae
七、源码
码云地址:[email protected]:pumpkingg/Spring-Cloud-Study.git 该篇随笔对应的代码是master分支下命名为blog2的Tag