服务容错保护:Spring Cloud Hystrix
在微服务的架构中,存在着很多的服务单元,若一个单元出现故障,就很容易因依赖关系而引发故障蔓延,最终导致整个系统瘫痪,这样的架构相较传统的架构更加不稳定,为了解决这样的问题,产生了断路器等一系列的保护机制
针对上述问题,Spring Cloud Hystrix实现了断路器、线程隔离等一系列服务保护功能,该框架的目标在于通过控制那些远程系统、服务和第三方库的节点,从而对延迟和故障提供更强大的容错能力,Hystrix具备服务降级,服务熔断、线程和信号隔离,请求缓存、请求合并以及服务监控等强大功能
快速入门
1.在Ribbon客户端的工程的pom.xml中添加spring-cloud-starter-hystrix依赖,并在主类中使用@EnableCircuitBreaker注解开启断路器功能
@SpringBootApplication
@EnableDiscoveryClient //该应用为Eureka客户端应用,获取服务发现的能力
@EnableCircuitBreaker //开启断路器功能
public class RibbonDemoApplication {
public static void main(String[] args) {
SpringApplication.run(RibbonDemoApplication.class, args);
}
@Bean
@LoadBalanced //开启客户端得负载均衡
RestTemplate restTemplate(){
return new RestTemplate();
}
}
注:可以使用@SpringCloudApplication注解来修饰应用主类,它包含了上述3个我们引用的注解
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
public @interface SpringCloudApplication {
}
- 改造服务消费方式,在service中调用服务,在调用方法中使用@HystrixCommand注解标明触发断路器后的回调函数
@Service
public class HystrixServiceImpl {
@Autowired
RestTemplate restTemplate;
@RequestMapping(value = "/ribbon",method = RequestMethod.GET)
@HystrixCommand(fallbackMethod = "back")
public String helloRibbon(){
ResponseEntity<String> forEntity = restTemplate.getForEntity("http://self-core/index", String.class);
return restTemplate.getForEntity("http://self-core/index",String.class).getBody();
}
/**
* 异常回调函数
* @return
*/
public String callback(){
return "error";
}
}
- 在服务的/index接口中,我们使用超时来出发断路器,由于Hystrix默认的超时超时时间是2000毫秒,在0-3000毫秒的超时时间内有一定几率出发断路器
@RequestMapping("/index")
public String index() throws Exception{
logger.info("eureka server Host= "+discoveryClient.getLocalServiceInstance().getHost()+
"serviceId ="+discoveryClient.getLocalServiceInstance().getServiceId());
int time = new Random().nextInt(3000);
logger.info("sleepTime"+time);
Thread.sleep(time);
return "hello,self-core";
}
- 当sleepTime > 2000的时候出发断路器,此时,执行回调的函数,返回error
Hystrix的工作原理
当一个请求调用了相关服务依赖之后,Hystrix是如何工作的?
工作流程图如下:
- 创建HystrixCommand或HystrixObservableCommand对象,
这2个对象用来表示依赖服务的操作请求,同时传递所有需要的参数
- HystrixCommand: 用在依赖的服务返回单个操作结果的时候。
- HystrixObservableCommand: 用在依赖的服务返回多个操作结果的时候。
2.命令执行
从上图中我们可以看到一共存在4种命令的执行方式,而Hystrix在执行时会根据创建的Command对象以及具体的情况来选择一个来执行,其中
HystrixCommand实现下面2种执行方式:
- execute (): 同步执行,从依赖的服务 返回一个单一的结果对象,或是在发生错误的时候抛出异常。R value = command.execute( );
- queue (): 异步执行,直接返回一个Future对象,其中包含了服务 执行结束时要返回的单一结果对象。 Future<R> value = command.queue( );
而HystriObservable实现了下面2种执行方式:
Observe():返回Observable对象,代表了操作的多个结果,它是一个Hot Observable, Observable<R> ohValue = command.observe();
toObservable():返回Observable对象,代表了操作的多个结果,但是它返回一个cold Observable, Observable<R> ocValue = command.toObservable();
- 结果是否被缓存
若当前命令的请求缓存功能是被启用的,并且该命令缓存命中,那么缓存的结果会立即以Observable对象的形式返回。
4.断路器是否打开
若缓存没有命中或没有启用,则Hystrix在执行命令前需要检查断路器是否为打开状态:
如果断路器是打开的,那么Hystrix不会执行命令,而是转接到fallback的处理逻辑(步骤8)
如果断路器是关闭的,那么Hystrix将执行下面的步骤,检查是否有可用的资源来执行命令
5. 线程池/请求队列/信号量是否占满
如果与命令相关的线程池和请求队列或者信号量已经被占满,Hystrix也不会执行命令,而是转到fallback处理逻辑(步骤8)
在这里Hystrix所判断的线程池并非容器的线程池,而是每个依赖服务的专有线程池,保证了Hystrix不会因为某个依赖服务的问题而影响到其他依赖服务
- HystrixObservableCommand.construct()或HystrixCommand.run()
- HystrixCommand.run(): 返回一个单一 的结果,或者抛出异常。
- HystrixObservableCommand.construct(): 返回一个Observable对象来发射多个结果,或通过onError发送错误通知。
如果这2个方法的执行时间超过命令设置的超时阀值,当前处理线程将会抛出一个TimeOutException(则会通过单独的计时线程来抛出),这时,Hystrix也会转接到fallback处理逻辑,当前命令的返回值也将被忽略
如果命令没有抛出异常并返回了结果,那么Hystrix在记录一些日志后将改结果返回,在使用run的情况下,Hystrix会返回一个Observable,它发射单个结果并产生onCompleted的结束通知,而在使用construct的情况下,Hystrix会直接返回该方法产生的Observable对象
7. 计算断路器的健康度
Hystrix会将“ 成功 ”、“ 失败”、“ 拒绝”、“ 超时 ”等信息报告给断路器,而断路器会维护一组计数器来统计这些数据。
断路器会使用这些统计数据来决定是否要将断路器打开,来对某个依赖服务的请求进行 “熔断/短路 ”,直到恢复期结束。 若在恢复期结束后, 根据统计数据判断如果还是未达到健康指标,就再次 “ 熔断/短路 ”
8. Fallback的处理
当命令执行失败的时候,Hystrix就会进入fallback尝试回退处理,我们通常称之为“服务降级”,从上面我们大概知道有那种情况会引起服务降级
第4步:当前命令处于“熔断/短路”状态,断路器打开的时候
第5步:当前命令的线程池,请求队列或者信号量被占满的时候
第6步:HystrixObservableCommand.construct()或HysttixCommand.run()抛出异常的时候
需要注意的是:服务降级的逻辑需要实现一个通用的响应结果,该结果的处理逻辑不应当依赖网络请求获取,如果一定要降级的逻辑中包含网络请求,那么该请求也必须包装在HystrixCommand或是HystrixObservableCommand中,从而形成级联的降级策略,最终的降级逻辑一定不是一个依赖网络的请求。
断路器的原理
断路器的使用详解
创建请求命令
Hystrix命令就是我们之前所说的HystrixCommand,用来封装具体的依赖服务调用逻辑,主要有2中方式,一种是通过代码的方式,还有一种是通过注解的方式,前面提到过。
代码方式:自定义类继承HystrixCommand
public class UserCommand extends HystrixCommand<User> {
private RestTemplate template;
private Long id;
public UserCommand(Setter setter, RestTemplate template, Long id) {
super(setter);
this.template = template;
this.id = id;
}
@Override
protected User run() throws Exception {
return template.getForObject("http://self-core/user/{1}",User.class,id);
}
/**
* 实现服务降级的时候重写getFallback()方法
* @return
*/
@Override
protected User getFallback() {
return new User();
}
}
通过上面实现的UserCommand,我们既可以实现请求的同步执行也可以实现异步执行
传统的同步异步执行:
同步执行:User user = new UserCommand(new RestTemplate(), 1L).execute(); 异步执行:Future<User> queue = new UserCommand(new RestTemplate(), 1L).queue();通过调用queue.get()获取结果
响应式的执行方式:
Observable<User> observe = new UserCommand(new RestTemplate(), 1L).observe();
Observable<User> observable = new UserCommand(new RestTemplate(), 1L).toObservable();
Observe()方法和toObservable()虽然都是返回的是Observable,但是他们略有不同,前者返回一个Hot Observable,该命令会在调用observe()的时候立即执行,当Observable每次被订阅的时候会重放它的行为, 而后者返回的是一个cold Observable,调用toObservable()方法之后不会立即执行,只有当所有订阅者都订阅它之后才会执行
使用HystrixCommand具备了observe()和toObservable()的功能,但是它的实现有一定局限性,返回的Observable只能发射一次数据,所以可以通过HystrixObservable实现的命令获取能发送多次的Observable
public class UserObservableCommand extends HystrixObservableCommand<User> {
private RestTemplate template;
private Long id;
public UserObservableCommand(Setter setter, RestTemplate template, Long id) {
super(setter);
this.template = template;
this.id = id;
}
@Override
protected Observable<User> construct() {
return Observable.create(new Observable.OnSubscribe<String>() {
@Override
public void call(Subscriber<? super String> subscriber) {
try{
if(subscriber.isUnsubscribed()){
User user = template.getForObject("http://self-core/user/{1}", User.class, id);
subscriber.onNext(user);
subscriber.onCompleted();
}
}catch (Exception e){
subscriber.onError(e);
}
}
};
}
/**
* 实现服务降级
* @return
*/
@Override
protected Observable<User> resumeWithFallback() {
return super.resumeWithFallback();
}
}
注解方式:我们前面快速入门中使用的@HystrixCommand可以优雅的定义Hystrix命令的实现,修饰的方法默认是同步执行的,如果想要异步执行的话,需要自己另外定义,如下:
@RequestMapping(value = "/hello",method = RequestMethod.GET)
@HystrixCommand(defaultFallback = "callback")
public Future<String> helloService(){
return new AsyncResult<String>() {
@Override
public String invoke() {
ResponseEntity<String> forEntity = restTemplate.getForEntity("http://self-core/hello", String.class);
return restTemplate.getForEntity("http://self-core/index",String.class).getBody();
}
};
}
如果想要通过注解@HystrixCommand实现observe()和toObservable()的功能,需要按照以下的方式实现
@HystrixCommand
public Observable<User> getUserById(final String id) {
return Observable.create(new Observable.OnSubscribe<String>() {
@Override
public void call(Subscriber<? super String> subscriber) {
try{
if(subscriber.isUnsubscribed()){
User user = template.getForObject("http://self-core/user/{1}", User.class, id);
subscriber.onNext(user);
subscriber.onCompleted();
}
}catch (Exception e){
subscriber.onError(e);
}
}
}
}
在使用@HystrixCommand实现响应式命令时,可以通过ObservableExecutionMode参数来控制使用的是observe()还是toObservable()的执行方式
@HystrixCommand(observableExecutionMode = ObservableExecutionMode.EAGER):EAGER表示observe()执行方式
@HystrixCommand(observableExecutionMode = ObservableExecutionMode.LAZY):LAZY表示toObservable()的模式值
定义服务降级
Fallback是Hystrix命令执行失败时使用的后备方法,用来实现服务的降级处理逻辑,在HystrixCommand中可以通过重载getFallback()方法实现服务降级,Hystrix会在run()执行过程中出现错误超时,线程池拒绝,断路器熔断等情况;在HystrixObservableCommand实现Hystrix命令时,我们可以通过重载resumeWithFallback方法来实现服务降级逻辑,该方法会返回一个observable对象,命令执行失败的时候,Hystrix会将Observable的结果通知给所有订阅者
通过@HystrixCommand注解的fallbackMethod参数来指定具体的服务降级实现方法
@HystrixCommand(fallbackMethod = "callback")
/**
* 异常回调函数
* @return
*/
public String callback(){
return "error";
}
同样,万一降级方法也有可能出现异常超时等其他原因造成降级方法无法稳定逻辑,可以在降级方法上添加@HystrixCommand注解在指定fallbackMethod实现级联降级
在实际使用中,也有一些情况不去实现降级逻辑:如执行写操作的命令;执行批处理或离线计算的命令
不论Hystrix命令是否实现了服务降级,命令状态和断路器的状态都会更新,并且我们可以由此了解到命令执行的失败情况
异常处理
异常传播
在HystrixCommand实现的run方法中抛出异常,除了HystrixBadRequestException之外,其他异常均会被Hystrix认为命令执行失败触发服务降级逻辑,所以当需要在命令执行中抛出不处罚服务降级的异常时来使用它
在注册配置实现Hystrix命令时,支持忽略指定异常类型,通过设置@HystrixCommand注解的ignoreExceptions参数,这样当抛出配置的异常时候不会引发服务降级
@HystrixCommand(fallbackMethod = "callback",ignoreExceptions = {NotFoundException.class,IndexOutOfBoundsException.class})
异常获取
当Hystrix因为异常(除了HystrixBadRequestException)进入服务降级逻辑之后,需要对不同异常进行处理,传统继承中我们可以通过在getFallback()方法里面调用getExecutionException方法获取异常判断处理
@Override
protected User getFallback() {
Throwable exec = getExecutionException();
return new User();
}
注解方式更加简单,在降级方法的参数中增加Thorwable e对象的定义,这样就能在方法内部获取出发降级的异常了
public String callback(Throwable e){
return "error";
}
命令名称、分组以及线程池的划分
以继承的方式实现的Hystrix命令使用类名作为默认的命令名称,可以在构造函数中通过Setter静态类来设置
public UserCommand() {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("GroupName")).
andCommandKey(HystrixCommandKey.Factory.asKey("CommandKey")));
}
上面的Setter没有直接设置命令名称而是先设置了命令组名,然后在调用andCommandKey来设置命令名,是因为在Setter的定义中,只有withGroupKey静态方法来能创建Setter实例,所以组名设置是每个Setter的必需参数,而命令名只是一个可选参数
设置了,命令组,Hystrix会根据组织来统计命令的告警,仪表盘等信息,此外Hystrix命令默认的线程划分也是根据命令组来实现的,默认情况下相同组名的命令使用同一个线程池,所以我们需要在创建Hystrix命令时指定命令组名实现默认线程池划分
线程池的划分当然不仅仅可以依靠命令组来完成,Hystrix还提供了HystrixThreadPoolKey来对线程池进行更细粒度的划分
public UserCommand() {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("GroupName")).
andCommandKey(HystrixCommandKey.Factory.asKey("CommandKey")).
andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("ThreadPoolKey")));
}
综上,没有指定threadPoolKey的时候使用命令组的方式划分线程池,制定了则按照HystrixThreadPoolKey
注解方式的实现
@HystrixCommand(groupKey = "group",commandKey = "command",threadPoolKey = "threadPool")
请求缓存
在高并发场景下,Hystrix提供了请求缓存的功能,我们可以方便的开启和使用请求缓存来优化系统
开启请求缓存功能
在继承方式实现的HystrixCommand或HystrixObservableCommand时,通过重载的getCacheKey()方法开启请求缓存
@Override
protected String getCacheKey() {
return String.valueOf(id);
}
我们通过在getCacheKey方法中返回请求缓存的key值(使用了传入的获取User对象的id值),就能让该请求命令具备缓存功能,当不同的外部请求调用同一个依赖服务时,Hystrix会根据getCacheKey方法返回的值来区分是否是重复的请求,如果cacheKey相同,那么该服务只会在第一个请求到达时被调用一次,另外的请求则是直接从请求缓存中返回结果
减少重复请数,同一用户请求上下文相同依赖的服务数据始终一致,请求缓存在run()和construct()执行之前生效,所以可以有效减少不必要的线程开销
清理失效缓存,在请求命令中存在更新数据的操作,缓存中的数据就需要在写操作的时候即使清理,以防读取脏数据
通过HystrixRequestCache.clear()方法进行缓存清理
/**
* 定义清理缓存的方法,在run()或construct()方法中写操作后调用
* @param id
*/
public static void reflushCache(Long id){
//根据命令key获取该命令的请求缓存对象
HystrixRequestCache.getInstance(HystrixCommandKey.Factory.asKey("commandKey"),
HystrixConcurrencyStrategyDefault.getInstance()).clear(String.valueOf(id));
}
在一个服务中提供查询获取的方法,使用Hystrix的缓存机制,在另外一个提供写的方法时候,需要清空缓存,这样才能保证下次获取的数据不会走缓存
如果使用注解实现请求缓存
除了传统的实现方式外,可以通过注解进行配置实现,主要有以下3个注解
- 设置请求缓存
主要通过@CacheResult注解来实现,当依赖服务被调用的时候并返回User对象,由于该方法被@CacheResult注解修改,所以Hystrix会将该结果置入缓存中,缓存key值就是它所有的参数
@CacheResult
@HystrixCommand
protected User getUserById( Long id) throws Exception {
return template.getForObject("http://self-core/user/{1}",User.class,id);
}
- 自定义缓存key
当使用注解来定义请求缓存的时,我们要为请求命令指定具体的缓存key生成规则时,可以使用@CacheResult和@CacheRemove注解的cacheKeyMethod属性来指定具体的生成函数,通过在请求命令的同一个类中定义一个专门生成Key的方法,并用cacheKeyMethod属性来指定该方法
或者通过@CacheKey注解在方法参数中指定缓存key,它优先级比cacheKeyMethod,在同时配置的情况下会被覆盖,@CacheKey注解除了可以指定方法参数作为缓存key之外,还可以指定参数对象的内部属性作为缓存key
使用cacheKeyMethod属性指定缓存key
@CacheResult(cacheKeyMethod = "getCacheKey")
@HystrixCommand
protected User getUserById( Long id) throws Exception {
return restTemplate.getForObject("http://self-core/user/{1}",User.class,id);
}
public Long getCacheKey(Long id){
return id;
}
使用@CacheKey作为缓存key
@CacheResult
@HystrixCommand
protected User getUserById(@CacheKey Long id) throws Exception {
return restTemplate.getForObject("http://self-core/user/{1}",User.class,id);
}
使用@CacheKey指定参数对象的属性作为缓存key
@CacheResult
@HystrixCommand
protected User getUserById(@CacheKey("id") User user) throws Exception {
return restTemplate.getForObject("http://self-core/user/{1}",User.class,id);
}
- 缓存清理
可以通过@CacheRemove注解来实现失效缓存的清理,@CacheRemove注解的commandKey属性是必须要指定的,他用来指明需要使用请求缓存的请求命令,因为只有通过配置该属性,Hystrix才能找到正确的请求命令缓存位置
@CacheResult
@HystrixCommand
protected User getUserById(@CacheKey("id") Long id) throws Exception {
return restTemplate.getForObject("http://self-core/user/{1}",User.class,id);
}
@CacheRemove(commandKey = "getUserById")
@HystrixCommand
protected User getUserById(@CacheKey("id") User user) throws Exception {
return restTemplate.getForObject("http://self-core/user/{1}",User.class,id);
}
请求合并
声明式服务调用:Spring Cloud feign
我们在使用Spring Cloud Ribbon时,通常会利用它对RestTemplate的请求拦截来实现对依赖服务的接口调用,而RestTemplate实现了HTTP请求的封装处理,形成一套模板化的调用方法,一个接口的多次调用起来会很重复,而且是简单的模板化内容,Spring Cloud Feign在此基础上做了进一步封装,由它来帮助我们定义和实现依赖服务接口的定义,在spring Cloud Feign的作用下我们只需要创建一个接口并用注解的方式配置它即可完成服务提供方的接口绑定。
快速入门
创建一个springBoot应用,在pom.xml中引入spring-cloud-starter-eureka和spring-cloud-start-feign的依赖
在入口的主类中FeignDemoApplication中添加@EnableFeignClients注解开启Spring Cloud Feign的支持功能
@EnableDiscoveryClient //该应用为Eureka客户端应用,获取服务注册或发现的能力
@EnableFeignClients// 开启spring Cloud Feign支持
@SpringBootApplication
public class FeignDemoApplication {
public static void main(String[] args) {
SpringApplication.run(FeignDemoApplication.class, args);
}
}
定义一个helloService接口,通过@FeignClient注解指定服务名来绑定服务,在使用springMVC的注解@RequestMapping来绑定具体该服务提供者的REST接口(服务名不区分大小写)
@FeignClient("self-core")
public interface HelloService {
@RequestMapping("/index")
String helloService();
@PostMapping("/index1")
String helloService1(@RequestBody User user);
@GetMapping("/index2")
String helloService2(@RequestParam("id") Long id);
@GetMapping("/index3")
User helloService3(@RequestHeader("id") Long id, @RequestHeader("name")String name);
}
接着,创建一个创建一个HelloController来实现对Feign客户端的调用,使用@Autowired直接注入上面定义的HelloService,在controller中实现对helloService的调用并提供了对外的接口
Core-self服务提供的服务如下:
@PostMapping("/index1")
public String show (@RequestBody User user) throws Exception{
return user.getName()+"=="+user.getId();
}
@GetMapping("/index2")
public String method(@RequestParam("id") Long id) throws Exception{
return "hello"+id;
}
@GetMapping("/index3")
public User shit(@RequestHeader("id") Long id,@RequestHeader("name")String name) throws Exception{
return new User(id,name);
}
最后通Ribbon实现服务消费者一样,在application.properties中指定服务注册中心,服务名,端口等信息
spring.application.name=feign-customer
server.port=15675
#服务注册中心的地址,集群配置多个用逗号隔开
eureka.client.service-url.defaultZone=http://localhost:1234/eureka/
测试,通过
在实际开发的过程中,同一个项目可能是服务的消费者也可能是服务的提供者。
参数绑定:RequestParam.RequestHead,RequestBody,ModelAttribute
继承特征
由于服务提供方的和服务客户端的接口数据基本相同,我们可以考虑将这部分内容进一步的抽像起来使用