资料参考:《Spring Cloud 微服务实战》
目录
1. 创建 HystrixCommand 或 HystrixObservableCommand 对象
6. HystrixObservableCommand.construct() 或 HystrixCommand.run()
在微服务架构中,我们将系统拆分为很多个服务,各个服务之间通过注册与订阅的方式相互依赖,由于各个服务都是在各自的进程中运行,依赖通过远程调用得方式执行 , 就有可能由于网络原因或者服务自身的问题导致调用故障或延迟。诺调用方得请求不断得增加,随着服务的积压,可能会导致服务崩溃。为了解决这一系列的问题,断路器等一系列服务保护机制出现了。
断路器本身是一种开关保护机制,用于在电路上保护线路过载,当线路中有电器发生短路时,断路器能够及时切断故障电路,防止发生过载、发热甚至起火等严重后果。
在分布式架构中,断路器模式的作用也是类似的。
针对上述问题,Spring Cloud Hystrix 实现了断路器、线路隔离等一系列服务保护功能。它也是基于 Netflix 的开源框架 Hystrix 实现的,该框架的目标在于通过控制那些访问远程系统、服务和第三方库的节点,从而对延迟和故障提供更强大的容错能力。Hystrix 具备服务降级、服务熔断、线程和信号隔离、请求缓存、请求合并以及服务监控等强大功能。
快速入门
在开始实现断路器之前,先用之前实现的一些内容作为基础,构建一个如下图所示的服务调用关系。
需要启动的工程有如下一些:
- eureka-server 工程:服务注册中心,端口为1111
- hello-service 工程:HELLO-SERVICE 的服务单元,两个实例启动端口分别为 1112和 1113.
- ribbon-consumer 工程:使用 Ribbon 实现的服务消费者,端口为 1114
以上几个服务,学习eureka和ribbon时已经用过了,都清楚。
在未加入断路器之前,关闭1112的实例,发送 GET 请求到 http://localhost:1112/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>
- 在 ribbon-consumer 工程的主类上使用 @EnableCircuitBreaker 注解开启断路器功能:
@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
@RibbonClient(name="provider-eureka", configuration=RibbonConfiguration.class)
public class DemoEurekaConsumeApplication {
@Bean
@LoadBalanced
RestTemplate restTemplate(){
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(DemoEurekaConsumeApplication.class, args);
}
}
注:此处还可以使用 Spring Cloud 应用中的 @SpringCloudApplication 注解来修饰主类,该注解的具体定义如下。可以看到,该注解中包含了上述所引用的三个注解,这意味着一个 Spring Cloud 标准应用应包含服务发现以及断路器。
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
public @interface SpringCloudApplication {
}
- 改造服务消费方式,新增 HelloService 类,注入 RestTemplate 实例。然后,将在 ConsumerController 中对 RestTemplate 的使用迁移到 helloService 函数中,最后,在 helloService 函数上增加 @HystrixCommand 注解来指定回调方法。
@Service
public class HelloService {
@Autowired
RestTemplate restTemplate;
@Autowired
private LoadBalancerClient loadBalancerClient;
@HystrixCommand(fallbackMethod = "helloFallback")
public String helloService(){
ServiceInstance serviceInstance = loadBalancerClient.choose("provider-eureka");
StringBuilder sb = new StringBuilder();
sb.append("host: ").append(serviceInstance.getHost()).append(", ");
sb.append("port: ").append(serviceInstance.getPort()).append(", ");
sb.append("uri: ").append(serviceInstance.getUri());
System.out.println(sb.toString());
return restTemplate.getForEntity("http://PROVIDER-EUREKA/hello",String.class).getBody();
}
public String helloFallback(){
return "error";
}
}
- 修改 ConsumerController 类, 注入上面实现的 HelloService 实例,并在 helloConsumer 中进行调用:
@RestController
public class ConsumerController {
@Autowired
HelloService helloService;
@RequestMapping(value = "ribbon-consumer", method = RequestMethod.GET)
public String helloConsumer(){
return helloService.helloService();
}
}
下面,对断路器实现的服务回调逻辑进行验证,重新启动之前关闭的 1112 端口的 hello-service,确保此时服务注册中心、两个 hello-service 和 ribbon-consumer 均已启动,再次访问 http://localhost:1114/ribbon-consumer 可以轮询两个 hello-serive 并返回一些文字信息。此时断开其中任意一个端口的 hello-service,再次访问,当轮询到关闭的端口服务时,输出内容为 error ,不再是之前的提示信息。
除了通过断开具体的服务实例来模拟某个节点无法访问的情况之外,还可以模拟一下服务阻塞(长时间未响应)的情况。下面对hello-serive 的 /index 接口做一些修改,具体如下:
@RestController
public class HelloController {
private final Logger logger = Logger.getLogger(getClass());
@Autowired
private DiscoveryClient client;
@RequestMapping(value = "/index")
public String index(){
ServiceInstance instance = client.getLocalServiceInstance();
// 让处理线程等待几秒钟
int sleepTime = new Random().nextInt(3000);
logger.info("sleepTime:"+sleepTime);
try {
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
logger.info("/hello:host:"+instance.getHost()+" port:"+instance.getPort()
+" service_id:"+instance.getServiceId());
return "hello world!";
}
}
通过Thread.sleep 函数可让 /index 接口的处理线程不是马上返回内容,而是在阻塞几秒后才返回内容。由于 Hystrix 默认超时时间为 2000 毫秒,所以这里采用了 0 至 3000 的随机数以让处理过程有一定概率发生超时来触发断路器。为了更精确的观察断路器的触发,在消费者调用函数中做一些时间记录,具体如下:
@Service
public class HelloService {
@Autowired
RestTemplate restTemplate;
@HystrixCommand(fallbackMethod = "helloFallback")
public String helloService(){
long beginTime = System.currentTimeMillis();
String body = restTemplate.getForEntity("http://hello-service/index",
String.class).getBody();
long endTime = System.currentTimeMillis();
System.out.println("Spend Time : "+ (endTime - beginTime));
return body;
}
public String helloFallback(){
return "error";
}
}
重新启动,可以发现,当随机数超过2000,便会返回error。即服务消费者因调用得服务超时从而触发熔断请求。并调用回调逻辑返回结果。
原理分析
通过上面得快速入门示例,我们对Hystrix得使用场景和使用方法已经有了一个基础得认识。接下来我们通过解读Netflix Hystrix官方得流程图来详细了解一下:当一个请求调用了相关依赖之后Hystrix是如何工作得。
工作流程
下面我们根据图中标记得数字顺序来解决每一个环节得详细内容。
1. 创建 HystrixCommand 或 HystrixObservableCommand 对象
首先,创建一个 HystrixCommand 或 HystrixObservableCommand 对象,用来表示对依赖服务的操作请求,同时传递所有需要的参数。从其命名中我们就能知道它采用了“命令模式” 来实现服务调用操作的封装。而这两个 Command 对象分别针对不同的应用场景。
- HystrixCommand :用在依赖的服务返回单个操作结果的时候。
- HystrixObservableCommand :用在依赖的服务返回多个操作结果的时候。
命令模式,将来自客户端的请求封装成一个对象,从而让你可以使用不同的请求对客户端进行参数化。它可以被用于实现“行为请求者” 与 “行为实现者” 的解耦,以便使两者可以适应变化。下面的示例是对命令模式的简单实现:
package com.example.demo.command;
// 接收者
public class Receiver {
public void active(){
//真正的业务逻辑
System.out.println("测试命令模式");
}
}
package com.example.demo.command;
//抽象命令
public interface Command {
void excute();
}
package com.example.demo.command;
import org.springframework.beans.factory.annotation.Autowired;
//具体命令实现
public class CommandImpl implements Command {
private Receiver receiver;
public CommandImpl(Receiver receiver) {
this.receiver = receiver;
}
@Override
public void excute() {
this.receiver.active();
}
}
package com.example.demo.command;
//请求者调用
public class Invoker {
private Command command;
public void setCommand(Command command) {
this.command = command;
}
public void active (){
command.excute();
}
}
package com.example.demo.command;
// 客户端调用
public class Client {
public static void main(String[] args) {
Receiver receiver = new Receiver();
Command command = new CommandImpl(receiver);
Invoker invoker = new Invoker();
invoker.setCommand(command);
invoker.active(); //客户端通过调用者来执行命令
}
}
从代码中,可以看到这样几个对象。
- Receiver:接收者,它知道如何处理具体的业务逻辑。
- Command:抽象命令,它定义了一个命令对象应具备的一系列命令操作,比如 execute 等。当命令操作被调用的时候就会触发接收者去做具体命令对应的业务逻辑。
- CommandImpl:具体的命令实现,在这里它绑定了命令操作与接收者之间的关系,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.example.demo.Observable_Subsciber;
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() { }
@Override
public void onError(Throwable throwable) { }
@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() 的返回对象。其中 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() 抛出异常的时候。
在服务降级的逻辑中,我们需要实现一个通用的响应结果。并且该结果的处理逻辑应当是从缓存或是根据一些静态逻辑来获取,而不是依赖网络请求获取。如果一定要在降级逻辑中包含网络请求,那么该请求也应该被包装在 HystrixCommand 或者是 HystrixObservableCommand 中,从而形成级联的降级策略,而最终的降级策略一定不是一个依赖网络请求的。而是一个能稳定返回结果的处理逻辑。
9、返回成功的响应