服务容错保护:Spring Cloud Hystrix
在微服务架构中,我们将系统拆分为很多个服务,各个服务之间通过注册与订阅的方式相互依赖,由于各个服务都是在各自的进程中运行,就有可能由于网络原因或者服务自身的问题导致调用故障或延迟,随着服务的积压,可能会导致服务崩溃。为了解决这一系列的问题,断路器等一系列服务保护机制出现了。
断路器本身是一种开关保护机制,用于在电路上保护线路过载,当线路中有电器发生短路时,断路器能够及时切断故障电路,防止发生过载、发热甚至起火等严重后果。
在分布式架构中,断路器模式的作用也是类似的。
针对上述问题,Spring Cloud Hystrix 实现了断路器、线路隔离等一系列服务保护功能。它也是基于 Netflix 的开源框架 Hystrix 实现的,该框架的目标在于通过控制那些访问远程系统、服务和第三方库的节点,从而对延迟和故障提供更强大的容错能力。Hystrix 具备服务降级、服务熔断、线程和信号隔离、请求缓存、请求合并以及服务监控等强大功能。
快速入门
在开始实现断路器之前,先用之前实现的一些内容作为基础,构建一个如下图所示的服务调用关系
需要启动的工程有如下一些:
- eureka-server 工程:服务注册中心,端口为1111
- hello-service 工程:HELLO-SERVICE 的服务单元,两个实例启动端口分别为 8001 和 8002.
- ribbon-consumer 工程:使用 Ribbon 实现的服务消费者,端口为 9000
在未加入断路器之前,关闭8001的实例,发送 GET 请求到 http://localhost:3333/ribbon-consumer ,可以获取下面的异常。
引入 Spring Cloud Hystrix
- 在 ribbon-consumer 工程的 pom.xml 的 dependency 节点中引入 spring-cloud-starter-hystrix 依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix</artifactId>
</dependency>
配置主类@EnableCircuitBreaker注解
- 在 ribbon-consumer 工程的主类上使用 @EnableCircuitBreaker 注解开启断路器功能:
package com;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@EnableCircuitBreaker
@EnableDiscoveryClient
@SpringBootApplication
public class Application {
@Bean
@LoadBalanced
RestTemplate restTemplate(){
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
注:此处还可以使用 Spring Cloud 应用中的 @SpringCloudApplication 注解来修饰主类,该注解的具体定义如下。可以看到,该注解中包含了上述所引用的三个注解,这意味着一个 Spring Cloud 标准应用应包含服务发现以及断路器。
改造服务消费方式
- 改造服务消费方式,在 HelloService 类,注入 RestTemplate 实例。然后,将在 ConsumerController 中对 RestTemplate 的使用迁移到 helloService 函数中,最后,在 helloService 函数上增加 @HystrixCommand 注解来指定回调方法。
package com.controller;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
@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";
}
}
- 修改 ConsumerController 类, 注入上面实现的 HelloService 实例,并在 helloConsumer 中进行调用:
package com.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ConsumerController {
@Autowired
HelloService helloService;
@RequestMapping(value = "ribbon-consumer", method = RequestMethod.GET)
public String helloConsumer(){
return helloService.helloService();
}
}
模拟短路方式1
下面,对断路器实现的服务回调逻辑进行验证,重新启动之前关闭的 8001 端口的 hello-service,确保此时服务注册中心、两个 hello-service 和 ribbon-consumer 均已启动,再次访问 http://localhost:9000/ribbon-consumer可以轮询两个 hello-serive 并返回一些文字信息。此时断开其中任意一个端口的 hello-service,再次访问,当轮询到关闭的端口服务时,输出内容为 error ,不再是之前的提示信息。
模拟短路方式2
除了通过断开具体的服务实例来模拟某个节点无法访问的情况之外,还可以模拟一下服务阻塞(长时间未响应)的情况。下面对hello-serive 的 /hello接口做一些修改,具体如下:
通过Thread.sleep 函数可让 /index 接口的处理线程不是马上返回内容,而是在阻塞几秒后才返回内容。由于 Hystrix 默认超时时间为 2000 毫秒,所以这里采用了 0 至 3000 的随机数以让处理过程有一定概率发生超时来触发断路器。为了更精确的观察断路器的触发,在消费者调用函数中做一些时间记录。
原理分析
工作流程
1、创建 HystrixCommand 或 HystrixObservableCommand 对象
**首先,创建一个 HystrixCommand 或 HystrixObservableCommand 对象,用来表示对依赖服务的操作请求,同时传递所有需要的参数。**从其命名中我们就能知道它采用了“命令模式” 来实现服务调用操作的封装。而这两个 Command 对象分别针对不同的应用场景。
- HystrixCommand :用在依赖的服务返回单个操作结果的时候。
- HystrixObservableCommand :用在依赖的服务返回多个操作结果的时候。
命令模式,将来自客户端的请求封装成一个对象,从而让你可以使用不同的请求对客户端进行参数化。它可以被用于实现“行为请求者” 与 “行为实现者” 的解耦,以便使两者可以适应变化。下面的示例是对命令模式的简单实现:
package com;
//接收者
public class Receiver {
public void action(){
//真正的业务逻辑
System.out.println("测试命令模式");
}
}
package com;
//抽象命令
public interface Command {
void execute();
}
package com;
//具体命令实现
public class ConcreteCommand implements Command {
private Receiver receiver;
ConcreteCommand(Receiver receiver) {
this.receiver = receiver;
}
@Override
public void execute() {
this.receiver.action();
}
}
package com;
//客户调用者
public class Invoker {
private Command command;
void setCommand(Command command) {
this.command = command;
}
void action (){
command.execute();
}
}
package com;
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(); //客户端通过调用者来执行命令
}
}
从代码中,可以看到这样几个对象。
- Receiver:接收者,它知道如何处理具体的业务逻辑。
- Command:抽象命令,它定义了一个命令对象应具备的一系列命令操作,比如 execute 等。当命令操作被调用的时候就会触发接收者去做具体命令对应的业务逻辑。
- ConcreteCommand:具体的命令实现,在这里它绑定了命令操作与接收者之间的关系,execute 命令的实现委托给了 Receiver 的 action 函数。
- Invoker:调用者,它持有一个命令对象,并且可以在需要的时候通过命令对象完成具体的业务逻辑。
从上面的示例中,我们可以看到,**调用者 Invoker 与操作者 Receiver 通过 Command 命令接口实现了解耦。**对于调用者来说,我们可以为其注入多个命令操作,调用者只需在需要的时候直接调用即可,而不需要知道这些操作命令实际是如何实现的。而在这里所提到的 HystrixCommand 和 HystrixObservableCommand 则是在 Hystrix 中对 Command 的进一步抽象定义。
2. 命令执行
命令执行方式一共有4种,而 Hystrix 在执行时会根据创建的Command对象以及具体的情况来选择一种执行。
其中 HystrixCommand 实现了下面两个执行方式。
- execute():同步执行,从依赖的服务返回一个单一的结果对象,或是在发生错误的时候抛出异常。
- queue():异步执行,直接返回一个 Future 对象,其中包含了服务执行结束时要返回的单一结果对象。
R execute();
Future<R> queue();
而 HystrixObservableCommand 实现了另外两种执行方式。
- observe():返回 Observable 对象,它代表了操作的多个结果,它是一个 HotObservable。
- toObservable():同样返回 Observable 对象,也代表了操作的多个结果,但它返回的是一个 Cold Observable。
Observable<R> observe();
Observable<R> toObservable();
在 Hystrix 的底层实现中大量使用了 RxJava ,为了更容易的理解后续内容,在这里对 RxJava 的观察者-订阅者模式做一个简单的入门介绍。
上面提到的 Observable 对象就是 RxJava 中的核心内容之一,可以理解为 “事件源” 或者 “被观察者”,与其对应的 Subscriber 对象,可以理解为 “订阅者” 或者 “观察者”。这两个对象是 RxJava 响应式编程的重要组成部分。
- Observable 用来向订阅者 Subscriber 对象发布事件,Subscriber 对象则在接收到事件后对其进行处理,而在这里所指的事件通常就是对依赖服务的调用。
- 一个 Observable 可以发出多个事件,直到结束或者发生异常。
- Observable 对象每发出一个事件,就会调用对应观察者 Subscriber 对象的 onNext() 方法。
- 每一个 Observable 的执行,最后一定会通过调用 Subscriber.onCompleted() 或者 Subscriber.onError() 来结束该事件的操作流。
下面通过一个简单的例子来直观理解一下 Observable 与 Subscribers:
package com;
import rx.Observable;
import rx.Subscriber;
public class Obs_Subs {
public static void main(String[] args) {
//创建事件源
Observable<String> observable = Observable.create(new Observable.OnSubscribe<String>() {
@Override
public void call(Subscriber<? super String> subscriber) {
subscriber.onNext("Hello RxJava ");
subscriber.onNext("I'm XX");
subscriber.onCompleted();
}
});
//创建订阅者
Subscriber<String> subscriber = new Subscriber<String>() {
@Override
public void onCompleted() {
System.out.println("onCompleted方法执行");
}
@Override
public void onError(Throwable throwable) {
System.out.println("onError方法执行");
}
@Override
public void onNext(String s) {
}
};
observable.subscribe(subscriber);
}
}
在该示例中,创建了一个简单的事件源 observable,一个对事件传递内容输出的订阅者 subscriber ,通过 observable.subscribe(subscriber) 来触发事件的发布。
在这里我们对于事件源 observable 提到了两个不同的概念:Hot Observable 和 Cold Observable ,分别对应了上面的 command.observe() 和 command.toObservable() 的返回对象。其中 HotObservable,不论 “事件源” 是否有 “订阅者” ,都会在创建后对事件进行发布,所以对于 Hot Observable 的每一个 “订阅者” 都有可能是从 “事件源” 的中途开始的,并可能只是看到了整个操作的局部过程。而 Cold Observable 在没有 “订阅者” 的时候并不会发布事件,而是进行等待,直到有 “订阅者” 之后才发布事件,所以对于 Cold Observable 的订阅者,它可以保证从一开始看到整个操作的全部过程。
3. 结果是否被缓存
若当前命令的请求缓存功能是被启用的,并且该命令缓存命中,那么缓存的结果会立即以 Observable 对象的形式返回。
4、断路器是否打开
在命令结果没有缓存命中的时候,Hystrix 在执行命令前需要检查断路器是否为打开状态:
- 打开:Hystrix不执行命令,转到 fallback 处理逻辑(对应下面第8步)。
- 关闭:Hystrix 跳到第5步,检查是否有可用资源来执行命令。
5. 线程池 / 请求队列 / 信息量是否占满
如果与命令相关的线程池 / 请求队列 / 信息量已经占满,那么 Hystrix 不会执行命令,跳转到 fallback 处理逻辑(对应下面第8步)。
注意:此处的线程池并非容器的线程池,而是每个依赖服务的专有线程池。Hystrix 为了保证不会因为某个依赖服务的问题影响到其他依赖服务而采用了 “舱壁模式” 来隔离每个依赖的服务。
6、HystrixObservableCommand.construct() 或 HystrixCommand.run()
Hystrix 会根据编写的方法来决定采取什么样的方式去请求依赖服务。
- HystrixCommand.run() :返回一个单一的结果,或者抛出异常。
- HystrixObservableCommand.construct():返回一个 Observable 对象来发射多个结果,或通过 onError 发送错误通知。
如果 run() 或 construct() 方法的执行时间超过了命令设置的超时阈值,当前线程抛出TimeoutException。这种情况下,也会跳转到 fallback 处理逻辑(第8步)。
7、计算断路器的健康度
Hystrix 会将 “成功”、“失败”、“拒绝”、“超时” 等信息报告给断路器,而断路器会维护一组计数器来统计这些数据。
断路器会使用这些统计数据来决定是否要将断路器打开,来对某个依赖服务的请求进行 “熔断 / 短路”,直到恢复期结束。
8、fallback 处理
当命令执行失败的时候,Hystrix 会进入 fallback 尝试回退处理,我们通常也称为 “服务降级”。下面就是能够引发服务降级处理的几种情况:
- 第4步,当前命令处于 “熔断 / 短路” 状态,断路器是打开的时候。
- 第5步,当前命令的线程池、请求队列或者信号量被占满的时候。
- 第6步,HystrixObservableCommand.construct() 或者 HystrixCommand.run() 抛出异常的时候。
9、返回成功的响应
断路器原理
断路器在HystrixCommand 和HystrixObservableCommand执行过程中起到了举足轻重的作用,它是Hystrix和核心部件。那么断路器是如何决策熔断和记录信息的呢?
HystrixCircuitBreaker 的定义:
主要定义了三个断路器的抽象方法。
- allowRequest:Hystrix 命令的请求通过它判断是否被执行。
- isOpen:返回当前断路器是否打开。
- markSuccess:用来闭合断路器。
另外还有三个静态类。
-
静态类 Factory 中维护了一个 Hystrix 命令与 HystrixCircuitBreaker 的关系集合:ConcurrentHashMap<String, HystrixCircuitBreaker> circuitBreakersByCommand,其中 String 类型的key 通过 HystrixCommandKey 定义,每一个 Hystrix 命令需要有一个 key 来标识,同时一个 Hystrix 命令也会在该集合中找到它对应的断路器 HystrixCircuitBreaker 实例。
-
静态类 NoOpCircuitBreaker 定义了一个什么都不做的断路器实现,它允许所有请求,并且断路器状态始终闭合。
-
静态类 HystrixCircuitBreakerImpl 是断路器接口 HystrixCIrcuitBreaker 的实现类,在该类中定义断路器的 4 个核心对象。
-
- HystrixCommandProperties properties :断路器对应 HystrixCommand 实例的属性对象。
- HystrixCommandMetrics metrics :用来让 HystrixCommand 记录各类度量指标的对象。
- AtomicLong circuitOpened :断路器打开或是上一次测试的事件戳。
HystrixCircuitBreakerImpl 的各个实现方法如下:
- isOpen:判断断路器的打开 / 关闭状态。
- allowRequest:判断请求是否被允许:
- markSuccess:“半开路” 状态时使用。若Hystrix命令调用成功,通过该方法将打开的断路器关闭,并重置度量指标对象。
依赖隔离
Hystrix 使用 “舱壁模式” 实现线程池的隔离,它为每一个依赖服务创建一个独立的线程池,就算某个依赖服务出现延迟过高的情况,也不会拖慢其他的依赖服务。
使用详解
创建请求命令
继承方式
Hystrix 命令就是我们之前所说的 HystrixCommand,它用来封装具体的依赖服务调用逻辑。
可以通过继承的方式来实现,比如:
package com.controller;
import com.netflix.hystrix.HystrixCommand;
import org.springframework.web.client.RestTemplate;
public class UserCommand extends HystrixCommand<String> {
private RestTemplate restTemplate;
private int id;
public UserCommand(Setter setter, RestTemplate restTemplate, int id) {
super(setter);
this.restTemplate = restTemplate;
this.id = id;
}
@Override
protected String run() throws Exception {
return restTemplate.getForObject("http://HELLO-SERVICE/user/{1}", String.class, id);
}
}
通过上面实现的UserCommand,我们可以在ConsumerController类中实现请求的同步执行或的异步执行
@RequestMapping(value = "ribbon-consumer/{age}", method = RequestMethod.GET)
public String users(@PathVariable("age") Integer age) throws ExecutionException, InterruptedException {
com.netflix.hystrix.HystrixCommand.Setter setter =
com.netflix.hystrix.HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey(""));
UserCommand userCommand = new UserCommand(setter, restTemplate, age);
//同步执行
//return userCommand.execute();
//异步执行
Future<String> queue = userCommand.queue();
return queue.get();
}
- 同步执行 userCommand.execute();
- 异步执行 Future queue = userCommand.queue() 。异步执行的时候,可以通过返回的futureUser 调用get方法获取结果
注解方式
也可以通过@HystrixCommand注解来更为优雅的试下Hystrix命令的定义,比如:
@HystrixCommand
public String helloUser(Integer age) {
return restTemplate.getForObject("http://HELLO-SERVICE/user/{1}", String.class, age);
}
虽然@HystrixCommand可以优雅的定义Hystrix命令的实现。但是如上定义的helloUser方式只是同步执行的实现,若要实现异步执行则还需另外定义,比如:
@HystrixCommand
public Future<String> getUserByIdAsync(Integer age) {
return new AsyncResult<String>() {
@Override
public String invoke() {
return restTemplate.getForObject("http://HELLO-SERVICE/user/{1}", String.class, age);
}
};
}
除了传统的同步执行与异步执行外,我们还可以将HystrixCommand通过Observable来实现响应式执行方式。通过调用observe()和toObservable()方法可以返回Observable对象,比如:
定义服务降级
继承方式
在**HystrixCommand中可以通过重载getFallback()**方法来实现服务降级逻辑,Hystrix会在run()执行过程中出现错误、超时、线程池拒绝、断路器熔断等情况下,执行getFallback()方法内的逻辑。
//不知道为什么没有触发getFallback
@Override
protected String getFallback() {
return "error";
}
在HystrixObservableCommand实现的Hystrix命令中,重载resumeWithFallback方法来实现服务降级逻辑。
@Override
protected Observable<User> resumeWithFallback() {
return Observable.create(new Observable.OnSubscribe<User>() {
@Override
public void call(Subscriber<? super User> subscriber) {
try {
if (!subscriber.isUnsubscribed()) {
subscriber.onNext(new User());
subscriber.onCompleted();
}
} catch (Exception e) {
subscriber.onError(e);
}
}
}).subscribeOn(Schedulers.io());
}
注解方式
如要通过注解实现服务降级只需要使用**@HystrixCommand中的fallbackMethod参数**来指定具体的服务降级实现方法,如下:
在使用注解来定义服务降级逻辑时,我们需要将具体的Hystrix命令与fallback实现函数定义在同一个类中,并且fallbackMethod的值必须与实现fallback方法的名字相同,而且参数要和getUserByIdAsync或helloUser保持一致。
由于必须在同一个类中,所以对于fallback的访问修饰符没有特定的要求,定义为private、protected、public均可。
若helloFallback1方法实现的并不是一个稳定的逻辑,它依然可能会发生异常,那么我们也可以为它添加@HystrixCommand注解以生成Hystrix命令,同时使用fallbackMethod来指定服务降级逻辑,比如
在实际使用时,我们需要为大多数执行过程中可能会失败的Hystrix命令实现服务降级逻辑,但是也有一些情况下不去实现降级逻辑
- 执行写操作的命令 当Hystrix命令是用来执行写操作而不是返回一些信息的时候,通常情况下这类操作的返回类型是void或是空的Observable,实现服务降级的意义不是很大。当写入操作失败的时候,我们通常只需要通知调用者即可。
- 执行批处理或离线计算的命令 这时通常这些操作只需要将错误传播给调用者,然后让调用者稍后重试而不是发送给调用者一个静默的降级处理响应。
异常处理
1. 异常传播
在调用服务执行HsytrixCommand实现的run()方法抛出异常时,除HystrixBadRequestException之外,其他异常都会认为是Hystrix命令执行失败并触发服务降级处理逻辑,所以当需要在命令执行中抛出不触发服务降级的异常时来使用它。
而在使用注册配置实现Hystrix命令时,它还支持忽略指定异常类型功能,只需要通过设置@HystrixCommand注解的ignoreException参数,比如:
@HystrixCommand(ignoreExceptions = {NullPointerException.class})
public String helloUser(Integer age) {
return restTemplate.getForObject("http://HELLO-SERVICE/user/{1}", String.class, age);
}
2.异常获取
注解方式获取异常(除HystrixBadRequestException外)来做针对性处理。只需在fallback实现方法的参数中增加Throwable e对象的定义,这样在方法内部就可以触发服务降级的具体异常内容了,比如:
//参数要和getUserByIdAsync或helloUser保持一致
@HystrixCommand(fallbackMethod = "helloFallback2")
public String helloFallback1(Integer age,Throwable e) {
e.printStackTrace();
return "helloFallback1 error===" + age;
}
命令名称、分组以及线程池划分
以继承的方式实现Hystrix命令使用类名作为默认的命令名称,我们也可以在构造函数中通过Setter静态类来设置
public UserCommand(Setter setter, RestTemplate restTemplate, int age) {
//super(setter);
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
.andCommandKey(HystrixCommandKey.Factory.asKey("HelloWorld")));
this.restTemplate = restTemplate;
this.age = age;
}
先调用了withGroupKey来设置命令组名,然后再调用andCommandKey来设置命令名。为什么这么麻烦?因为在Setter的定义中,只有withGroupKey静态函数可以创建Setter的实例,基于这个原因,所以GroupKey是每个Setter必需的参数,而CommandKey则是可选参数。
通过设置命令组,Hystrix会根据组来组织和统计命令的告警、仪表盘等信息。那么为什么一定要设置命令组呢?因为除了根据组能实现统计外,Hystrix命令默认的线程划分也是根据命令组的。默认情况下,Hystrix会让相同命令组名的命令使用同一个线程池,所以需要在创建Hystrix命令时为其指定命令组名来实现默认的线程池的划分。
如果Hystrix的线程池分配仅仅只能依靠命令组来划分,那么就显得不够灵活了,所以Hystrix还提供了HystrixThreadPoolKey来对线程池进行设置,通过它可以实现更细粒度的线程池的划分。
public UserCommand(Setter setter, RestTemplate restTemplate, int age) {
//super(setter);
//super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
// .andCommandKey(HystrixCommandKey.Factory.asKey("HelloWorld")));
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
.andCommandKey(HystrixCommandKey.Factory.asKey("HelloWorld"))
.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("HelloWorldPool")));
this.restTemplate = restTemplate;
this.age = age;
}
如果没有特别指定HystrixThreadPoolKey的情况下,依然会使用命令组的方式来划分线程池。通常情况下,尽量使用HystrixThreadPoolKey的方式来指定线程池的划分,而不是通过组名的默认方式实现划分,因为多个不同的命令可能从业务逻辑上看是属于同一个组,但是往往实现本身需要跟其他命令进行隔离。
当我们使用@HystrixCommand注解的时候,只需要设置commandKey、groupKey以及threadPoolKey属性就可以设置命令名称、分组以及线程划分。
@HystrixCommand(fallbackMethod = "helloFallback1",commandKey="HelloWorld",groupKey="ExampleGroup",threadPoolKey="HelloWorldPool")
public Future<String> getUserByIdAsync(Integer age) {
return new AsyncResult<String>() {
@Override
public String invoke() {
return restTemplate.getForObject("http://HELLO-SERVICE/user/{1}", String.class, age);
}
};
}
请求缓存
在高并发的场景之下,Hystrix中提供了请求缓存的功能,可以方便地开启和使用请求缓存来优化系统,达到减轻高并发时请求线程的消耗、降低请求响应时间的效果
继承方式
1. 开启请求缓存功能
Hystrix请求缓存的使用非常简单,我们只需要在实现HystrixCommand或HystrixObservableCommand时,通过重载getCacheKey()方法来开启请求缓存,比如:
package com.controller;
import com.netflix.hystrix.*;
import com.netflix.hystrix.strategy.concurrency.HystrixConcurrencyStrategyDefault;
import com.netflix.hystrix.strategy.concurrency.HystrixRequestContext;
import org.springframework.web.client.RestTemplate;
public class UserCommand extends HystrixCommand<String> {
private RestTemplate restTemplate;
private static final HystrixCommandKey GETTER_KEY = HystrixCommandKey.Factory.asKey("CommandKey");
private int age;
public UserCommand(Setter setter, RestTemplate restTemplate, int age) {
super(setter);
//super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
// .andCommandKey(HystrixCommandKey.Factory.asKey("HelloWorld")));
//super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
// .andCommandKey(HystrixCommandKey.Factory.asKey("HelloWorld"))
// .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("HelloWorldPool")));
HystrixRequestContext.initializeContext();//初始化请求上下文,不然无法开启请求缓存
this.restTemplate = restTemplate;
this.age = age;
}
@Override
protected String run() {
return restTemplate.getForObject("http://HELLO-SERVICE/user/{1}", String.class, age);
}
@Override
protected String getFallback() {
return "我也不想失败";
}
//通过重载getCacheKey()方法来开启请求缓存
@Override
protected String getCacheKey() {
return String.valueOf(age);
}
}
在上面的例子中,我们通过在getCacheKey方法中返回的请求缓存key值(使用了传入的获取age值),就能让该请求命令具备缓存功能。此时,当不同的外部请求处理逻辑调用了同一个依赖服务时,Hystrix会根据getCacheKey方法返回的值来区分是否是重复的请求,如果它们的cacheKey相同,那么该依赖服务只会在第一个请求到达时被真实地调用一次,另外一个请求则是直接从缓存中返回结果。所以通过开启请求缓存可以让我们实现的Hystrix命令具备下面几项好处:
- 减少重复的请求数,降低依赖服务的并发度
- 在同一个用户请求的上下文中,相同依赖服务的返回数据始终保持一致
- 请求缓存在run()和construct()执行之前生效,所以可以有效减少不必要的线程开销
2.清理失效缓存功能
使用请求缓存时,如果只是读操作,那么不需要考虑缓存内容是否正确的问题,但是如果请求命令中还有更新数据的操作,那么缓存中的数据就需要我们在进行写操作时进行及时处理,以防止读操作的请求命令获取到失效的数据。
在Hystrix中,可以通过HystrixRequestCache.clear()方法来进行缓存的清理。
package com.controller;
import com.netflix.hystrix.*;
import com.netflix.hystrix.strategy.concurrency.HystrixConcurrencyStrategyDefault;
import com.netflix.hystrix.strategy.concurrency.HystrixRequestContext;
import org.springframework.web.client.RestTemplate;
public class UserCommand extends HystrixCommand<String> {
private RestTemplate restTemplate;
private static final HystrixCommandKey GETTER_KEY = HystrixCommandKey.Factory.asKey("CommandKey");
private int age;
public UserCommand(Setter setter, RestTemplate restTemplate, int age) {
super(setter);
//super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
// .andCommandKey(HystrixCommandKey.Factory.asKey("HelloWorld")));
//super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
// .andCommandKey(HystrixCommandKey.Factory.asKey("HelloWorld"))
// .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("HelloWorldPool")));
HystrixRequestContext.initializeContext();//初始化请求上下文,不然无法开启请求缓存
this.restTemplate = restTemplate;
this.age = age;
}
@Override
protected String run() {
return restTemplate.getForObject("http://HELLO-SERVICE/user/{1}", String.class, age);
}
@Override
protected String getFallback() {
return "我也不想失败";
}
//通过重载getCacheKey()方法来开启请求缓存
@Override
protected String getCacheKey() {
return String.valueOf(age);
}
public static void flushCache(int age) {
// 刷新缓存,根据id进行清理
HystrixRequestCache.getInstance(GETTER_KEY, HystrixConcurrencyStrategyDefault.getInstance()).clear(String.valueOf(age));
}
}
该示例中主要有两个请求命令:UserGetCommand用于根据id获取User对象、而UserPostCommand用于更新User对象。当我们队UserGetCommand命令实现了请求缓存后,那么势必需要为UserPostCommand命令实现缓存的清理,以保证User被更新之后,Hystrix请求缓存中相同缓存Key的结果被移除,这样在下一次获取User的时候不会从缓存中获取到未更新的结果。
注解方式
方式1使用@CacheResult实现缓存功能
// 异步执行
@CacheResult(cacheKeyMethod = "getCacheKey")
@HystrixCommand(fallbackMethod = "helloFallback1", commandKey = "HelloWorld", groupKey = "ExampleGroup", threadPoolKey = "HelloWorldPool")
public Future<String> getUserByIdAsync(Integer age) {
return new AsyncResult<String>() {
@Override
public String invoke() {
return restTemplate.getForObject("http://HELLO-SERVICE/user/{1}", String.class, age);
}
};
}
private String getCacheKey(Integer age){
return String.valueOf(age);
}
方式2使用@CacheResult和@CacheKey实现缓存功能(不建议使用,好像会报错)
@CacheResult
@HystrixCommand(commandKey = "findUserById", groupKey = "UserService", threadPoolKey = "userServiceThreadPool")
public UserVO findById2(@CacheKey("id") Long id) {
ResponseEntity<UserVO> user = restTemplate.getForEntity("http://users-service/user?id={id}", UserVO.class, id);
return user.getBody();
}
注意: @CacheKey的优先级比cacheKeyMethod的优先级低。
@CacheKey注解除了可以指定方法参数作为缓存key之外,它还允许访问参数对象的内部属性作为缓存Key。比如,下面,它指定了User对象的id属性作为缓存key。
@CacheResult
@HystrixCommand
public User getUserById(@CacheKey("id") User user) {
return restTemplate.getForObject("http://USER_SERVICE/user/{1}", User.class, user.getId());
}
缓存清理
我们已经通过@CacheResult注解将请求结果置入Hystrix的请求缓存中。若该内容调用了update操作进行了更新,那么此时请求缓存中的结果与实际结果就会产生不一致,所以我们需要在update类型的操作上对失效的缓存进行清理。在Hystrix的注解配置中,可以通过@CacheRemove注解来实现失效缓存的清理。
@CacheRemove(commandKey = "findUserById")
@HystrixCommand(commandKey = "updateUser",groupKey = "UserService",threadPoolKey = "userServiceThreadPool")
public void updateUser(@CacheKey("id")UserVO user){
restTemplate.postForObject("http://users-service/user",user,UserVO.class);
}
需要注意的是,@CacheRemove注解的commandKey属性(注意不是cacheKey)时必须指定的,它用来指明需要使用请求缓存的请求命令,因为只有通过该属性的配置,Hystrix才能找到正确的请求命令缓存位置。
请求合并
继承方式
通常微服务架构中的依赖通过远程调用实现,而远程调用中最常见的问题就是通信消耗与连接数占用。在高并发的情况之下,因通信次数的增加,总的通信时间消耗将会变的不那么理想。同时,因为对依赖服务的线程池资源有限,将出现排队等待与响应延迟的情况。为了优化这两个问题,Hystrix提供了HystrixCollapser来实现请求的合并,以减少通信消耗和线程数的占用。
HystrixCollapser实现了在HystrixCommand之前放置一个合并处理器,它将处于一个很短时间窗(默认10毫秒)内对同一依赖服务的多个请求进行整合并以批量方式发起请求的功能(服务提供方也需要提供相应的批量实现接口)。通过HystrixCollapser的封装,开发者不需要去关注线程合并的细节过程,只需要关注批量化服务和处理。下面我们从HystrixCollapser的使用实例,对其合并请求的过程一探究竟。
1.Hystrix的请求合并示例
public abstract class HystrixCollapser<BatchReturnType, ResponseType, RequestArgumentType> implements
HystrixExecutable<ResponseType>, HystrixObservable<ResponseType> {
...
public abstract RequestArgumentType getRequestArgument();
protected abstract HystrixCommand<BatchReturnType> createCommand(Collection<CollapsedRequest<ResponseType, RequestArgumentType>> requests);
protected abstract void mapResponseToRequests(BatchReturnType batchResponse, Collection<CollapsedRequest<ResponseType, RequestArgumentType>> requests);
...
}
从HystrixCollapser抽象类的定义中可以看到,它指定了三个不同的类型:
- BatchReturnType:合并后批量请求的返回类型
- ResponseType:单个请求返回的类型
- RequestArgumentType:请求参数类型
而对于这三个类型的使用可以在它的三个抽象方法中看到:
- RequestArgumentType getRequestArgument():该函数用来定义获取请求参数的方法。
- HystrixCommand createCommand(Collection<CollapsedRequest<ResponseType, RequestArgumentType>> requests):合并请求产生批量命令的具体实现方法。
- mapResponseToRequests(BatchReturnType batchResponse, Collection<CollapsedRequest<ResponseType, RequestArgumentType>> requests):批量命令结果返回后的处理,这里需要实现将批量结果拆分并传递给合并前的各个原子请求命令的逻辑。
接下来,我们通过一个简单的示例来直观的理解实现请求合并的过程。
假设,当前微服务USER-SERVICE提供了两个获取User的接口:
- /users/{id}:根据id返回User对象的GET请求接口。
- /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.getForObject("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对象的请求命令进行合并的实现:
- 第一步:为请求合并的实现准备一个批量请求命令的实现,具体如下:
public class UserBatchCommand extends HystrixCommand<List<User>> {
UserService userService;
List<Long> userIds;
public 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的列表结果。
- 第二步,通过继承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(100)));
this.userService = userService;
this.userId = userId;
}
@Override
public Long getRequestArgument() {
return userId;
}
@Override
protected HystrixCommand<List<User>> createCommand(Collection<CollapsedRequest<User, Long>> collapsedRequests) {
List<Long> userIds = new ArrayList<>(collapsedRequests.size());
userIds.addAll(collapsedRequests.stream().map(CollapsedRequest::getArgument).collect(Collectors.toList()));
return new UserBatchCommand(userService, userIds);
}
@Override
protected void mapResponseToRequests(List<User> batchResponse, Collection<CollapsedRequest<User, Long>> collapsedRequests) {
int count = 0;
for (CollapsedRequest<User, Long> collapsedRequest : collapsedRequests) {
User user = batchResponse.get(count++);
collapsedRequest.setResponse(user);
}
}
}
在上面的构造函数中,我们为请求合并器设置了时间延迟属性,合并器会在该时间窗内收集获取单个User的请求并在时间窗结束时进行合并组装成单个批量请求。下面getRequestArgument方法返回给定的单个请求参数userId,而createCommand和mapResponseToRequests是请求合并器的两个核心:
- createCommand:该方法的collapsedRequests参数中保存了延迟时间窗中收集到的所有获取单个User的请求。通过获取这些请求的参数来组织上面我们准备的批量请求命令UserBatchCommand实例。
- mapResponseToRequests:在批量命令UserBatchCommand实例被触发执行完成之后,该方法开始执行,其中batchResponse参数保存了createCommand中组织的批量请求命令的返回结果,而collapsedRequests参数则代表了每个被合并的请求。在这里我们通过遍历批量结果batchResponse对象,为collapsedRequests中每个合并前的单个请求设置返回结果,以此完成批量结果到单个请求结果的转换。
注解方式
在快速入门的例子中,我们使用@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;
}
@HystrixCommand
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属性为合并请求器设置相关属性,这里使用@HystrixProperty(name=“timerDelayInMilliseconds”, value = “100”)将合并时间窗设置为100毫秒。这样通过@HystrixCollapser注解简单而又优雅地实现了在/users/{id}依赖服务之前设置了一个批量请求合并器。
请求合并的额外开销
虽然通过请求合并可以减少请求的数量以缓解依赖服务线程池的资源,但是在使用的时候也需要注意它所带来的额外开销:用于请求合并的延迟时间窗会使得依赖服务的请求延迟增高。比如:某个请求在不通过请求合并器访问的平均耗时为5ms,请求合并的延迟时间窗为10ms(默认值),那么当该请求的设置了请求合并器之后,最坏情况下(在延迟时间窗结束时才发起请求)该请求需要15ms才能完成。
由于请求合并器的延迟时间窗会带来额外开销,所以我们是否使用请求合并器需要根据依赖服务调用的实际情况来选择,主要考虑下面两个方面:
- 请求命令本身的延迟。如果依赖服务的请求命令本身是一个高延迟的命令,那么可以使用请求合并器,因为延迟时间窗的时间消耗就显得莫不足道了。
- 延迟时间窗内的并发量。如果一个时间窗内只有1-2个请求,那么这样的依赖服务不适合使用请求合并器,这种情况下不但不能提升系统性能,反而会成为系统瓶颈,因为每个请求都需要多消耗一个时间窗才响应。相反,如果一个时间窗内具有很高的并发量,并且服务提供方也实现了批量处理接口,那么使用请求合并器可以有效的减少网络连接数量并极大地提升系统吞吐量,此时延迟时间窗所增加的消耗就可以忽略不计了。
属性详解
在之前介绍Hystrix的使用方法时,已经涉及过一下Hystrix属性的配置,我们可以根据实现HystrixCommand的不同方式将配制方法分为如下两类。
①通过继承的方式实现,可用Setter对象来对请求命令的属性进行设置,比如下面的例子:
public UserCommand() {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("GroupName"))
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
.withExecutionTimeoutInMilliseconds(5000))
.andCommandKey(HystrixCommandKey.Factory.asKey("CommandName"))
.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("ThreadPoolKey")));
}
②通过注解的方法实现,只需要使用@HystrixCommand中的commandProperties属性来设置,比如:
@HystrixCommand(commandKey="hello",commandProperties={
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="5000")
})
public User findById(String name,int age){
return restTemplate.getForObject("http://HELLO-SERVICE/hystrix/getUser?name={1}&age={2}", User.class, name, age);
}
Hystrix为我们提供的属性都存在四种优先级别的配置(优先级由低到高)。
- 全局默认值:如果没有设置下面三个优先级的属性,那么这个属性就是默认值。由于该属性通过代码定义,所以对这个级别,我们需要关注它在代码中定义的默认值即可。
- 全局配置属性:通过在配置文件中定义全局属性值,在应用启动时或在与Spring Cloud Config和Spring Cloud Bus实现的动态刷新配置功能的配合下,可以实现对“全局默认值”的覆盖已经在运行期对“全局默认值”的动态调整。
- 实例默认值:通过代码为实例定义的默认值。通过代码的方式为实例设置属性值来覆盖默认的全局配置
- 实例配置属性:通过配置文件来为指定的实例进行属性配置,以覆盖前面的三个默认值。它也可用Spring Cloud Config 和Spring Cloud Bus实现的动态刷新配置功能实现对具体实例配置的动态调整。
关于配置的写法,这里作简要说明:
属性配置可以是在properties文件中,也可以是在方法注解的属性里配置,两处配置的属性名称有区别,在properties里配置的属性是以 hystrix.command.default. 、 hystrix.threadpool.default 、 hystrix.collapser.default 开头,其中default表示默认值,如需要配置指定commandKey的值,将default换成commandKey即可。如果是在方法注解的属性里配置,则不需要这个前缀。下面我们来具体看看Hystrix有哪些具体的属性配置,且详细说明了属性配置在properties里配置和在方法注解里配置的写法。
1.Command属性
Command属性主要用来控制HystrixCommand命令的行为。它主要有下面5种不同类型的属性配置。
A. execution配置
execution配置控制的是HystrixCommand.run()的执行。
-
execution.isolation.strategy:该属性用来设置HystrixCommand.run()执行的隔离策略,它有如下两个选项。
- THREAD:通过线程池隔离的策略。它在独立的线程上执行,并且它的并发限制受线程池中线程数量的限制。
- SEMAPHORE:通过信号量隔离的策略。它在调用线程上执行,并且它的并发限制受信号量计数的限制。
-
execution.isolation.thread.timeoutMilliseconds: 该属性用来配置HystrixCommand执行的超时时间,单位为毫秒。当HystrixCommand执行时间超过该配置值后,Hystrix会将该命令标记为TIMEOUT并进入服务降级处理逻辑
-
execution.timeout.enabled:该属性用来设置HystrixCommand.run()的执行是否启用超时时间。默认为true,如果设置为false,那么execution.isolation.thread.timeoutMilliseconds属性将不起作用
-
execution.isolation.thread.interruptOnTimeout:该属性用来配置当HystrixCommand.run()执行超时的时候是否要将它中断。
- execution.isolation.semaphore.maxConcurrentRequests:当HystrixCommand的隔离策略使用信号量时,该属性用来配置信号量的大小(并发请求数)。当最大并发请求数达到该设置值时,后续的请求将会被拒绝。
B. fallback配置
下面这些属性用来控制HystrixCommand.getFallback()的执行。这些属性同时适用于线程池的信号量的隔离策略。
-
fallback.isolation.semaphore.maxConcurrentRequests:该属性用来设置从调用线程中允许HystrixCommand.fallback()方法执行的最大并发请求数。当达到最大并发请求数时,后续的请求将会被拒绝并抛出异常(因为它已经没有后续的fallback可以被调用了)
-
fallback.enabled:该属性用来设置服务降级策略是否启用,如果设置为false,那么当请求失败或拒绝发生时,将不会调用HystrixCommand.getFallback()来执行服务降级逻辑。
C. circuitBreaker配置
下面这些是断路器的属性位置,用来控制HystrixCircuitBreaker的行为。
- circuitBreaker.enabled:该属性用来确定当服务请求命令失败时,是否使用断路器来跟踪其健康指标和熔断请求
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z8k5Jsqn-1600187989992)(media/20181006212445738.png)] - circuitBreaker.requestVolumeThreshold:该属性用来设置在滚动时间窗中,断路器熔断的最小请求数。例如:默认值为20的时候,如果滚动时间窗(默认10s)内收到了19个请求,即使这19个请求都失败了,断路器也不会打开。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-herncLZn-1600187989992)(media/20181006212644748.png)] - circuitBreaker.sleepWindowInMilliseconds:该属性用来设置当断路器打开之后的休眠时间窗。休眠时间窗结束后,会将断路器置为“半开”状态,尝试熔断的请求命令,如果依然失败就将断路器继续设置为“打开”状态,如果成功就设置为“关闭”状态。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KMmHeJck-1600187989992)(media/20181006212826506.png)] - circuitBreaker.errorThresholdPercentage:该属性用来设置断路器打开的错误百分比条件。比如,默认值为50的情况下,表示在滚动时间窗中,在请求数量超过circuitBreaker.requestVolumeThreshold阈值的前提下,如果错误请求数的百分比超过50,就把断路器设置为“打开”状态,否则就设置为“关闭”状态。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZMFW0xqz-1600187989993)(media/20181006213154918.png)] - circuitBreaker.forceOpen:如果将该属性设置为true,断路器将强制进入“打开”状态,它会拒绝所有请求。该属性优先于circuitBreaker.forceClosed属性
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gtlkt3PH-1600187989993)(media/20181006213254756.png)] - circuitBreaker.forceClosed:如果将该属性设置为true,断路器将强制进入“关闭”状态,它会接收所有请求。如果circuitBreaker.forceOpen属性为true,该属性不会生效。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xze3h11Y-1600187989994)(media/20181006213347250.png)]
D. metrics配置
下面的属性均与HystrixCommand和HystrixObservableCommand执行中捕获的指标信息有关。
- metrics.rollingStats.timeInMilliseconds:该属性用来设置滚动时间窗的长度,单位为毫秒。该时间同于断路器判断健康度时需要收集信息的持续时间。断路器在收集指标信息的时候会根据设置的时间窗长度拆分成多个“桶”来累计各度量值,每个“桶”记录了一段时间内的采集指标。例如,当采用默认值10000毫秒时,断路器默认将其拆分成10个桶(桶的数量也可通过metrics.rollingStats.numBuckets参数设置),每个桶记录1000毫秒内的指标信息。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C6aNMBRV-1600187989994)(media/20181006213715521.png)]
注意:该属性从Hystrix1.4.12版本开始只有在应用初始化时生效,通过动态刷新不会产生效果,避免运行期监测数据丢失。
- metrics.rollingStats.numBuckets:该属性用来设置滚动时间窗统计指标信息时划分“桶”的数量。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R9ChKagC-1600187989995)(media/20181006213813129.png)]
注意:metrics.rollingStats.timeInMilliseconds参数的设置必须能被metrics.rollingStats.numBuckets参数整除,否则抛出异常,且1.4.12版本开始只有在应用初始化时生效,通过动态刷新不会产生效果,避免运行期监测数据丢失。 - metrics.rollingPercentile.enabled:该属性用来设置对命令执行的延迟是否使用百分位数来跟踪和计算。如果为false,那么所有的概要统计都将返回-1。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w2RDAPs9-1600187989995)(media/20181006213935961.png)] - metrics.rollingPercentile.timeInMilliseconds:该属性用来设置百分位统计的滚动窗口的持续时间,单位毫秒
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PjQTxLFt-1600187989995)(media/20181006214034608.png)]
注意:该属性从1.4.12版本开始只有在应用初始化时生效,通过动态刷新不会产生效果,避免运行期监测数据丢失。 - metrics.rollingPercentile.numBuckets:该属性用来设置百分位统计滚动窗口中使用“桶”的数量。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DtfjUjyH-1600187989996)(media/20181006214135159.png)]
注意:metrics.rollingPercentile.timeInMilliseconds参数的设置必须能被metrics.rollingPercentile.numBuckets参数整除,否则抛出异常,且1.4.12版本开始只有在应用初始化时生效,通过动态刷新不会产生效果,避免运行期监测数据丢失。 - metrics.rollingPercentile.bucketSize:该属性用来设置在执行过程中每个“桶”中保留的最大执行次数。如果在滚动时间窗内发生超过该设定值的执行次数,就从最初的位置开始重写。例如,将该值设为100,滚动窗口10秒,若在10秒内一个“桶”中发生500次执行,那么该“桶”中只保留最后的100次执行统计。另外,增加该值的大小将会增加内存量的消耗,并增加排序百分位数所需的计算时间。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DOoK1Lxu-1600187989996)(media/20181006214304423.png)]
注意:该属性从1.4.12版本开始只有在应用初始化时生效,通过动态刷新不会产生效果,避免运行期监测数据丢失。 - metrics.healthSnapshot.intervalInMilliseconds:该属性用来设置采集影响断路器状态的健康快照(请求成功、错误百分比)的间隔等待时间。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ruW7IAQ9-1600187989997)(media/20181006214411979.png)]
E. requestContext配置
下面的属性涉及HystrixCommand使用的HystrixRequestContext的设置。
- requestCache.enabled:此属性用来设置是否开启请求缓存。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gJcgY9gs-1600187989997)(media/20181006214525956.png)] - requestLog.enabled:该属性用来设置HystrixCommand的执行和事件是否打印日志到HystrixRequestLog中。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bEynsRP1-1600187989997)(media/20181006214622360.png)]
2.collapser属性
该属性除了在代码中用Setter()和配置文件配置外,也可使用注解进行配置。可使用@HystrixCollapser中的collapserProperties属性来设置,比如:
@HystrixCollapser(batchMethod="findByBatch",collapserProperties={
@HystrixProperty(name="timerDelayInMilliseconds",value="100")
})
1234
下面这些属性用来控制命令合并相关的行为。
- maxRequestsInBatch:该参数用来设置一次请求合并批处理中允许的最大请求数。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CfGIzHRI-1600187989998)(media/20181006214854591.png)] - timerDelayInMilliseconds:该属性用来设置批处理过程中每个命令延迟的时间,单位为毫秒。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-olIyHxFJ-1600187989998)(media/20181006215119292.png)] - requestCache.enabled:该属性用来设置批处理过程中是否开启缓存。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FbGlcgO5-1600187989999)(media/20181006215320781.png)]
3.threadPool属性
该属性除了在代码中用Setter()和配置文件配置外,还可使用注解进行配置。可使用@HystrixCommand中的threadPoolProperties属性来设置,比如:
@HystrixCommand(threadPoolProperties={
@HystrixProperty(name="coreSize",value="20")
})
1234
下面这些属性用来控制Hystrix命令所属线程池的配置。
- coreSize:该属性用来设置执行命令线程池的核心线程数,该值也是命令执行的最大并发量。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FjIxLX28-1600187989999)(media/2018100621555276.png)] - maxQueueSize:该属性用来设置线程池的最大队列大小。当设置为-1时,线程池将使用SynchronousQueue实现的队列,否则将使用LinkedBlockingQueue实现的队列。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0yxxS8w2-1600187990000)(media/2018100621565596.png)]
注意:该属性只有在应用初始化时生效,通过动态刷新不会产生效果。 - queueSizeRejectionThreshold:该属性用来为队列设置拒绝阈值。通过该参数,即使队列没有达到最大值也能拒绝请求。该参数主要是对LinkedBlockingQueue队列的补充,因为LinkedBlockingQueue队列不能动态修改它的对象大小,而通过该属性就可以调整拒绝请求的队列大小了。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gs5isonP-1600187990000)(media/20181006215818426.png)]
注意:当maxQueueSize属性值为-1时,该属性不会生效。
threadPool属性通过Setter设置示例:
public UserCommand() {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("GroupName"))
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
.withExecutionIsolationStrategy(ExecutionIsolationStrategy.THREAD)
.withExecutionTimeoutInMilliseconds(5000)
)
.andCommandKey(HystrixCommandKey.Factory.asKey("CommandName"))
.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("ThreadPoolKey"))
//设置 threadPool属性
.andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
.withCoreSize(20)));
}
12345678910111213
下面是整理:
- HystrixCommandProperties
/**
* Command execution properties.
*/
# 隔离策略,默认是线程隔离,还有信号量隔离,参见枚举:ExecutionIsolationStrategy
hystrix.command.default.execution.isolation.strategy=THREAD
# 隔离线程超时时间,默认1s
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=1000
# 是否启用超时配置
hystrix.command.default.execution.timeout.enabled=true
# 超时的时候是否中断隔离线程
hystrix.command.default.execution.isolation.thread.interruptOnTimeout=true
# 隔离线程正在执行取消操作时是否中断
hystrix.command.default.execution.isolation.thread.interruptOnFutureCancel=false
# 隔离策略的最大信号量,只有使用信号量隔离策略时生效
hystrix.command.default.execution.isolation.semaphore.maxConcurrentRequests=10
/**
* Command fallback properties.HystrixCommand.getFallback()
*/
# 降级方法的最大调用线程数,如果超出此信号量,会抛出异常
hystrix.command.default.fallback.isolation.semaphore.maxConcurrentRequests=10
# 是否启用降级
hystrix.command.default.fallback.enabled=true
/**
* Command circuit breaker properties.
*/
# 是否启用断路器
hystrix.command.default.circuitBreaker.enabled=true
# 请求量阈值,请求量达到该值是会开启断路器
hystrix.command.default.circuitBreaker.requestVolumeThreshold=20
# 当断路器打开后,会直接拒绝请求,此时间是配置多长时候后再次尝试处理请求
hystrix.command.default.circuitBreaker.sleepWindowInMilliseconds=5000
# 打开断路器并走回退逻辑的错误率,默认50%
hystrix.command.default.circuitBreaker.errorThresholdPercentage=50
# 是否强制打开断路器,打开后会直接拒绝所有请求
hystrix.command.default.circuitBreaker.forceOpen=false
# 是否强制关闭断路器,关闭后会处理所有请求
hystrix.command.default.circuitBreaker.forceClosed=false
/**
* Command metrics properties.主要用于统计执行情况
*/
# 统计的时间窗口值
hystrix.command.default.metrics.rollingStats.timeInMilliseconds=10000
# 统计时间窗口内分成的份数,需要保证timeInMilliseconds % numBuckets == 0
hystrix.command.default.metrics.rollingStats.numBuckets=10
# 是否启用百分数统计
hystrix.command.default.metrics.rollingPercentile.enabled=true
# 百分数统计的时间周期
hystrix.command.default.metrics.rollingPercentile.timeInMilliseconds=60000
# 百分数统计时间内分成的份数
hystrix.command.default.metrics.rollingPercentile.numBuckets=6
# 百分数统计每份的最大数量。每个bucket只取这个配置数量的执行数来统计
hystrix.command.default.metrics.rollingPercentile.bucketSize=100
# 记录健康快照间隔毫秒数
hystrix.command.default.metrics.healthSnapshot.intervalInMilliseconds=500
/**
* Command CommandRequest Context properties.
*/
# 是否启用请求缓存。当HystrixCommand.getCacheKey()调用后,缓存到HystrixRequestCache
hystrix.command.default.requestCache.enabled=true
# 是否启用请求日志记录。HystrixCommand执行或者事件的日志到HystrixRequestLog
hystrix.command.default.requestLog.enabled=true
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465
- HystrixCollapserProperties
/**
* Collapser properties.
*/
# 批处理最大请求数,达到该值时就算没有达到时间也会触发批处理,默认值Integer.MAX_VALUE
hystrix.collapser.default.maxRequestsInBatch=0x7fffffff
# 触发批处理的延迟,在触发之前的同样请求可能会放到同一个批处理中
hystrix.collapser.default.timerDelayInMilliseconds=10
# 是否启用请求缓存
hystrix.collapser.default.requestCache.enabled=true
# 统计时间窗口值
hystrix.collapser.default.metrics.rollingStats.timeInMilliseconds=10000
# 统计时间窗口内分成的份数
hystrix.collapser.default.metrics.rollingStats.numBuckets=10
# 是否启用百分数统计
hystrix.collapser.default.metrics.rollingPercentile.enabled=true
# 百分数统计的时间周期
hystrix.collapser.default.metrics.rollingPercentile.timeInMilliseconds=60000
# 百分数统计时间内分成的份数
hystrix.collapser.default.metrics.rollingPercentile.numBuckets=6
# 百分数统计每份的最大数量。每个bucket只取这个配置数量的执行数来统计
hystrix.collapser.default.metrics.rollingPercentile.bucketSize=100
12345678910111213141516171819202122
- HystrixThreadPoolProperties
/**
* Thread pool properties.
*/
# 是否启用maximumSize配置
hystrix.threadpool.default.allowMaximumSizeToDivergeFromCoreSize=false
# 线程数量
hystrix.threadpool.default.coreSize=10
# 最大执行线程数
hystrix.threadpool.default.maximumSize=10
# 线程存活毫秒数
hystrix.threadpool.default.keepAliveTimeMinutes=1
# 最大等待线程队列,如果-1为SynchronousQueue;其他则为LinkedBlockingQueue
hystrix.threadpool.default.maxQueueSize=-1
# 拒绝队列大小,即使maxQueueSize没有达到,达到queueSizeRejectionThreshold该值后,请求也会被拒绝。当maxQueueSize为-1,则该属性不可用
hystrix.threadpool.default.queueSizeRejectionThreshold=5
# 线程池统计时间窗口值
hystrix.threadpool.default.metrics.rollingStats.timeInMilliseconds=10000
# 线程池统计时间窗口内分成的份数
hystrix.threadpool.default.metrics.rollingStats.numBuckets=10
1234567891011121314151617181920
Hystrix 仪表盘
1.在Spring Cloud中创建一个Hystrix Dashboard
架构如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4vCPDUUV-1600187990001)(media/20181007134525255.png)]
- 第一步:创建一个普通的Spring Boot工程
创建一个Spring Boot工程这个比较简单,直接创建一个名为hystrix-dashboard的Spring Boot工程。 - 第二步:添加相关依赖
Spring Boot工程创建好之后,修改pom.xml文件,添加相关依赖,如下:
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-parent</artifactId>
<version>Dalston.SR3</version>
<relativePath/>
</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>
12345678910111213141516171819202122
依赖这里,我们主要修改一下parent的内容,然后添加三个依赖,注意不要有遗漏哦。
- 第三步:入口类上添加注解
添加好依赖之后,在入口类上添加@EnableHystrixDashboard注解,表示开启仪表盘功能,如下:
@SpringBootApplication
@EnableHystrixDashboard
public class HystrixDashboardApplication {
public static void main(String[] args) {
SpringApplication.run(HystrixDashboardApplication.class, args);
}
}
12345678
- 第四步:属性配置
最后,我们可以根据个人偏好来配置一下application.properties文件,我这里配置两个基本的属性,如下:
spring.application.name=hystrix-dashboard
server.port=2001
12
OK,做完这些之后,我们的监控环境基本上就搭建成功了。
运行效果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HLziAApT-1600187990001)(media/20181007130246128.png)]
通过Hystrix Dashboard主页面的文字介绍,我们可以知道,Hystrix Dashboard共支持三种不同的监控方式
- 默认的集群监控:通过URL:http://turbine-hostname:port/turbine.stream开启,实现对默认集群的监控。
- 指定的集群监控:通过URL:http://turbine-hostname:port/turbine.stream?cluster=[clusterName]开启,实现对clusterName集群的监控。
- 单体应用的监控:通过URL:http://hystrix-app:port/hystrix.stream开启,实现对具体某个服务实例的监控。
由于对集群的监控需要整合Turbine才能实现。我们先来实现对单个服务的监控。
OK,现在我们的仪表盘工程已经创建成功了,但是还不能用来监控某一个服务,要监控某一个服务,需要该服务提供一个/hystrix.stream接口,so,我们需要对我们的服务消费者工程稍加改造。
2.改造要监控的服务
我们来改造一下我们的服务消费者工程,改造方式很简单,两个步骤就搞定,首先在pom.xml文件中添加如下依赖:
<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>
12345678
然后在服务消费者工程的入口类上添加@EnableCircuitBreaker注解,表示开启断路器功能。
此时,我们再来启动我们的eureka-server、provider、和consumer工程,在consumer工程的启动日志中,我们可以看到如下信息:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GVJAobWq-1600187990002)(media/2018100713211189.png)]
这个信息表明我们的consumer工程目前已经具备了/hystrix.stream接口,我们可以直接访问这个接口了。但是这里有一个细节需要小伙伴们注意:要访问/hystrix.stream接口,得先访问consumer工程中的任意一个其他接口,否则如果直接访问/hystrix.stream接口的话,会打印出一连串的ping: ping: …。 OK,我先访问consumer中的任意一个其他接口,然后在访问/hystrix.stream接口。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XIuMEipZ-1600187990002)(media/20181007132429425.png)]
然后点击Monitor Stream按钮,我们就可以看到监控画面了,如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-etAitGLC-1600187990003)(media/2018100713250937.png)]
3.参数详解
OK,仪表盘已经显示出来了,那么仪表盘上的各项数据都是什么意思呢?我们来看下面一张图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-m7RBivEl-1600187990003)(media/20181007132733968.png)]
参考: https://segmentfault.com/a/1190000011478978
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1VgHsNDV-1600187990003)(media/20181007133123794.png)]
注意:当使用Hystrix Board来监控Spring Cloud Zuul构建的API网关时,Thread Pool信息会一直处于Loading状态。这是由于Zuul默认会使用信号量来实现隔离,只有通过Hystrix配置把隔离机制改成为线程池的方式才能够得以展示。
Turbine集群监控
架构如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r54jtedf-1600187990004)(media/20181007134648464.png)]
上文我们看了一个监控单体应用的例子,在实际应用中,我们要监控的应用往往是一个集群,这个时候我们就得采取Turbine集群监控了。Turbine有一个重要的功能就是汇聚监控信息,并将汇聚到的监控信息提供给Hystrix Dashboard来集中展示和监控。那我们就来看看Turbine集群监控如何使用。
1.搭建监控环境
监控环境的搭建也是分为四个步骤:
- 第一步:创建一个普通的Spring Boot工程
第一步创建一个名叫turbine的普通Spring Boot工程。
- 第二步:添加依赖
工程创建完成之后,我们需要添加一个依赖,如下:
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-parent</artifactId>
<version>Dalston.SR3</version>
<relativePath/>
</parent>
<dependencies>
<!-- 其他默认的依赖 -->
<!-- 我们要添加的依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-turbine</artifactId>
</dependency>
</dependencies>
123456789101112131415161718
- 第三步:添加注解
在入口类上添加@EnableTurbine注解表示开启Turbine,如下:
@SpringBootApplication
@EnableDiscoveryClient
@EnableTurbine
public class TurbineApplication {
public static void main(String[] args) {
SpringApplication.run(TurbineApplication.class, args);
}
}
123456789
- 第四步:修改配置
在application.properties配置文件中加入eureka和turbine的相关配置,如下:
spring.application.name=turbine
server.port=2002
management.port=2003
eureka.client.service-url.defaultZone=http://localhost:1111/eureka/
turbine.app-config=ribbon-consumer
turbine.cluster-name-expression="default"
turbine.combine-host-port=true
1234567
关于这个配置文件,我说如下几点:
1.turbine.app-config=ribbon-consumer指定了要监控的应用名字为ribbon-consumer
2.turbine.cluster-name-expression="default",表示集群的名字为default
3.turbine.combine-host-port=true表示同一主机上的服务通过host和port的组合来进行区分,默认情况下是使用host来区分,这样会使本地调试有问题
4. management.port 配置Spring Boot Actuator的端口
1234
与消息代理结合
Spring Cloud在封装Turbine的时候,还封装了基于消息代理的实现。可以将所有需要收集的监控消息都输出到消息代理中,然后Turbine服务再从消息代理中异步获取这些监控消息,最后将这些监控消息聚合并输出到Hystrix Dashboard中。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-boEKMRS2-1600187990004)(media/2018100714173456.png)]
这里多了一个重要元素RabbitMQ
构建一个新的应用以实现基于消息代理的Turbine聚合服务。
- 创建一个Spring Boot工程,命名turbine-amqp。
- 编辑pom.xml,增加相关依赖。
<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>
1234567891011
这里主要引入了spring-cloud-starter-turbine-amqp,它实际包装了spring-cloud-starter-turbine-stream和spring-cloud-starter-stream-rabbit。
- 在主类上加入注解@EnableTurbineStream来启用Turbine Stream的配置。
package com.didispace;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.turbine.stream.EnableTurbineStream;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableAutoConfiguration
@EnableTurbineStream
@EnableDiscoveryClient
public class TurbineApplication {
public static void main(String[] args) {
SpringApplication.run(TurbineApplication.class, args);
}
}
1234567891011121314151617181920
- 修改配置文件
spring.application.name=turbine-amqp
server.port=7989
management.port=7990
eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/
1234567
- 修改服务消费者Ribbon-consumer的pom.xml,增加spring-cloud-netflix-hystrix-amqp依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix-hystrix-amqp</artifactId>
</dependency>
12345
-
测试
1 启动eureka、hello-Service、Ribbon-consumer、Turbine以及Hystrix Dashboard。
2 确保RabbitMQ正常运行。
3 访问Hystrix Dashboard。
4 开启http://localhost:8989/turnbine.stram监控。
5 观察结果