Spring Cloud微服务阅读随笔--第5章【服务容错保护:Spring Cloud Hystrix-断路器】
第5章【服务容错保护:Spring Cloud Hystrix】
在微服务架构中,我们将系统拆分成了很多服务单元,各单元的应用间通过服务注册与订阅的方式互相依赖。由于每个单元都在不同的进程中运行,依赖通过远程调用的执行,这样就有可能因为网络原因或是服务自身问题出现调用故障或延迟,而这些问题会直接导致调用方的对外服务也出现延迟,若此时调用方的请求不断增加,最后就会因等待出现故障的依赖方响应形成任务挤压,最终导致自身服务的瘫痪。
举个例子,在一个电商网站中,我们可能会将系统拆分成用户、订单、库存、积分、评论等一系列服务单元。用户创建一个订单的时候,客户端将会调用订单服务的创建订单接口,此时创建订单接口又向库存服务来请求出货(判断是否足够库存来出货)。此时若库存服务因自身处理逻辑等原因造成响应缓慢,会直接导致创建订单服务的线程被挂起,以等待库存申请服务的响应,在漫长的等待之后用户会因为请求库存失败而得到创建订单失败的结果。如果在高并发情况下,因这些挂起的线程在等待库存服务的响应而未能释放,使的后续到来的创建订单请求被阻塞,最终导致订单服务不可用。
在微服务架构中,存在着那么多的服务单元,若一个单元出现故障,就很容易因依赖关系而引发故障的蔓延,最终导致整个系统的瘫痪,这样的架构相较传统架构更加不稳定。为了解决这样的问题,产生了断路器等一系列的服务保护机制。
断路器模式源于Martin Fowler的Circuit Breaker一文。“断路器”本身是一种开关装置,用于在电路上保护线路过载,当线路中有电器发生断路时,“断路器”能够及时切断故障电路,防止发生过载、发热甚至起火等严重后果。
在分布式架构中,断路器模式的作用也是类似的,当某个服务单元发生故障(类似用电器发生断路)之后,通过断路器的故障监控(类似熔断保险丝),向调用返回一个错误响应,而不是长时间的等待。这样就不会使得线程因调用故障服务被长时间占用不释放,避免了故障在分布式系统中的蔓延。
针对上述问题,Spring Cloud Hystrix 实现了断路器、线程隔离等一系列服务保护功能。它也是基于Netflix的开源框架Hystrix实现的,该框架的目标在于通过控制那些访问远程系统、服务和第三方库的节点,从而对延迟和故障提供更强大的容错能力。Hystrix具备服务降级、服务熔断、线程和型号隔离、请求缓存、请求合并以及服务监控等强大功能。
接下来,我们就从一个简单示例开始对Spring Cloud Hystrix的学习与使用。
快速入门
在开始使用Spring Cloud Hystrix实现断路器之前,我们先用之前实现的一些内容作为基础,构建一个如下图架构所示的服务调用关系。
我们在这里需要启动的工程有如下一些。
1. eureka-server工程:服务注册中心,端口为1111
2. hello-service 工程:HELLO-SERVICE的服务单元。两个示例启动端口分别为8081和8082
3. ribbon-consume工程:使用Ribbon实现的服务消费者,端口为9000
在未加入断路器之前,关闭8081的实例,发送GET请求到http://localhost:9000/ribbon-consumer,可以获得下面的输出:
{ "timestarnp": 1473992080343, "status": 500, "error": "Internal Server Error", "exception": "org.springfrarnework.web.client.ResourceAccessException", "message": "I/0 error on GET request for \"http://HELLO-SERVICE/hello\": Connection refused: connect; nested exception is java.net.ConnectException: Connection refused: connect", "path": "/ribbon-consumer" }
下面我们开始引入Spring Cloud Hystrix
1. 在ribbon-consumer工程的pom.xml的dependency节点中引入spring-cloud-starter-hystrix依赖:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactid>spring-cloud-starter-hystrix</artifactid> </dependency>
2. 在ribbon-consumer工程的主类ConsumerApplication中使用@Enable-CiruitBreaker注解开启断路器功能:
@EnableCircuitBreaker @EnableDiscoveryClient @SpringBootApplication public class ConsumerApplication {
@Bean @LoadBalanced RestTemplate restTemplate() { return new RestTemplate(); }
public static void main(String[) args) { SpringApplication.run(ConsumerApplication.class, args); } }
注意:这里还可以使用Spring Cloud 应用中的@SpringCloudApplication注解来修饰应用主类,该注解的具体定义如下所示。可以看到,该注解中包含了上述我们引用的三个注解,这也意味着一个Spring Cloud标准应用包含服务发现以及断路器。
@Target ({ ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
public @interface SpringCloudApplication {
}
3. 改造服务消费方式,新增HelloService类,注入RestTemplate实例。然后,将在ConsumerController中对RestTemplate的使用迁移到helloService函数中,最后,在helloService函数上增加@HystrixCommand注解来指定回调方法:
@Service public class HelloService {
@Autowired RestTemplate restTemplate;
@HystrixCommand(fallbackMethod = "helloFallback") public String helloService() { return restTemplate.getForEntity("http://HELLO-SERVICE/hello", String.class) .getBody(); }
public String helloFallback () ( return "error"; } }
5. 修改ConsumerController类,注入上面实现的HelloService实例,并在helloConsumer中进行调用:
@RestController public class ConsumerController{ @Aurowired HelloService helloService; @RequestMapping(value="/ribbon-consumer",method=RequestMethod.GET) public String helloConsumer(){ return helloService.helloservice(); } }
下面,我们来验证下通过断路器实现的服务回调逻辑,重新启动之前关闭的8081端口的Hello-Service,确保此时服务注册中心、两个Hello-Service以及RIBBON-CONSUMER均已启动,访问http://localhost:9000/ribbon-consumer可以轮询两个HELLO-SERVICE并返回一些文字信息。此时我们继续断开8081的HELLO-SERVICE,然后访问http://localhost:9000/ribbon-consumer,当轮询到8081服务端时,输出内容为error,不再是之前的错误内容,Hystrix的服务回调生效。除了通过断开具体的服务实例来模拟某个节点无法访问的情况之外,我们还可以模拟一下服务阻塞(长时间未响应)的情况。我们对HRLLO-SERVICE的/hello接口做一些修改,具体如下:
@requestMapping(value="/hello",method=requestMethod.GET) public String hello() throws Exception{ ServiceInstance instance = client.getLocalServiceInstance(); //让处理线程等待几秒钟 int sleepTime = new Randow().nextInt(3000); logger.info("sleepTime:"+sleepTime); Thread.sleep(sleepTime); logger.info("/hello,host:"+instance.getHost()+","service_id:"+instance.getServiceId()); return "Hello World"; }
通过Thread.sleep()函数可让/hello接口的处理线程不是马上返回内容,而是在阻塞几秒之后才返回内容。由于Hystrix默认超时时间为2000毫秒,所以这里采用了0至3000的随机数以让处理过程有一定概率发生超时来触发断路器。为了更精准地观察断路器的触发,在消费者调用函数中做一些时间记录,具体如下:
@HystrixCommand(fallbackMethod="helloFallback",commandKey="hellokey") public String hello(){ long start = System.currentTimeMillis(); //消费服务的逻辑 ... long end = System.currentTimeMillis(); logger.info("Spend time:"+(end - start)); return result.toString(); }
重新启动HELLO-SERVICE和RIBBON-CONSUMER的实例,连续访问http://loaclhost:9000/ribbon-consumer几次,我们可以观察到,当RIBBON-CONSUMER的控制台中输出的Spend time大于2000的时候,就会返回error,即服务消费者因调用的服务从而触发熔断请求,并调用逻辑返回结果。
原理分析
通过上面的快速入门示例,我们对Hystrix的使用场景和使用方法已经有了一个基础的认识。接下来我们通过解读Hetflix Hystrix官方的流程图来详细了解一下:当一个请求调用了相关服务依赖之后Hystrix是如何工作的(即如上例中所示,当访问了http://localhost:9000/ribbon-consumer请求之后,在RIBBON-CONSUMER中是如何处理的)。
工作流程图
1. 创建HystrixCommand或HystrixObservableCommand对象
首先,构建一个HystrixCommand或是HystrixObservableCommand对象,用来表示对依赖服务的操作请求,同时传递所有需要的参数。从其命名中我们就能知道它才用了“命令模式”来实现对服务调用操作的封装。而这两个Command对象分别针对不同的应用场景。
1. HystrixCommand:用来依赖的服务返回单个操作结果的时候。
2. HystrixObservableCommand:用在依赖的服务返回多个操作结果的时候。
命令模式,将来自客户端的请求封装成一个对象,从而让你可以使用不同的请求对客户端进行参数优化。它可以被用于实现“行为请求者”与“行为实现者”的解耦,以便使两者可以适应变化。下面的实例是对命令模式的简单实现:
//接收者 public class Receiver { public void action() { //真正的业务逻辑 } } //抽象命令 interface Command { void execute(); } //具体命令实现 public class ConcreteCommand implements Command { private Receiver receiver; public ConcreteCommand(Receiver receiver) { this.receiver = receiver; } public void execute() { this.receiver.action(); } } //客户端调用者 public class Invoker { private Command command; public void setCommand(Command command) { this.command= command; } public void action() { this. command. execute() ; } public class Client { public static void main(String[] args) { Receiver receiver = new Receiver(); Command command = new ConcreteCommand(receiver); Invoker invoker = new Invoker(); invoker.setCommand(command); invoker.action(); //客户端通过调用者来执行命令 } }
从代码中,我们可以看到这样几个对象。
1. Receiver:接收者,它知道如何处理具体的业务逻辑。
2. Command:抽象命令,它定义了一个命令对象应具备的一系列命令操作,比如execute()、undo()、redo()等。当命令操作被调用的时候就会触发接收者去做具体命令对应的业务逻辑。
3. ConcreteCommand:具体的命令实现,在这里它绑定了命令操作与接受者之间的关系,execute()命令的实现委托给了Recever的action()函数。
4. Invoker:调用者,它持有一个命令对象,并且可以在需要的时候通过命令对象完成具体的业务逻辑。
从上面的实例中,我们可以看到,调用者Invoker与操作者Receiver通过Command命令接口实现了解耦。对于调用者来说,我们可以为其注入多个命令操作,比如新建文件、复制文件、删除文件这样三个操作,调用者只需在需要的时候直接调用即可,而不需要知道这些操作命令实际是如何实现的。而在这里所提到的HYstrixCommand和HystrixObservableCommand则是在Hystrix中对Command的进一步抽象定义。在后续的内容汇总,会逐步展开介绍它的部分内容来帮助理解器运作原理。
从上面的实例中我们也可以发现,Invoker和Receiver的关系非常类似于“请求-响应”模式,所以它比较适用于实现记录日志、撤销操作、队列请求等。
在下面这些情况下应考虑使用命令模式。
1. 使用命令模式作为“回调(CallBack)”在面向对象(oop)系统中的替代。“CallBack”讲的便是先将一个函数登记上,然后在以后调用此函数。
2. 需要在不同的时间指定请求、将请求排队。一个命令对象和原先的请求发出者可以有不同的生命期。换言之,原先的请求发出者可能已经不在了,而命令对象本身仍是活动的。这是命令的接收者可以是在本地,也可以在网络的另一个地址。命令对象可以在序列化之后传送到另外一台机器上去。
3. 系统需要支持命令的撤销。命令对象可以把状态存储起来,等到客户端需要撤销命令所产生的效果时,可以调用undo()方法,把命令所产生的效果撤销掉。命令对象还可以提供redo()方法,以供客户端在需要时再重新实施命令效果。
4. 如果要将系统中所有的数据更新到日志里,以便在系统崩溃时,可以根据日志读回所有的数据更新命令,重新调用Execute()方法一条一条执行这些命令,从而恢复系统在崩溃前所做的数据更新。
2. 命令执行
从图中我们可以看到一共存在4种命令的执行方式,而Hystrix在执行时会根据创建的Command对象以及具体的情况来选择一个执行。其中HysrixCommand实现了下面两个执行方式、
1. execute():同步执行,从依赖的服务返回一个单一的结果对象,或是在发生错误的时候抛出异常。
2. queue():异常执行,直接返回一个Future对象,其中包含了服务执行结束时要返回的单一结果对象。
R value = command.execute();
Future<R> fValue = command.queue();
而HystrixObservableCommand实现了另外两种执行方式。
1. observe():返回Observable对象,它代表了操作的多个结果,它是一个Hot Observable。
2. toObservable():同样会返回对象,它代表了操作的多个结果,但它返回的是一个Cold Observable。
Observable<R> ohValue = command.observe();
Observable<R> ocValue = command.toObservable();
在Hystrix的底层实现中大量地使用了RxJava,为了更容易地理解后续内容,在这里对RxJava的观察者-订阅者模式做一个简单的入门介绍。
上面我们所提到的Observable对象就是RxJava中的核心内容之一,可以把它理解为“事件源”或是“被观察者”,与其对应的Subscriber对象,可以理解为“订阅者”或是“观察者”。这两个对象是RxJava响应式编程的重要注册部分。
1. Observable用来向订阅者Subscriber对象发布事件,Subscriber对象则在接收到事件后对其进行处理,而在这里所指的时间通常就是对依赖服务的调用。
2. 一个Observable可以发出多个事件,直到结束或是发生异常。
3. Observable对象每发出一个事件,就会调用对应观察者Subscriber对象的onNext()方法。
4. 每一个Observable的执行,最后一定会通过调用Subscriber.onCompleted()或是Subscriber.onError()来结束该事件的操作流。
下面我们通过一个简单的例子来直观理解一下Observable与Subscribers:
//创建事件源observable Observable <String> observable = Observable.create(new Observable.OnSubscribe<String>){ @Override public void call(Subscriber<? super String>subscriber){ subscriber.onNext("Hello RXJava"); subscriber.onNext("i am 杨大爷"); subscriber.onCompleted(); } }); //创建订阅者subscriber Subscriber<String> subscriber= new Subscriber<String>(){ @Override public void onCompleted(){ } @Override public void onError(Throwable e){ } @Override public void onNext(String s){ System.out.println("Subscriber:"+s); } }; //订阅 observable.subscribe(subscriber);
在该示例中,创建了一个简单的事件源observable,一个对事件传递内容输出的订阅者subscriber,通过observable.subscribe(subscriber)来触发事件的发布。
在这里我们对于事件源observable提到了两个不同的概念:Hot Observable 和 Cold Observable,分别对应了上面command.observe()和 command.toObservable()的返回对象。其中Hot Observable,它不论“事件源”是否有“订阅者”,都会在创建后对事件进行发布,所以对于Hot Observable的每一个“订阅则”都有可能是从“事件源”中的中途开始的,并可能只是看到了整个操作的局部过程。而Cold Observable在没有“订阅者”的时候并不会发布事件,而是进行等待,直到有“订阅者”之后才发布事件,所以对于Cold Observable的订阅者,它可以保证从一开始看到整个操作的全部过程。
大家从表面上可能认为只是在HysrixObservableCommand中使用了RxJava,然而实际上execute(),queue()也都使用了RxJava来实现。从下面的源码中我们可以看到:
1. execute()是通过queue()返回的异步对象Future<R>的get()方法来实现同步执行的。该方法会等待任务执行结束,然后获得R类型的结果进行返回。
2. queue()则是通过toObservable()来获得一个Cold Observable,并且通过toBlocking()将该Observable转换成BlockingObservable,并且通过toBlocking()将该Observable转换成BlockingObservable,它可以把数据以阻塞的方式发射出来。而 toFuture方法 则是把BlockingObservable转换为一个Future,该方法只是创建一个Future返回,并不会阻塞,这使得消费者可以自己决定如何处理异步操作。而execute()就是直接使用了queue()返回的Future中的阻塞方法get()来实现同步操作的。同时通过这种方式转换的Future要求Observable只发射一个数据,所以这两个实现都只能返回单一结果。
public R execute(){ try{ return queue().get(); }catch (Exception e){ throw decomposeException(e); } } public Future<R> queue(){ final Observable<R> o = toObservable(); final Future<R> f = o.toBlocking().toFuture(); if (f.isDone()){ //处理立即抛出的错误 ... } return f; }
3. 结果是否被缓存
若当前命令的请求缓存功能是被启用的,并且该命令缓存命中,那么缓存的结果会立即以Observable对象的形式返回。
4. 断路器是否打开
在命令结果没有缓存命中的时候,Hystrix在执行命令前需要检查断路器是否为打开状态:
1. 如果断路器是打开的,那么Hystrix不会执行命令,而是转接到fallback处理逻辑(对应下面第8步)。
2. 如果断路器是关闭的,那么Hystrix跳到第5步,检查是否有可用资源来执行命令。
关于断路器的具体实现细节,后续会更加详细的分析。
5. 线程池、请求队列、信号量是否占满
如果与命令相关的线程池和请求队列,或者信号量(不使用线程池的时候)已经被占满,那么Hystrix也不会执行命令,而是转接到fallback处理逻辑(对应下面第8步)。
需要注意的是,这里Hystrix所判断的线程池并非容器的线程池,而是每个依赖服务的专有线程池。Hystrix为了保证不会因为某个依赖服务的问题影响到其他依赖服务而采用了“舱壁模式”(Bulkhead Pattern)来隔离每个依赖的服务。关于依赖服务的隔离与线程池相关的内容见后续详细介绍。
6. HysrixObservableCommand.construct()或HystrixCommand.run()
Hysrix会根据我们编写的方法来决定采取什么样的方式去请求依赖服务。
1. HysrixCommand.run():返回一个单一的结果,或者抛出异常。
2. HystrixObservableCommand.construct():返回一个Observable对象来发射多个结果,或通过onError发送错误通知。
如果run()或construct()方法的执行时间超过了命令设置的超时阈值,当前处理线程将会抛出一个TimeoutException(如果该命令不在其自身的线程中执行,则会通过单独的计时线程来抛出)。在这种情况下,Hystrix会转接到fallback处理逻辑(第8步)。同时,如果当前命令没有被取消或中断,那么它最终会忽略run()或者construct()方法的返回。
如果命令没有抛出异常并返回了结果,那么Hystrix在记录一些日志并采集监控报告之后将该结果返回。在使用run()的情况下,Hystrix会返回一个Observable,它发射单个结果并产生onCompleted的结束通知:而在使用construct()的情况下,Hystrix会直接返回该方法产生的Observable对象。
7. 计算断路器的健康度
Hystrix会将“成功”、“失败”、“拒绝”、“超时”等信息报告给断路器,而断路器会维护一组计数器来统计这些数据。
断路器会使用这些统计数据来决定是否要将断路器打开,来对某个依赖服务的请求进行“熔断、短路”,直到恢复期结束后,根据统计数据判断如果还是未达到健康指标,就再次“熔断、短路”。
8. fallback处理(服务降级)
当命令执行失败的时候,Hystrix会进入fallback尝试回退处理,我们通常也称该操作为“服务降级”。而能引起服务降级处理的情况有下面几种:
1. 第4步,当前命令处于“熔断、短路”状态,断路器是打开的时候。
2. 第5步,当前命令的线程池、请求队列或者信号量被占满的时候。
3. 第6步,HystrixObservableCommand.construct()或HystrixCommand.run()抛出异常的时候。
在服务降级逻辑中,我们需要实现一个通用的响应结果,并且该结果的处理逻辑应当是从缓存或是根据一些静态逻辑来获取,而不是依赖网络请求获取。如果一定要在降级逻辑中包含网络请求,那么该请求也必须被包装在HystrixCommand或是HystrixObservableCommand中,从而形成级联的降级策略,而最终的降级逻辑一定不是一个依赖网络请求的处理,而是一个能够稳定地返回结果的处理策略。
在HystrixCommand和HysrixObservableCommand中实现降级逻辑时还略有不同:
1. 当使用HystrixCommand的时候,通过实现HystrixCommand.getFallback()来实现服务降级逻辑。
2. 当使用HystrixObservableCommand的时候,通过HystrixObservable-command.resumeWithFallback()实现服务降级逻辑,该方法会返回一个Observable对象来发射一个或多个降级结果。
当命令的降级逻辑返回结果之后,Hystrix就将该结果返回给调用者。当使用HystrixCommand.getFallback()的时候,它会返回一个Observable对象,该对象会发射getFallback()的处理结果。而使用HystrixObservableCommand.resumeWithFallback()实现的时候,它会将Observable对象直接返回。
如果我们没有为命令实现降级逻辑或者降级处理逻辑中抛出了异常,Hystrix依然会返回一个Observable对象,但是它不会发射任何结果数据,而是通过onError方法通知命令立即中断请求,并通过onError()方法将引起命令失败的异常发送给调用者。实现一个有可能失败的降级逻辑是一种非常糟糕的做法,我们应该在实现降级策略时尽可能避免失败的情况。
当然完全不可能出现失败的完美策略是不存在的,如果降级执行发现失败的时候,Hystrix会根据不同的执行方法做出不同的处理。
3. execute():抛出异常。
4. queue():正常返回Future对象,但是当调用get()来获取结果的时候会抛出异常。
5. observe():正常返回Observable对象,当订阅它的时候,将立即通过调用订阅者的onError方法来通知中止请求。
6. toObservable():正常返回Observable对象,当订阅它的时候,将通过调用订阅者的onError方法来通知中止请求。
9. 返回成功的响应
当Hystrix命令执行成功之后,它将处理结果直接返回或是以Observable的形式返回。而具体以哪种方式返回取决于之前第2步中我们所提到的对命令的4种不同执行方式,下图中总结了这4种调用方式之间的依赖关系。我们可以将此图与在第2步中对前两者源码的分析联系起来,并且从源头toObservable()来开始分析。
1. toObservable():返回最原始的Observable,必须通过订阅它才会真正触发命令的执行流程。
2. observe():在toObservable()产生原始Observable之后立即订阅它,让命令能够马上开始异步执行,并返回一个Observable对象,当调用它的subscribe时,将重新产生结果和通知给订阅者。
3. queue():将toObservable()产生的原始Observable通过toBlocking()方法转换成BlockingObservable对象,并调用它的toFuture()方法异步的Future对象。
4. execute():在queue()产生异步结果Future对象之后,通过调用get()方法阻塞并等待结果的返回。
断路器原理
断路器在HystrixCommand和HystrixObservableCommand执行过程中起到了举足轻重的作用,它是Hystrix的核心部件。那么断路器是如何决策熔断和记录信息呢?
我们先来看看断路器HystrixCircuitBreaker的定义:
public interface HystrixcircuitBreaker{ public static class Factory{...}
static class HystrixCircuitBreakerImpl complements HystrixCircuitBreaker{...}
static class NoOpCircuitBreaker implements HystrixCircuitBreaker{...}
public boolean allowRequest{};
public boolean isOpen{};
void markSuccess{}; }
可以看到它的接口定义并不复杂,主要定义了三个断路器的抽象方法。
1. allowRequest():每个Hystrix命令的请求都通过它判断是否被执行。
2. isOpen():返回当断路器是否打开。
3. markSuccess():用来闭合断路器。
另外还有三个静态类。
1. 静态类Factory中维护了一个Hystrix命令与HysrixCircuitBreaker的关系集合:ConcurrentHashMap<String,HysrixCircuitBreaker>circuit-BreakersByCommand,其中String类型的key通过HystrixCommandKey定义,每一个Hystrix命令需要有一个key来标识,同时一个Hystrix命令也会在该集合中找到它对应的断路器HystrixCircuitBreaker实例。
2. 静态类NoOpCircuitBreaker定义了一个什么都不做的断路器实现,它允许所有请求,并且断路器状态始终闭合。
3. 静态类HystrixCiruitBreakerImpl是断路器接口HystrixCircuitBreaker的实现类,在该类中定义了断路器的4个核心对象。
a. HystrixCommandProperties:断路器对应 HystrixCommand 实例的属性对象,它的详细内容我们将在后续章节做具体的介绍。
b. HystrixCommandMetrics metrics:用来让 HystrixCommand 记录各类 度量指标的对象。
c. AtomicBoolean circuitOpen:断路器是否打开的标志,默认为false。
d. AtomicLong circuitOpenedOrLastTestedTime:断路器打开或是上一次 测试的时间戳。
HystrixCircuitBreakeImpl对 HystrixCircuitBreaker接口的各个方法实 现如下所示。
1. isOpen ():判断断路器的打开/关闭状态。 详细逻辑如下所示。
a. 如果断路器打开标识为true, 则直接返回true, 表示断路器处于打开状态。否则, 就从度量指标对象metrics中获取 HealthCounts统计对象做进一步判断(该对象记录了一个滚动时间窗内的请求信息快照,默认时间窗为10秒)。
i. 如果它的请求总数(QPS)在预设的阙值范围内就返回 false,表示断路器处于未打开状态。该阙值的配置参数为circuitBreakerRequestVolumeThreshold, 默认值为20。
ii. 如果错误百分比在阈值范围内就返回 false, 表示断路器处于未打开状态。该阙值的配置参数为circuitBreakerErrorThresholdPercentage,默认值为50。
iii. 如果上面的两个条件都不满足,则将断路器设置为打开状态 (熔断/短路)。 同时,如果是从关闭状态切换到打开状态的话,就将当前时间记录到上面提到的 circuitOpenedOrLastTestedTirne 对象中。
public boolean isOpen(){ if (circuitOpen.get()){ return true; } HealthCounts health = metrics.getHealthCounts(); if (health.getTotalRequests()<properties.circuitBreakerRequestVolumeThreshold().get()){ return false; } if (health.getErrorPercentage()<properties.circuitBreakerErrorThresholdPercentage().get()){ return false; }else{ if (circuitOpen.compareAndSet(false,true)){ circuitOpenedOrLastTestedTime.set(System.currentTimeMillis()); return true; }else{ return ture; } } }
2. allowRequest(): 判断请求是否被允许,这个实现非常简单。 先根据配置 对象 properties中的断路器判断强制打开或关闭属性是否被设置。 如果强制打开,就直接返回false,拒绝请求。 如果强制关闭,它会允许所有请求,但是同时也会调用 isOpen ()来执行断路器的计算逻辑, 用来模拟断路器打开/关闭的行为。 在默认情况下,断路器并不会进入这两个强制打开或关闭的分支中去,而是通过 ! isOpen () I I allowSingleTest ()来判断是否允许请求访问。 !isOpen()之前已经介绍 过, 用来判断和计算当前断路器是否打开,如果是断开状态就允许请求。 那么 allowSingleTest()是用来做什么的呢?
@Override public boolean allowRequest(){ if (properties.cicuitBreakerForceOpen().get()){ return false; } if (properties.circuitBreakerForceClosed().get()){ isOpen(); return true; } return !isOpen() || allowSingleTest(); }
从allowSingleTest()的实现中我们可以看到,这里使用了在isOpen()函数中当 断路器从闭合到打开时候所记录的时间戳。 当断路器在打开状态的时候,这里会判断 (断开时的时间戳+配置中的circuitBreakerSleepWndowinMilliseconds 时间 )是否小于当前时间,是的话,就将当前时间更新到记录断路器打开的时间对象 circuitOpenedOrLastTestedTime 中,并且允许此次请求。 简单地说, 通过 circuitBreakerSleepWindowinMilliseconds 属性设置了一个断路器打开 之后的休眠时间(默认为5秒),在该休眠时间到达之后,将再次允许请求尝试访问,此时断路器处于 “半开” 状态,若此时请求继续失败, 断路器又进入打开状态, 并继续等待下一个休眠窗口过去之后再次尝试;若请求成功, 则将断路器重新置于关闭状态。所以通过 allowSingleTest()与isOpen ()方法的配合,实现了断路器 打开和关闭状态的切换。
public boolean allowSingleTest(){ long timeCircuitOpenedOrWasLastTested = circuitOpenedOrLastTestedTime.get(); if (circuitOpen.get()&& System.currentTimeMillis()>timeCircuitOpenedOrWasLastTested+properties.circuitBreakerSleepWindowInMilliseconds().get()){ if (circuitOpenedOrLastTestedTime.compareAndSet(timeCircuitOpenedOrWasLastTested, System.currentTimeMillis())){ return true; } } return false; }
3. markSuccess():该函数用来在“半开路”状态时使用。若在Hystrix命令调用成功通过调用它将打开的断路器关闭,并重置度量指标对象。
public void markSuccess(){ if (circuitOpen.get()){ if (circuitOpen.compareAndSet(true,false)){ metrics.resetStream(); } } }
下图是Netflix Hystrix 官方文档中关于断路器的详细执行逻辑,可以帮助我们理解上面的分析内容
依赖隔离
“舱壁模式”对于熟悉Docker的读者一定不陌生,Docker通过“舱壁模式”实现进程的隔离,使得容器与容器之间不会互相影响。而Hystrix则使用该模式实现线程池的隔离,它会为每一个依赖服务创建一个独立的线程池,这样就算某个依赖服务出现延迟过高的情况,也只是对该依赖服务的调用产生影响,而不会拖慢其他的依赖服务。
通过实现对依赖服务的线程池隔离,可以带来如下优势:
1. 应用自身得到完全保护,不会受不可控的依赖服务影响。即便给依赖服务分配的线程池被填满,也不会影响应用自身的其余部分。
2. 可以有效降低接入新服务的风险。如果新服务接入后运行不稳定或存在问题,完全不会影响应用其他的请求。
3. 当依赖的服务从失效恢复正常后,它的线程池会被清理并且能够马上恢复健康的服务,相比之下,容器级别恢复速度要慢得多。
4. 当依赖的服务出现配置错误的时候,线程池会快速反映出此问题(通过失败次数、延迟、超时、拒绝等指标的增加情况)。同时,我们可以在不影响应用功能的情况下通过实时的动态属性刷新(后续会通过Spring Cloud Config 与Spring Cloud Bus的联合使用来介绍)来处理它。
5. 当依赖的服务因实现机制调整等原因造成其性能出现很大变化的时候,线程池的监控指标信息会反映出这样的变化。同时,我们也可以通过实时动态刷新自身应用对依赖服务的阈值进行调整以适应依赖方的改变。
6. 除了上面通过线程池隔离服务发挥的优点之外,每个专有线程池都提供了内置的并发实现,可以利用它为同步的依赖服务构建异步访问。
总之,通过对依赖服务实现线程池隔离,可让我们的应用更加健壮,不会因为个别依赖服务出现问题而引起非相关服务的异常。同时,也使得我们的应用变得更加灵活,可以在不停止服务的情况下,配合动态配置刷新实现性能配置上的调整。
虽然线程池隔离的方案带来如此多的好处,但是很多使用者可能会担心为每一个依赖服务都分配一个线程池是否过多地增加系统的负载和开销。对于这一点,使用者不用过于担心,因为这些顾虑也是大部分工程师们会考虑到的,Netflix也针对线程池的开销做了相关的测试,以用结果打消Hystrix实现性能影响的顾虑。
下图是Netflix Hystrix官方提供的一个Hystrix命令的性能监控图,该命令以每秒60个请求的速度(QPS)对一个单服务实例进行访问,该服务实例每秒运行的线程数峰值为350个。
从图中的统计我们可以看到,使用线程池隔离与不使用线程池隔离的耗时差异如下表所示:
在99%的情况下,使用线程池隔离的延迟有9ms,对于大多数需求来说这样的消耗是微乎其微的,更何况可为系统在稳定性和灵活性上带来巨大的提升。虽然对于大部分的请求我们可以忽略线程池的额外开销,而对于小部分延迟本身就非常小的请求(可能只需要1ms),那么9ms的延迟开销还是非常昂贵的。实际上Hystrix也为此设计了另外的解决方案:信号量。
在Hystrix中除了可使用线程池之外,还可以使用信号量来控制单个依赖服务的并发度,信号量的开销远比线程池的开销小,但是它不能设置超时和实时异步访问。所以,只有在依赖服务是足够可靠的情况下才使用信号量。在HystrixCommand和HystrixObservableCommand中有两处支持信号量的使用。
1. 命令执行:如果将隔离策略参数execution.isolation.strategy设置为SEMAPHORE,Hystrix会使用信号量替代线程池来控制依赖服务的并发。
2. 降级逻辑:当Hystrix尝试降级逻辑时,它会在调用线程中使用信号量。
信号量的默认值为10,我们也可以通过动态刷新配置的方式来控制并发线程的数量。对于信号量大小的估算方法与线程池并发度的估算类似。仅访问内存数据的请求一般耗时在1ms以内,性能可以达到5000rps(rps 指每秒的请求数),这样级别的请求可以将信号量设置为1 或者2 ,我们可以按此标准并根据实际请求耗时来设置信号量。
使用详解
在“快速入门”一节中我们已经使用过Hystrix中的核心注解@HystrixCommand,通过它创建了HystrixCommand的实现,同时利用了fallback属性指定了服务降级的实现方法。然而这些还只是Hystrix使用的一小部分,在实现一个大型分布式系统时,往往还需要更高级的配置功能。接下来我们将详细介绍Hystrix各接口和注解的使用方法。
创建请求命令
Hystrix命令就是我们之前所说的HystrixCommand,它用来封装具体的依赖服务调用逻辑。
我们可以通过继承的方式来实现,比如:
public class UserCommand extends HystrixCommand<User>{ private RestTemplate restTemplate; private Long id; public UserCommand(Setter setter,RestTemplate restTemplate,Long id ){ super(setter); this.restTemplate = restTemplate; this.id = id; } @Override protected User run(){ return restTemplate.getForObject("http://USER-SERVICE/users/{1}",User.class,id); } }
通过上面实现的UserCommand,我们既可以实现请求的同步执行也可以实现异步执行。
1. 同步执行:User u = new UserCommand(restTemplate , 1L).execute();
2. 异步执行:Future<User> futureUser = new UserCommand(restTemplate,1L).queue();。异步执行的时候,可以通过返回的futureUser调用get方法来获取结果。
另外,也可以通过@HystrixCommand注解来更为优雅地实现Hystrix命令的定义,比如:
public class UserService{ @Autowired private RestTemplate restTemplate; @HystrixCommand public User getUserById(Long id){ return restTemplate.getForObject("http://USER-SERVICE/users/{1}",User.class,id); } }
虽然@HystrixCommand注解可以非常优雅地定义Hystrix命令的实现,但是如上定义的getUserById方式只是同步执行的实现,若要实现异步执行则还需另外定义,比如:
@HystrixCommand public Future<User> getUserByIdAsync(final String id){ return new AsyncResult<User>(){ @Override public User invoke(){ return restTemplate.getForObject("http://USER-SERVICE/users/{1}",User.class,id); } }; }
除了传统的同步执行与异步执行之外,我们还可以将HystrixCommand通过Observable来实现响应式执行方式。通过调用observe()和toObservable()方法可以返回Obserable对象,比如:
Observable<String> ho = new UserCommand(restTemplate,1L).observe(); Observable<String> co = new UserCommand(restTemplate,1L).toObservable();
observe()和toObservable()虽然都返回了Observable,但是它们略有不同,前者返回的是一个Hot Observable,该命令会在observe()调用的时候立即执行,当Observable每次被订阅的时候会重放它的行为;而后者返回的是一个Cold Observable,toObservable()执行之后,命令不会被立即执行,只有当所有订阅者都订阅它之后才会执行。更多关于这两个方法的区别可见“原理分析”小节的内容,这里不做具体说明。
虽然HystrixComand具备了observe()和toObservable()的功能,但是它的实现有一定的局限性,它返回的Observable只能发射一次数据,所以Hystrix还提供了另外一个特殊命令封装HystrixObservableCommand,通过它实现的命令可以获取能发射多次的Observable。
如果使用HystrixObservableCommand来实现命令封装,需要将命令的执行逻辑在construct方法中重载,这样Hystrix才能将具体逻辑包装到Observable内,如下所示:
public class UserObservableCommand extends HystrixObservableCommand<User>{ private RestTemplate restTemplate; private Long id; public UserObservableCommand(Setter setter,RestTemplate restTemplate,Long id){ super(setter); this.restTemplate = restTempalte; this.id = id ; } @Override protected Observable<User> construct(){ return Observable.create(new Observable.OnSubscribe<User>(){ @Override public void call(Subscriber<? super User> observer){ try{ if(!observer.isUnsubscribed()){ User user= restTemplate.getForObject("http://USER-SERVICE/user/{1}",User.class,id); observer.onNext(user); observer.onCompleted(); } }catch (Exception e){ observer.onError(e); } } }); } }
而对此的注解实现依然是使用@HystrixCommand,只是方法定义需要做一些变化,具体内容与construct()的实现类似,如下所示:
@HystrixCommand public Observable<User> getUserById(final String id){ return Observable.create(new Observable.OnSubscribe<User>(){ @Override public void call(Subscriber<? super User>observer){ try{ if(!observable.isUnsubscribed()){ User user = restTemplate.getForObject("http://HELLO-SERVICE/users/{1}",User.class,id); observer.onNext(user); observer.onCompleted(); } }catch (Eception e){ observer.onError(e); } } }); }
在使用@HystrixCommand注解实现响应式命令时,可以通过observable-ExecutionMode参数来控制是使用observe()还是toObservable()的执行方式。该参数有下面两种设置方式。
1. @HystrixCommand(observableExecutionMode = ObservableExecution-Mode.EAGER):EAGER是该参数的模式值,表示使用observe()执行方式。
2. @HystrixCommand(observableExecutionMode = ObservableExecution-Mode.LAZY):表示使用toObservable()执行方式。
定义服务降级(fallback)
fallback是Hystrix命令执行失败时使用的后备方法,用来实现服务的降级处理逻辑。在HystrixCommand中可以通过重载getFallback()方法来实现服务降级逻辑,Hystrix会在run()执行过程中出现错误、超时、线程池拒绝、断路器熔断等情况时,执行getFallback()方法内的逻辑,比如我们可以用如下方式实现服务降级逻辑:
public class UserCommand extends HystrixCommand<User>{ private RestTemplate restTemplate; private Long id; public UserCommand(Setter setter,RestTemplate restTemplate,Long id){ super(setter); this.restTemplate = restTempalte; this.id = id ; } @Override protected User run(){ return restTemplate.getForObject("http://USER-SERVICE/users/{1}",User.class,id); }
@Override protected User getFallback(){ return new User(); } }
在HystrixObservableCommand实现的Hysrix命令中,我们可以通过重载resumeWithFallback方法来实现服务降级逻辑。该方法返回一个Observable对象,当命令执行失败的时候,Hystrix会将observable中的结果通知给所有的订阅者。
若要通过注解实现服务降级只需要使用@HystrixCommand中的fallbackMethod参数来指定具体的服务降级实现方法,如下所示:
public class UserService(){ @Autowired private RestTemplate restTemplate; @HystrixCommand(fallbackMethod = "defaultUser") public User getUserById(Long id){ return restTemplate.getForObject("http://USER-SERVICE/users/{1}",User.class,id); } public User defaultUser(){ return new User(); } }
在使用注解来定义服务降级时,我们需要将具体的Hystrix命令与fallback实现函数定义在同一个类中,并且fallbackMethod的值必须与实现fallback方法的名字相同。由于必须定义在一个类中,所以对于fallback的访问修饰符没有特定的要求,定义为private、protected、public均可。
在上面的例子中,defaultUser方法将在getUserById执行时发生错误的情况下被执行。若defaultUser方法实现的并不是一个稳定逻辑,它依然可能会发生异常,那么我们也可以为它添加@HystrixCommand注解以生成Hystrix命令,同时使用fallbackMethod来指定服务降级逻辑,比如:
public class UserService{ @Autowired private RestTemplate restTemplate; @HystrixCommand(fallbackMethod = "defaultUser") public User getUserById(Long id){ return restTemplate.getForObject("http://USER-SERVICE/users/{1}",User.class,id); } @HystrixCommand(fallback = "defaultUserSec") public User defaultUser(){ //此处可能是另外一个网络请求来获取,所以也有可能失败 return new User("First Fallback"); } public User defaultUserSec(){ return new User("Second Fallback"); } }
在实际使用时,我们需要为大多数执行过程中可能会失败的Hystrix命令实现服务降级逻辑,但是也有一些情况可以不去实现降级逻辑,如下所示。
1. 执行写操作的命令:当Hysrix命令是用来执行写操作而不是返回一些信息的时候,通常情况下这类操作的返回类型是void或是为空的Observable,实现服务降级的意义不是很大。当写入操作失败的时候,我们通常只需要通知调用者即可。
2. 执行批处理或离线计算的命令:当Hystrix命令是用来执行批处理程序生成一份报告或是进行任何类型的离线计算时,那么通常这些操作只需要将错误传播给调用者,然后让调用者稍后重试而不是发送给调用者一个静默的降级处理响应。
不论Hystrix命令是否实现了服务降级,命令状态和断路器状态都会更新,并且我们可以由此了解到命令执行的失败情况。
异常处理
异常传播
在HystrixCommand实现的run()方法中抛出异常时,除了HystrixBadRequest-Exception之外,其他异常均会被Hystrix认为命令执行失败并触发服务降级的处理逻辑,所以当需要在命令执行中抛出不触发服务降级的异常时来使用它。
而在使用注册配置实现Hystrix命令时,它还支持忽略指定异常类型功能,只需要通过设置@HysrixCommand注解的ignoreExceptions参数,比如:
@HystrixCommand(ignoreException=(BadRequestException.class)) public User getUserById(Long Id){ return restTemplate.getForObject("http://USER-SERVICE/users/{1}",User.class,id); }
如上面代码的定义,当getUserById方法抛出了类型为BadRequestException的异常时,Hystrix会将它包装在HystrixBadRequestException中抛出,这样就不会触发后续的fallback逻辑。
异常获取
当Hystrix命令因为异常(除了HystrixBadRequestException的异常)进入服务降级逻辑之后,往往需要对不同异常做针对性的处理,那么我们如何来获取当前抛出的异常呢?
在以传统继承方式实现的Hystrix命令中,我们可以用getFallback()方法通过Throwable getExecutionException()方法来获取具体的异常,通过判断来进入不同的处理逻辑。
除了传统的实现方式之外,注解配置方式也同样可以实现异常的获取。它的实现也非常简单,只需要在fallback实现方法的参数中增加Throwable e对象的定义,这样在方法内部就可以获取触发服务降级的具体异常内容了,比如:
@HystrixCommand(fallbackMethod = "fallback1") User getUserById(String id){ throw new RuntimeException("getUserById command failed"); } User fallback1(String id ,Throwable e){ assert "getUserById command failed".equals(e.getMessage()); }
命令名称、分组以及线程池划分
以继承方式实现的Hystrix命令使用类名作为默认的命令名称,我们也可以在构造函数中通过Setter静态类来设置,比如:
public UserCommand(){ super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("GroupName")) .andCommandKey(HystrixCommandKey.Factory.asKey("CommandName"));); }
从上面Setter的使用中可以看到,我们并没有直接设置命令名称,而是先调用了withGroupKey来设置命令组名,然后才通过调用andCommandKey来设置命令名。这是因为在Setter的定义中,只有withGroupKey静态函数可以创建Setter的实例,所以GroupKey是每个Setter必须的参数,而CommandKey则是一个可选参数。
通过设置命令组,Hystrix会根据组来组织和统计命令的告警、仪表盘等信息。那么为什么一定要设置命令组呢?因为除了根据组能实现统计之外,Hystrix命令默认的线程划分也是根据命令分组来实现的。默认情况下,Hystrix会让相同组名的命令使用同一个线程池,所以我们需要再创建Hystrix命令时为其指定命令组来实现默认的线程池划分。
如果Hystrix的线程池分配仅仅依靠命令组来划分,那么它就显得不够灵活了,所以Hystrix还提供了HystrixThreadPoolKey来对线程池进行设置,通过它我们可以实现更细粒度的线程池划分,比如:
public UserCommand(){ super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("CommandGroupKey")) .andCommandKey(HystrixCommandKey.Factory.asKey("CommandKey")) .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("ThreadPoolKey"))); }
如果在没有特别指定HystrixThreadPoolKey的情况下,依然会使用命令组的方式来划分线程池。通常情况下,尽量通过HystrixThreadPoolKey的方式来指定线程池的划分,而不是通过组名的默认方式实现划分,因为多个不同的命令可能从业务逻辑上来看属于同一个组,但是往往从实现本身上需要跟其他命令进行隔离。
上面已经介绍了如何为通过继承实现的HystrixCommand设置命令名称、分组以及线程池划分,那么当我们使用@HystrixCommand注解的时候,又该如何设置呢?只需设置@HystrixCommand注解的commandKey、groupKey以及threadPoolKey属性即可,它们分别表示了命令名称、分组和线程池划分,比如我们可以像下面这样进行设置:
@HystrixCommand(commandKey="getUserById",groupKey="UserGroup",threadPoolKey="getUserByIdThread") public User getUserById(Long id){ return restTemplate.getForObject("http://USER-SERVICE/user/{1}",User.class,id); }
请求缓存
当系统用户不断增长时,每个微服务需要承受的并发压力越来越大。在分布式环境下,通常压力来自于依赖服务的调用,因为请求依赖服务的资源需要通过通信来实现,这样的依赖方式比起进程内的调用方式会引起一部分的性能损失,同时HTTP相比于其他高性能的通信协议在速度上没有任何优势,所以它有些类似对于数据库这样的外部资源进行读写操作,在高并发的情况下可能会成为系统的瓶颈。既然如此,我们很容易地可以联想到,类似数据访问的缓存保护是否也可以应用到依赖服务的调用上呢?
答案显而易见,在高并发的场景下,Hystrix中提供了请求缓存的功能,我们可以方便地开启和使用请求缓存来优化系统,达到减轻高并发时的请求线程消耗、降低请求响应时间的效果。
开启请求缓存功能
Hystrix请求缓存的使用非常简单,我们只需要在实现HystrixCommand或HystrixObservableCommand时,通过重载getCacheKey()方法来开启请求缓存,比如:
pulic class UserCommand extends HystrixCommand<User>{ private RestTemplate restTemplate; private Long id; public UserCommand(RestTemplate restTemplate , Long id){ super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("UserGroup"))); this.restTemplate=restTemplate; this.id= id; } @Override protected User run(){ return restTemplate.getForObject("http://USER-SERVICE/user/{1}",User.class,id); } @Override protected String getCacheKey(){ return String.valueOf(id); } }
在上面的例子中,我们通过在getCacheKey方法中返回的请求缓存key值(使用了传入的获取User对象的id值),就能让该请求命令具备缓存功能。此时,当不同的外部请求处理逻辑调用了同一个依赖服务时,Hystrix会根据getCacheKey方法返回的值来区分是否是重复的请求,如果他们的cacheKey相同,那么该依赖服务只会在第一个请求达到时被真实地调用一次,另外一个请求则是直接从请求缓存中返回结果,所以通过开启请求缓存可以让我们实现的Hystrix命令具备下面几项好处:
1. 减少重复的请求数,降低依赖服务的并发度。
2. 在同一用户请求的上下文中,相同依赖服务的返回数据始终保持一致。
3. 请求缓存在run()和construct()执行之前生效,所以可以有效减少不必要的线程开销。
清理失效缓存功能
使用请求缓存时,如果只是读操作,那么不需要考虑缓存内容是否正确的问题,但是如果请求命令中还有更新数据的写操作,那么缓存中的数据就需要我们在进行写操作时进行及时处理,以防止读操作的请求命令获取到了失效的数据。
在Hystrix中,我们可以通过HystrixRequestCache.clear()方法来进行缓存的清理,具体示例如下:
public class UserGetCommand extends HystrixCommand<User>{ private static final HystrixCommandKey GETTER_KEY = HystrixCommandKey.Factory.asKey("CommandKey"); private RestTemplate restTemplate; private Long id; public UserGetCommand (RestTemplate restTemplate,Long id){ super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("GetSetGet")) .andCommandKey(GETTER_KEY)); this.restTemplate=restTemplate; this.id=id; } @Override protected User run(){ retrun restTemplate.getForObject("http://USER-SERVICE/users/{1}",User.class,id); } @Override protected String getCacheKey(){ //根据id置入缓存 return String.valueOf(of); } public static void flushCache(Long id){ //刷新缓存,根据id进行清理 HystrixRequestCache.getInstance(GETTER_KEY,HystrixConcurrencyStrategyDefault.getInstance()).clear(String.valueOf(id)); } } public class UserPostCommand extends HystrixCommand<User>{ private RestTemplate restTemplate; private User user; public UserPostCommand(RestTemplate restTemplate,User user){ super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("GetSetGet"))); this.restTemplate=restTemplate; this.user=user; } @Override protected User run(){ //写操作 User r = restTemplate.postForObject("http://USER-SERVICE/users/{1}",user,User.class); //刷新缓存,清理缓存中失效的User UserGetCommand.flushCache(user.getId()); return r; } }
该示例中主要有两个请求命令:UserCommand用于根据id获取User对象、而UserPostCommand用于获取更新User对象。当我们对UserGetCommand命令实现了请求缓存之后,那么势必需要为UserPostCommand命令实现缓存的清理,以保证User被更新之后,Hystrix请求缓存中相同缓存Key的结果被移除,这样在下一次获取User的时候不会从缓存中获取到未更新的结果。
我们可以看到,在上面UserGetCommand的实现中,增加了一个静态方法flushCache,该方法通过HystrixRequestCache.getInstance()方法从默认的Hystrix并发策略中根据GETTER_KEY获取到该命令的请求缓存对象HystrixRequestCache的实例,然后再调用该请求缓存对象实例的clear方法,对Key为更新User的id值的缓存内容进行清理。而在UserPostCommand的实现中,在run方法调用依赖服务之后,增加了对UserGetCommand中静态方法flushCache的调用,以实现对失效缓存的清理。
工作原理
通过上面的入门例子,我们已经能够体会到Hystrix中实现请求缓存是非常方便的,那么它是如何做到的呢?我们不妨通过分析其源码来了解一下它的实现原理,对其有一个深入的理解,有助于知道我们正确使用和配置请求缓存。由于getCacheKey方法在AbstractCommand抽象命令类中实现,所以我们可以先从这个抽象命令的实现中看起。
从下面AbstractCommand的源码片段中,我们可以看到,getCacheKey方法默认返回的是null,并且从isRequestCacheingEnabled方法的实现逻辑中我们还可以知道,需要重写getCacheKey方法,让它返回一个非null值,缓存功能才开启;同时请求命令的缓存开启属性也需要设置为true才能开启(该属性默认为true,所以通常用该属性来控制请求缓存功能的强制关闭)。
abstract class AbstractCommand<R> implements Hystrixinvokableinfo<R>, HystrixObservable<R> { ... protected final HystrixRequestCache requestCache; protected String getCacheKey() { return null; } protected boolean isRequestCachingEnabled() { return properties.requestCacheEnabled() .get() && getCacheKey() != null; } ... public Observable<R> toObservable() { ... //尝试从缓存中获取结果 final boolean requestCacheEnabled = isRequestCachingEnabled(); final String cacheKey = getCacheKey(); final AbstractCommand<R> _cmd = this; if (requestCacheEnabled) { HystrixCommandResponseFromCache<R> fromCache = (HystrixCommandResponseFromCache<R>) requestCache.get(cacheKey); if (fromCache != null) { isResponseFromCache = true; return handleRequestCacheHitAndEmitValues(fromCache, _cmd); } } ... Observable<R> hystrixObservable = Observable.defer(applyHystrixSemantics).map(wrapWithAllOnNextHooks); Observable<R> afterCache; //加入缓存 if (requestCacheEnabled && cacheKey != null) { HystrixCachedObservable<R> toCache = HystrixCachedObservable.from(hystrixObservable, this); HystrixCommandResponseFromCache<R> fromCache = (HystrixCommandResponseFromCache<R>) requestCache.putifAbsent(cacheKey, toCache); if (fromCache ! = null) { toCache.unsubscribe(); isResponseFromCache = true; return handleRequestCacheHitAndEmitValues(fromCache, cmd); } else { afterCache = toCache.toObservable(); } else { afterCache = hystrixObservable; } } ... }
另外,从命令异步执行的核心方法toObservable()中,我们可以看到与缓存相关的主要执行步骤,它分为两部分内容:1. 尝试获取请求缓存以及 2. 将请求结果加入缓存。
1. 尝试获取请求缓存:Hystrix命令在执行前会根据之前提到的isRequest-CachingEnabled方法来判断当前命令是否启用了请求缓存。如果开启了请求缓存并且重写了getCacheKey方法,并返回一个非null的缓存Key值,那么就使用getCacheKey返回的Key值去调用HystrixRequestCache中的get(String cacheKey)来获取缓存的HystrixCacheObservable对象。
2. 将请求结果加入缓存:在执行命令缓存操作之前,我们可以看到已经获得了一个延迟执行的命令结果对象hystrixObservable。接下来与尝试获取请求缓存操作一样,需要先判断当前命令是否开启了请求缓存功能,如果开启了请求缓存并且getCacheKey返回具体的Key值,就将hystrixObserable对象包装成请求缓存结果HystrixCacheObservable的实例对象toCache,然后将其放入当前命令的缓存中。从调用的方法putIfAbsent中,我们大致可以猜到在请求缓存对象HystrixRequestCache中维护了一个线程安全的Map来保存请求缓存的响应,所以在调用putIfAbsent将包装的请求缓存放入缓存对象后,对其返回结果fromCache进行判断,如果其不为null,说明当前缓存Key的请求命令缓存命中,直接对toCache执行取消订阅操作(即,不再发起真实请求),同时调用缓存命令的处理方法handleRequestCacheHitAndEmitValues来执行缓存命中的结果获取。如果返回的fromCache为null说明缓存没有命中,则将当前结果toCache缓存起来,并将其转换成Observable返回给调用者使用。
使用注解实现请求缓存
Hystrix的请求缓存除了可以通过上面传统的方式实现之外,还可以通过注解的方式进行配置实现。注解配置的定义实现同JSR 107的定义非常相似,但由于Hystrix不需要独立外置的缓存系统来支持,所以没有JSR 107(Java Specification Requests缓存规范)的定义那么复杂,它只提供了三个专用于请求缓存的注解。
【JSR 107是Java缓存API的定义,也被称为JCache。它定义了一些列开发人员使用的标准化Java缓存API和服务提供商使用的标准SPI】
下面我们几个方面的实例来看看这几个注解的具体使用方法。
1. 设置请求缓存:通过注解为请求命令开启缓存功能非常简单,如下例所示,我们只需添加@CacheRequest注解即可。当该依赖服务被调用并返回User对象时,由于该方法被@CacheResult注解修改,所以Hystrix会将该结果置入请求缓存中,而它的缓存Key值会使用所有的参数,也就是这里Long类型的id值。
@CacheResult @HystrixCommand public User getUserById(Long id){ return restTemplate.getForObject("http://USER-SERVICE/users/{1}",User.class,id); }
2. 定义缓存key:当使用注解来定义请求缓存时,若要为请求命令指定具体的缓存Key生成规则,我们可以使用@CacheResult和@CacheRemove注解的cacheKeyMethod方法来指定具体的生成函数;也可以通过使用@CacheKey注解在方法参数中指定用于组装缓存Key的元素。
使用cacheKeyMehthod方法的示例如下,它通过在请求命令的同一个类中定义一个专门生成Key的方法,并用@CacheResult注解的cacheKeyMethod方法来指定它即可。它的配置方式类似于@HystrixCommand服务降级fallbackMethod的使用。
@CacheResult(cacheKeyMethod="getUserByCacheKey") @HystrixCommand public User getUserById(Long id){ return restTemplate.getForObject("http://USER-SERVICE/users/{1}",User.class,id); }
@CacheKey注解除了可以指定方法参数作为缓存Key之外,它还允许访问参数对象的内部属性作为缓存Key。比如下面的例子,它指定了User对象的id属性作为缓存Key。
@CacheResult @HystrixCommand public User getUserById(@CacheKey("id") User user){ return restTemplate.getForObject("http://USER-SERVICE/users/{1}",User.class,id); } @CacheRemove(commandKey="getUserById") @HystrixCommand public void update(@CacheKey("id") User user){ return restTemplate.postForObject("http://USER-SERVICE/users",user,User.class); }
需要注意的是,@CacheRemove注解的commandKey属性是必须要指定的,它用来指明需要请求缓存的请求命令,因为只有通过该属性的配置,Hystrix才能找到正确的请求命令缓存位置。
请求合并
微服务架构中的依赖通常通过远程调用实现,而远程调用中最常见的问题就是通信消耗与连接数占用。在高并发的情况下,因通信次数的增加,总的通信时间消耗将会变得不那么理想。同时,因为依赖服务的线程池资源有限,将出现排队等待与响应延迟的情况。为了优化这两个问题,HystrixCollapser来实现请求的合并,以减少通信消耗和线程的占用。
HystrixCollapser实现了在HystrixCommand之前放置一个合并处理器,将处于一个很短的时间窗(默认10毫秒)内对统一依赖服务的多个请求进行整合并以批量方式发起请求的功能(服务提供方也需要提供相应的批量实现接口)。通过hystrixCollapser的封装,开发者不需要关注线程合并的细节过程,只需关注批量化服务和处理。下面我们从HystrixCollaper的使用实例中对其合并请求的过程一探究竟。
public abstract class HystrixCollapser<BatchReturnType,ResponseType,RequestArgumentType> implements HystrixExecutable<ResponseType>,HystrixObservable<ResponseType>{ ... public abstract RequestArgumentType getRequestArgument(); protected abstract HystrixCommand<BatchReturnType> createCommand(Colletion<CollapsedRequest<ResponseType,RequestArgumentType>>requests); protected abstract void mapResponseToRequests(BatchReturnType batchResponse,Colletion<CollapsedRequest<ResponseType,RequestArgumentType>> requests); ... }
从HystrixCollapser抽象类的定义中可以看到,它指定了三个不同的类型。
1. BatchReturnType:合并后批量请求的返回类型。
2. ResponseType:单个请求返回的类型。
3. RequestArgumentType:请求参数类型。
而对于这三个类型的使用可以在它的三个抽象方法中看到。
1. RequestArgumentType getRequestArgument():该函数用来定义获取请求参数的方法。
2. HystrixCommand<BatchReturnType> createCommand(Colletion<CollapsedRequest<ResponseType,RequestArgumentType>> requests):合并请求产生批量命令的具体实现方法。
3. mapResponseToRequests(BatchReturnType batchResponse,Colletion <CollapsedRequest<ResponseType,RequestArgumentType>>requests):批量命令结果返回后的处理,这里需要实现将批量结果拆分并传递给合并前的各个原子请求命令的逻辑。
接下来,我们通过一个简单的实例来直观理解实现请求合并的过程。
假设当前微服务USER-SERVICE提供了两个获取User的接口。
1. /users/{id}:根据id返回User对象的GET请求接口。
2. /users?ids={ids}:根据ids返回User对象列表的GET请求接口,其中ids为以逗号分隔的id集合。
而在服务消费端,已经为这两个远程接口通过RestTemplate实现了简单的调用,具体如下所示:
@Service public class UserServiceImpl implements UserService{ @Autowired private RestTemplate restTemplate; @Override public User find(Long id){ return restTemplate.getForObjecet("http://USER-SERVICE/users/{1}",User.class,id); }
@Override public List<User> findAll(List<Long> ids){ return restTemplate.getForObject("http://USER-SERVICE/users?ids={1}",List.class,StringUtils.join(ids,"")); } }
接着,我们实现将短时间内多个获取单一User对象的请求命令进行合并。
1. 第一步,为请求合并的实现准备一个批量请求命令的实现,具体如下:
public class UserBatchCommand extends HystrixComand<List<User>>{ UserService userService; List<Long> userIds; pulic UserBatchCommand(UserService userService,List<Long> userIds){ super(Setter.withGroupKey(asKey("userServiceCommand"))); this.userIds = userIds; this.userService = userService; } @Override protected List<User> run() throws Exception{ return userService.findAll(userIds); } }
批量请求命令实际上就是一个简单的HystrixCommand实现,从上面的实现中可以看到它通过调用userService.findAll方法来访问/users?ids={ids}接口以返回User的列表结果。
2. 第二步,通过继承HystrixCollapser实现请求合并器:
public class UserCollapseCommand extends HystrixCollapser<List<User>, User, Long> { private UserService userService; private Long userid; public UserCollapseCommand(UserService userService, Long userid) { super(Setter.withCollapserKey(HystrixCollapserKey.Factory.asKey ("userCollapseCommand"))
.andCollapserPropertiesDefaults( HystrixCollapserProperties.Setter() .withTimerDelayinMilliseconds(lOO))); this.userService = userService; this.userid = userid; } @Override public Long getRequestArgument () { return userid; } @Override protected HystrixComrnand<List<User>> createComrnand(Collection<CollapsedRequest<User, Long>> collapsedRequests) { List<Long> userids = new ArrayList<>(collapsedRequests.size()); userids.addAll(collapsedRequests.stream().map(CollapsedRequest::getArgument).calleet(Collectors.toList())); return new UserBatchCommand(userService, userids); } @Override protected void mapResponseToRequests(List<User> batchResponse, Collection<CollapsedRequest<User, Long>> collapsedRequests) { int count = O; for (CollapsedRequest<User, Long> collapsedRequest : collapsedRequests) { User user = batchResponse.get(count++); collapsedRequest.setResponse(user); } }
}
在上面的构造函数中,我们为请求合并器设置了时间延迟属性,合并器会在该时间窗内收集获取单个User的请求并在时间窗结束时进行合并组装成单个批量请求。getRequestArgument方法返回给定的单个请求参数userId,而createCommand和mapResponseToRequests是请求合并器的两个核心。
1. createCommand:该方法的collapsedRequests参数中保存了延迟时间窗中收集到的所有获取单个User的请求。通过获取这些请求的参数来组织上面我们准备的批量请求命令UserBatchCommand实例。
2. mapResponseToRequests:在批量请求命令UserBatchCommand实例被触发执行完成之后,该方法开始执行,其中batchResponse参数保存了createCommand中组织的批量请求命令的返回结果,而collapsedRequests对象,为collapsedRequests中每个合并前的单个请求设置返回结果,以此完成批量结果到单个请求结果的转换。
下图展示了在未使用HystrixCollapser 请求合并器之前的线程使用情况。可以看到,当服务消费者同时对USER-SERVICE的/users/{id}接口发起了5个请求时,会向该依赖服务的独立线程池中申请5个线程来完成各自的请求操作。
而在使用了HystrixCollapser请求合并器之后,相同情况下的线程占用如下图所示。由于同一时间发生的5个请求合并器的一个时间窗内,这些发向/users/{id}接口的请求被请求合并器拦截下来,并在合并器中进行组合,然后将这些请求合并成一个请求发向USER-SERVICE的批量接口/users?ids={ids}。在获取到批量请求结果之后,通过请求合并器再将批量结果拆分并分配给每个被合并的请求。从图中我们可以看到,通过使用请求合并器有效减少了对线程池中资源的占用。所以在资源有效并且短时间内会产生高并发请求的时候,为避免连接不够用而引起的延迟可以考虑使用请求合并器的方式来处理和优化。
使用注解实现请求合并器
在快速入门的例子中,我们使用@HystrixCommand注解优雅地实现了HystrixCommand的定义,那么对于请求合并器是否也可以通过注解来定义呢?答案是肯定的!
以上面实现的请求合并器为例,还可以通知如下方式实现:
@Service public class UserService{ @Autowired private RestTemplate restTemplate; @HystrixCollapser (batchMethod="findAll",collapserProperties={ @HystrixProperty(name="timerDelayInMilliseconds",value="100") }) public User find(Long id){ return null; } public List<User> findAll (List<Long> ids){ return restTemplate.getForObject("http://USER-SERVICE/users?ids={1}",List.class,StringUtils.join(ids,",")); } }
我们之前已经介绍过@HystrixCommand了,可以看到,这里通过它定义了两个Hystrix命令,一个用于请求/users/{id}接口,一个用于请求/users?ids={ids}接口。而在请求/users/{id}接口的方法上通过@HystrixCollapser注解为其创建了合并请求器,通过batchMethod属性指定了批量请求的实现方法为findAll方法(即请求/users?ids={ids}接口的命令),同时通过collapserProperties属性为合并请求器设置了相关属性,这里使用@HysrixProperty(name="timerDelayInMilliseconds",value="100")将合并时间窗设置为100毫秒。这样通过@HystrixCollapser注解简单而又优雅地实现了在/users/{id}依赖服务之前设置了一个批量请求合并器。
请求合并的额外开销
虽然通过请求合并可以减少请求的数量以缓解依赖服务线程池的资源,但是在使用的时候也需要注意它带来的额外开销:用于请求合并的延迟时间窗会使得依赖服务的请求延迟增高。比如,某个请求不通过请求合并器访问的平均耗时为5ms,请求合并的延迟时间窗为10ms(默认值),那么当该请求设置了请求合并器之后,最坏情况下(在延迟时间窗结束时才发起请求)该请求需要15ms才能完成。
由于请求合并器的延迟时间窗会带来额外开销,所以我们是否使用请求合并器需要根据依赖服务调用的实际情况来选择,主要考虑下面两个方面。
1. 请求命令本身的延迟。如果依赖服务的请求命令本身是一个高延迟的命令,那么可以使用请求合并器,因为延迟时间窗的时间消耗显得微不足道了。
2. 延迟时间窗内的并发量。如果一个事件窗内只有1~2个请求,那么这样的依赖服务不适合使用请求合并器。这种情况不但不能提升系统性能,反而会成为系统瓶颈,因为每个请求都需要多消耗一个时间窗才能响应。相反,如果一个时间窗内具有很高的并发量,并且服务提供方也实现了批量处理接口,那么使用请求合并器可以有效减少网络连接数量并极大提升系统吞吐量,此时延迟时间窗所增加的消耗就可以忽略不计了。
属性详解
在之前介绍Hystrix的使用方法时,已经涉及一些Hystrix属性的配置,我们可以根据事先HystrixCommand的不同方式将配置方法分为如下两类。
1. 当通过继承的方式实现时,可使用Setter对象来对请求命令的属性进行设置,比如下面的例子:
public HystrixCommandInstance(int id){ super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter().withExecutionTimeoutInMilliseconds(500))); this.id=id; }
2. 当通过注解的方法实现时,只需使用@HystrixCommand中的command-Properties属性来设置,比如:
@HystrixCommand(commandKey="helloKey") commandProperties={ @HystrixProperty(name="exectution.isolation.thread.timeoutInMilliseconds",value= "5000") } ) public User getUserById(Long id){ return restTemplate.getForObject("http://USERR-SERVICE/users/{1}",User.class,id); }
实际上,Hystrix为我们提供的配置内容和配置方式远不止上面这些,它提供了非常丰富和灵活的配置方法,下面我们将详细介绍HystrixPropertiesStrategy实现的各项配置属性。在具体说明这些属性之前,我们需要了解一下这些属性都存在下面4个不同优先级别的配置(优先级由低到高)。
1. 全局默认值:如果没有设置下面三个级别的属性,那么这个属性就是默认值。由于该属性通过代码定义,所以对于这个级别,我们主要关注它在代码中定义的默认值即可。
2. 全局配置属性:通过在配置文件中定义全局属性值,在应用启动时或在与Spring Cloud Config和Spring Cloud Bus实现的动态刷新配置功能配合下,可以实现对"全局默认值"的覆盖以及在运行期对“全局默认值”的动态调整。
3. 实例默认值:通过代码为实例定义的默认值。通过代码的方式为实例设置属性值来覆盖默认的全局配置。
4. 实例配置属性:通过配置文件来为指定的实例进行属性配置,以覆盖前面的三个默认值。它也可用Spring Cloud Config和Spring Cloud Bus实现的动态刷新配置功能实现对具体实例配置的动态调整。
通过理解Hystrix4个级别的属性配置,对设置Hystrix的默认值以及在线上如何根据实际情况去调整配置非常有帮助,下面我们来具体看看它有哪些具体的属性配置。
Command属性
Command属性主要用来控制HystrixCommand命令的行为。
它主要有下面5种不同类型的属性配置。
execution配置
execution配置控制的是HystrixCommand.run()的执行。
1. execution.isolation.strategy:该属性用来设置HystrixCommand.run()执行的隔离策略,它有如下两个选项。
a. THREAD:通过线程池隔离的策略。它在独立的线程上执行,并且它的并发限制受线程池中线程数量的限制。
b. SEMAPHORE:通过信号隔离的策略。它在调用线程上执行,并且它的并发限制受信号量计数的限制。
2. execution.isolation.thread.timeoutInMilliseconds:该属性用来配置HystrixCommand执行的超时时间,单位为毫秒。当HystrixCommand执行时间超过该配置值之后,Hystrix会将该执行命令标记为TIMEOUT并进入服务降级处理逻辑。
3. execution.timeout.enabled:该属性用来配置HystrixCommand.run()的执行是否启用超时时间。默认为ture,如果设置为false,那么execution.isolation.thread.timeoutInMilliseconds属性的配置将不再起作用。
4. execution.isolation.thread.interruptOnTimeOut:该属性用来配置当HystrixCommand.run()执行超时的时候是否要将它中断。
5. execution.isolation.thread.interruptOnCancel:该属性用来配置当HystrixCommand.run()执行被取消的时候是否要将它中断。
6.execution.isolation.semaphore.maxConcurrentRequests:当HystrixCommand的隔离策略使用信号量的时候,该属性用来配置信号量的大小(并发请求数)。当最大并发请求数达到设置值时,后续的请求将会被拒绝。
fallback配置
下面这些属性用来控制HystrixCommand.getFallback()的执行。这些属性同时适用于线程池的信号量的隔离策略。
1. fallback.isolation.semaphore.maxConcurrentRequests:该属性用来设置从调用线程中允许HystrixCommand.getFallback()方法执行的最大并发请求数。当达到最大并发请求数时,后续的请求将会被拒绝并抛出异常(因为它已经没有后续的fallback可以被调用了)。
2. fallback.enabled:该属性用来设置服务降级策略是否启用,如果设置为false,那么当请求失败或者拒绝发生时,将不会调用HystrixCommand.getFallback()来执行服务降级逻辑。
circuitBreaker配置
下面这些是断路器的属性配置,用来控制HystrixCircuitBreaker的行为。
1. circuitBreaker.enabled:该属性用来确定当服务请求命令失败时,是否使用断路器来跟踪器健康指标和熔断请求。
2. circuitBreaker.requestVolumeThreshold:该属性用来设置在滚动时间窗中,断路器熔断的最小请求数。例如,默认该值为20的时候,如果滚动时间窗(默认10秒)内仅收到了19个请求,即使这19个请求都失败了,断路器也不会打开。
3. circuitBreaker.sleepWindowInMilliseconds:该属性用来设置当断路器打开之后的休眠时间窗。休眠时间窗结束之后,会将断路器置为“半开”状态,尝试熔断的请求命令,如果依然失败就将断路器继续设置为“打开”状态,如果成功就设置为“关闭”状态。
4. circuitBreaker.errorThresholdPercentage:该属性用来设置断路器打开的错误百分比条件。例如,默认值为5000的情况下,标识在滚动时间窗内,在请求数量超过circuitBreaker.requestVolumeThreshold阈值的前提下,如果错误请求数的百分比超过50,就把断路器设置为“打开”状态,否则就设置为“关闭”状态。
5. circuitBreaker.forceOpen:如果将该属性设置为true,断路器将强制进入“打开状态”,它会拒绝所有请求。该属性优于circuitBreaker.forceClosed属性。
6. circuitBreaker.forceClosed:如果将该属性值设置为true,断路器将强制进入“关闭”状态,它会接收所有请求。如果circuitBreaker.forceOpen属性为true,该属性不会生效。
metrics配置
下面的属性均与HystrixCommand和HystrixObservableCommand执行中捕获的指标信息有关。
1. metrics.rollingStats.timeInMilliseconds(应用初始化时生效,且必须能被numBucket整除):该属性用来设置滚动是时间窗的长度,单位为毫秒。该时间用于断路器判断健康度时需要收集信息的持续时间。断路器在收集指标信息的时候会根据设置的时间窗长度拆分为多个“桶”来累计各度量值,每个“桶”记录一段时间内的采集指标。例如,当采用默认值10000毫秒时,断路器默认将其拆分成10个桶(桶的数量也可通过metrics.rolling-Stats.mumBuckets参数设置),每个桶记录1000毫秒内的指标信息。
2. metrics.rollingStats.numBuckets:该属性用来设置滚动时间窗统计指标信息时划分“桶”的数量。
3. metrics.rollingPercentile.enabled:该属性用来设置对命令执行的延迟是否使用百分位数来跟踪和计算。如果设置为false,那么所有的概要统计都将返回-1。
4. metrics.rollingPercentile.timeInMilliseconds(仅初始化时生效):该属性用来设置百分位统计的滚动窗口的持续时间,单位为毫秒。
5. metrics.rollingPercentile.numBuckets(仅初始化时生效):该属性用来设置百分位统计滚动窗口中使用“桶”的数量。
6. metrics.rollingPercentile.bucketSize:该属性用来设置在执行过程中每个“桶”中保留的最大执行次数。如果在滚动时间窗内发生超过该设定值的执行次数,就从最初的位置开始重写。例如,将该值设置为100,滚动窗口为10秒,若在10秒内一个“桶”中发生了500次执行,那么该“桶”中只保留最后的100次执行的统计。另外,增加该值的大小将会增加内存量的消耗,并增加排序百分位数所需的计算时间。
7. metrics.healthSnapshot.intervalInMilliseconds:该属性用来设置采集影响断路器状态的健康快照(请求的成功、错误的百分比)的间隔等待时间。
requestContext配置
下面这些属性涉及HystrixCommand使用的HystrixRequestContext的设置。
1. requestCache.enabled:此属性用来配置是否开启请求缓存。
2. requestLog.enabled:该属性用来设置HystrixCommand的执行和事件是否打印日志到HystrixRequestLog中。
collapser配置
该属性除了代码中用set和配置文件配置之外,也可使用注解进行配置。可使用@HystrixCollapser中的collapserProperties属性来设置,比如:
@HystrixCollapser(batchMethod="batch",collapserProperties={ @HystrixProperty(name="timerDelayInMilliseconds",value="20") })
下面这些属性用来控制命令合并相关的行为。
1. maxRequestInBatch:该参数用来设置一次请求合并批处理中允许的最大请求数。
2. timerDelayInMilliseconds:该参数用来设置批处理过程中每个命令延迟的时间,单位为毫秒。
3. requestCache.enabled:该参数用来设置批处理过程中是否开启请求缓存。
threadPool配置
该属性除了在代码中用set和配置文件配置之外,还可使用注解进行配置。可使用@HystrixCommand中的threadPoolProperties属性来设置,比如:
@HytrixCommand(fallbackMethod="helloFallback",commandKey="helloKey",threadPoolProperties={
threadPoolProperties={
@HystrixProperty(name="coreSize",value="20")
}
)
下面这些属性用来控制Hystrix命令所属线程池的配置。
1. coreSize:改参数用来设置执行命令线程池的核心线程数,该值也就是命令执行的最大并发量。
2. maxQueueSize(仅初始化时生效):该参数用来设置线程池的最大队列大小。当设置为-1时,线程池将使用SynchronousQueue实现的队列,否则将使用LinkedBlockingQueue实现的队列。
3. queueSizeRejectionThreshold:该参数用来为队列设置拒绝阈值。通过该参数,即时队列没有达到最大值也能拒绝请求。该参数主要是对LinkedBlocking-Queue队列的补充,因为LinkedBlockingQueue队列不能动态修改它的对象大小,而通过该属性就可以调整拒绝请求的队列大小。
4. metrics.rollingStats.timeInMilliseconds:该参数用来设置滚动时间窗的长度,单位为毫秒。该滚动时间窗的长度用于线程池的指标度量,它会被分成多个“桶”来统计指标。
5. metrics.rollingStats.numBuckets:该参数用来设置时间窗被换分成“桶”的数量。
Hystrix仪表盘
在断路器原理的介绍中,我们多次提到关于请求命令的度量指标的判断。这些度量指标都是HystrixCommand和HystrixObservableCommand实例在执行过程中记录的重要信息,它们除了在Hystrix断路器实现中使用之外,对于系统运维也有非常大的帮助。这些指标信息会以“滚动时间窗”与“桶”结合的方式进行汇总,并在内存中驻留一段时间,以供内部或外部进行查询使用,Hystrix仪表盘就是这些指标内容的消费者之一。
通过之前的内容,我们已经体验到了Spring Cloud 对Hystrix的优雅整合。除此之外,Spring Cloud还完美地整合了它的仪表盘组件Hystrix Dashboard,它主要用来实时监控Hystrix的各项指标信息。通过Hystrix Dashboard反馈的实时信息,可以帮助我们快速发现系统中存在的问题,从而及时地采用应对措施。
本节中我们将在Hystrix入门例子的基础上,构建一个HystrixDashboard来对RIBBON-CONSUMER实现监控,完成后的架构如下图所示。
在Spring Cloud 中构建一个Hystrix DashBoard非常简单,只需要下面4步:
1. 创建一个标准的Spring Boot工程,命名为hystrix-dashboard。
2. 编辑pom.xml,具体依赖内容如下所示:
<parent> <groupid>org.springframework.cloud</groupid> <artifactid>spring-cloud-starter-parent</artifactid> <version>Brixton.SRS</version> <relativePath /> <!-- lookup parent from repository --> </parent>
<dependencies> <dependency> <groupid>org.springframework.cloud</groupid> <artifactid>spring-cloud-starter-hystrix</artifactid> </dependency> <dependency> <groupid>org.springframework.cloud</groupid> <artifactid>spring-cloud-starter-hystrix-dashboard</artifactid> </dependency> <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-actuator</artifactid> </dependency> </dependencies>
3. 为应用主类加上@EnableHystrixDashboard,启用Hystrix Dashboard功能。
4. 根据实际情况修改application.properties配置文件,比如选择一个未被占用的端口等,此步不是必须的。
spring.application.name=hystrix-dashboard
server.port=2001
到这里我们已经完成了基本配置,接下来可以启动该应用,并访问http://localhost:2001/hystrix。 可以看到如下页面:
这里Hystrix Dashboard的监控首页,该页面中没有具体的监控信息。从页面的文字内容中我们可以知道,Hystrix Dashboard共支持三种不同的监控方式,如下所示。
1. 默认的集群监控:通过URL http://turbine-hostname:port/turbine.stream开启,实现对默认集群的监控。
2. 指定的集群监控:通过URL http://turbine-hostname:port/turbine.stream?cluster=[clusterName]开启,实现对clusterName集群的监控。
3. 单体应用的监控:通过URL http://hystrix-app: port/hystrix.stream开启,实现对具体某个服务实例的监控。
前两者都是对集群的监控,需要整合Turbine 才能实现,这部分我们将在下一节中做详细介绍。在本节中,我们主要实现对单个服务实例的监控。
既然Hystrix Dashboard监控单实例节点需要通过访问实例的/hystrix.stream接口来实现,我们自然需要为服务实例添加这个端点,而添加该功能的步骤也同样简单,只需要下面两步。
1. 服务实例pom.xml中的dependencies节点中新增spring-boot-starter-actuator监控模块以开启监控相关的端点,并确保已经引入断路器的依赖spring-cloud-starter-hystrix:
<dependencies>
... <dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
2. 确保在服务实例的主类中已经使用@EnableCiruitBreaker注解,开启了断路器功能。
在为RIBBON-CONSUMER加入上面的配置之后,重启它的实例,此时我们可以在控制台看到打印了大量的监控端点,其中/hystrix.steam 就是用于Hystrix Dashboard来展示监控信息的接口。
到这里已经完成了所有的配置,在Hystrix Dashboard的首页输入http://localhost:9000/hystrix.stream,可以看到已启动对RIBBON-CONSUMER的监控,单机Monitor Stream按钮,可以看到如下页面:
在对该页面进行介绍之前,先看看在首页中我们还没有介绍的另外两个参数。
1. Delay:该参数用来控制服务上轮询监控信息的延迟时间,默认为2000毫秒,可以通过配置该属性来降低客户端的网络和CPU消耗。
2. Title:该参数对应了上图头部标题Hystrix Stream 之后的内容,默认会使用具体监控实例的URL,可以通过配置该信息来展示更合适的标题。
回到监控页面,我们来详细说说其中各元素的具体含义。
3. 可以在监控信息的左上部找到两个重要的图形信息:一个实心圆和一条曲线。
a. 实心圆:其中有两种含义。通过颜色的变化代表了实例的健康程度,如下图所示,它的健康度从绿色、黄色、橙色、红色递减。该实心圆除了颜色的变化之外,它的大小也会根据实例的请求流量发生变化,流量越大该实心圆就越大。所以通过该实心圆的展示,我们可以在大量的实例中发现故障实例和高压力实例。
b. 曲线:用来记录2分钟内流量的相对变化,可以通过它来观察流量的上升和下降趋势。
4. 其他一些数量指标如下图所示。
通过本节内容我们已经能够使用Hystrix Dashboard来对单个实例做信息监控了,但是在分布式系统中,往往有非常多的实例需要去维护和监控。到目前为止,我们能做到的就是通过开启多个窗口来监控多个实例,很显然这样的做法并不合理。在下一节中,我们将介绍利用Turbine 和Hystrix Dashboard配合实现对集群的监控。
Turbine集群监控
在上一节介绍Hystrix Dashboard的首页时,我们提到过除了可以开启单个实例的监控页面之外,还有一个监控端点/turbine.stream是对集群使用的。从端点的命名中,可以猜测到这里我们将引入Turbine,通过它来汇集监控信息,并将聚合后的信息提供给Hystrix Dashboard来集中展示和监控。
构建监控聚合服务
下面我们将在上一节内容的基础上做一些扩展,通过引入Turbine来聚合RIBBON-CONSUMER服务的监控信息,并输出给Hystrix Dashboard 来进行展示,最后完成如下图所示的结构。
具体实现步骤如下:
1. 创建一个标准的Spring Boot工程,命名turbine。
2. 编辑pom.xml,具体依赖内容如下所示。
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>l.8</java.version>
</properties> <parent>
<groupid>org.springframework.cloud</groupid>
<artifactid>spring -cloud -starter -parent</artifactid>
<version>Brixton.SR5</version>
<relativePath /> < ! - - lookup parent from repository -->
</parent> <dependencies>
<dependency>
<groupid>org.springframework.cloud</groupid>
<artifactid>spring-cloud-starter-turbine</artifactid>
</dependency>
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
3. 创建应用主类TurbineApplication,并使用@EnableTurbine注解开启Turbine。
@EnableTurbine @EnableDiscoveryClient @SpringBootApplication public class TurbineApplation{ public static void main(String[] args){
SpringApplication.run(TurbineApplication.class,args);
} }
在application.properties中加入Eureka和Turbine的相关配置,具体如下:
spring.application.name=turbine
server.port=8989
management.port=8990
eureka.client.serviceUrl.defaultZone=http://localhost:llll/eureka/
turbine. app - config=RIBBON - CONSUMER
turbine. cluster -name -expression="default"
turbine. combine - host -port=true
其中,turbine.app-config参数指定了需要收集监控信息的服务名;turbine.cluster-name-expression参数指定了集群名称为default,当服务数量非常多的时候,可以启动多个Turbine 服务来构建不同的聚合集群,而该参数可以用来区分这些不同的聚合集群,同时该参数可以在Hystrix 仪表盘中来定位不同的聚合集群,只需在Hystrix Stream的URL中通过cluster参数来指定;turbine.combine-host-port参数来设置为true,可以让同一主机上的服务通过主机名与端口号的组合来进行区分,默认情况下会以host来区分不同的服务,这会使得在本地调试的时候,本机上的不同服务聚合一个服务来统计。
在完成了上面的内容构建之后,我们来体验一下Turbine 对集群的监控能力。分别启动eureka-server、HELLO-SERVICE、RIBBON-CONSUMER、Turbine以及Hystrix Dashboard。访问Hystrix Dashboard,并开启对http://localhost:8989/turbine.stream的监控,我们可以看到如下页面:
从图中可以看到,虽然我们如之前的架构那样启动了两个RIBBON-CONSUMER,但是在监控页中依然只是展示了一个监控图。不过仔细的读者可能已经发现,图中集群报告区域中的Hosts 属性与之前尝试单机监控时已经有所不同。由此我们可以知道RIBBON-CONSUMER启动了两个示例,这里只展示了一个监控图,是由于这两个实例是同一个服务,而对于集群来说我们关注的是服务集群的高可用性,所以Turbine会将相同服务作为整体来看待,并汇总成一个监控图。
与消息代理结合
Spring Cloud在封装Turbine的时候,还封装了基于消息代理的收集实现。所以,我们可以将所有收集的监控信息都输出到消息代理中,然后Turbine服务再从消息代理中异步获取这些监控信息,最后将这些监控信息聚合并输出到Hystrix Dashboard 中。通过引入消息代理,我们的Turbine 和Hysrix Dashboard 实现的监控架构可以改成如下图所示的结构。
从图中可以看到,这里多了一个重要元素RabbitMQ。对于RabbitMQ的安装我们可以查看第9章的相关内容,这里不做过多说明。下面,我们来构建一个新的应用以实现基于消息代理的Turbine聚合服务,具体步骤如下所示。
1. 创建一个标准的Spring Boot 工程,命名为turbine-amqp。
2. 编辑pom.xml,具体依赖内容如下所示:
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>l.8</java.version>
</properties> <parent>
<groupid>org.springframework.cloud</groupid>
<artifactid>spring-cloud-starter-parent</artifactid>
<version>Brixton.SRS</version> <relativePath /> <!-- lookup parent from repository -->
</parent> <dependencies>
<dependency>
<groupid>org.springframework.cloud</groupid>
<artifactid>spring-cloud-starter-turbine-amqp</artifactid> </dependency>
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
可以看到这里主要引入了spring-cloud-starter-turbine-amqp依赖,它实际上包装了 spring-cloud-starter-turbine-stream 和 spring-cloud-stater-stream-rabbit。
注意,这里需要使用Java8来运行
1. 在应用主类中使用@EnableTurbineStream注解来启用Turbine Stream的配置。
@EnableTurbineStream @EnableDiscoveryClient @SpringBootApplication public class TurbineApplication{ public static void main (String[] args){ SpringApplication.run(TurbineApplication.class,args); } }
2. 配置application.properties文件。
spring-application.name=turbine server.port = 8989 management.prot=8990 eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/
对于Turbine的配置已经完成了,下面需要对服务消费者RIBBON-CONSUMER做一些修改,使其监控信息能够输出到RabbitMQ上。这个修改也非常简单,只需在pom.xml中增加对spring-cloud-netflix-hystrix-amqp的依赖,具体如下:
<dependencies>
... <dependency>
<groupid>org.springframework.cloud</groupid>
<artifactid>spring-cloud-netflix-hystrix-amqp</artifactid>
</dependency>
</dependencies>
在完成了上面的配置之后,继续启动eureka-server、HELLO-SERVICE、RIBBON-CONSUMER、Turbine以及Hystrix Dashboard,同时确保RabbitMQ已在正常运行,访问Hystrix Dashboard,并开启对http://localhost:8989/turbine.stream的监控,我们可以获得如之前实现的同样结果,只是这里的监控信息收集是通过消息代理异步实现的。
恭喜,本章完!