在微服务架构中,我们将系统拆分为很多个服务,各个服务之间通过注册与订阅的方式相互依赖,由于每个单元都是在各自的进程中运行,依赖通过远程调用的方式执行,就有可能由于网络原因或者服务自身的问题导致调用故障或延迟,进而出现一些列问题。为了解决这一系列的问题,断路器等一系列服务保护机制出现了。
在微服务架构中,存在很多单元,若其中一个单元出现故障,容易因为依赖关系而引发故障的蔓延,最终导致系统瘫痪,为了解决这些问题,产生了断路器等一系列服务保护机制。
Spring Cloud Hystrix实现了断路器、线程隔离等一系列服务保护功能,该框架的目标在于通过控制那些访问远程系统、服务和第三方库的节点,从而对延迟和故障提供更强大的容错能力。Hystrix 具备服务降级、服务熔断、线程和信号隔离、请求缓存、请求合并以及服务监控等强大功能。
快速入门
在开始使用Spring Cloud Hystix实现断路器之前,我们先用之前实现的一些内容作为基础,构建一个如下图架构所示的服务调用关系.
启动的工程如下:
- eureka-server 工程:服务注册中心,端口 8761.
- hello-service 工程:两个启动的实例端口分别为8081 和 8082.
- ribbon-consume 工程:服务消费者,端口9000
在未加入断路器之前,关闭 8081 的实例,发送GET请求到 http://localhost:9000/ribbon-consumer/ 可以获得以下输出
This application has no explicit mapping for /error, so you are seeing this as a fallback.
Thu Jan 03 13:18:53 CST 2019
There was an unexpected error (type=Internal Server Error, status=500).
I/O error on GET request for "http://HELLO-SERVICE/hello": Connection refused: connect; nested exception is java.net.ConnectException: Connection refused: connect
下面引入 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 注解开启断路器功能:
@EnableCircuitBreaker
@EnableDiscoveryClient
@SpringBootApplication
public class RibbonConsumerApplication {
@LoadBalanced
@Bean
RestTemplate restTemplate(){
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(RibbonConsumerApplication.class, args);
}
}
- 改造服务消费方式,新增 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/index",
String.class).getBody();
}
public String helloFallback(){
return "error";
}
}
- 修改 ConsumerController 类, 注入上面实现的 HelloService 实例,并在 helloConsumer 中进行调用:
package com.gildata.ribbonconsumer.web;
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;
/**
* Created by liaock on 2019/1/3.
*/
@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";
}
}
下面,对断路器实现的服务回调逻辑进行验证,重新启动之前关闭的 8081 端口的 hello-service,确保此时服务注册中心、两个 hello-service 和 ribbon-consumer 均已启动,再次访问 http://localhost:8761/ribbon-consumer 可以轮询两个 hello-serive 并返回一些文字信息。此时断开其中任意一个端口的 hello-service,再次访问,当轮询到关闭的端口服务时,输出内容为 error
另外Hystrix 默认超时时间为 2000 毫秒,超过这个响应时间会触发断路器,也返回error,这个可以通过添加Thread.sleep 函数来进行测试.
原理分析
工作流程
1.创建 HystrixCommand 或 HystrixObservableCommand 对象
首先,创建一个 HystrixCommand 或 HystrixObservableCommand 对象,用来表示对依赖服务的操作请求,同时传递所有需要的参数。从其命名中我们就能知道它采用了“命令模式” 来实现服务调用操作的封装。而这两个 Command 对象分别针对不同的应用场景。
- HystrixCommand :用在依赖的服务返回单个操作结果的时候。
- HystrixObservableCommand :用在依赖的服务返回多个操作结果的时候。
命令模式,将来自客户端的请求封装成一个对象,从而让你可以使用不同的请求对客户端进行参数化。它可以被用于实现“行为请求者” 与 “行为实现者” 的解耦,以便使两者可以适应变化。下面的示例是对命令模式的简单实现:
// 接收者
public class Receiver {
public void active(){
//真正的业务逻辑
System.out.println("测试命令模式");
}
}
//抽象命令
public interface Command {
void excute();
}
//具体命令实现
public class CommandImpl implements Command {
private Receiver receiver;
public CommandImpl(Receiver receiver) {
this.receiver = receiver;
}
@Override
public void excute() {
this.receiver.active();
}
}
//客户端调用
public class Invoker {
private Command command;
public void setCommand(Command command) {
this.command = command;
}
public void active (){
command.excute();
}
}
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 对象,其中包含了服务执行结束时要返回的单一结果对象。
而 HystrixObservableCommand 实现了另外两种执行方式。 -
observe():返回 Observable 对象,它代表了操作的多个结果,它是一个 HotObservable。
-
toObservable():同样返回 Observable 对象,也代表了操作的多个结果,但它返回的是一个 Cold Observable。
- 结果是否被缓存
若当前命令的请求缓存功能是被启用的,并且该命令缓存命中,那么缓存的结果会立即以 Observable 对象的形式返回。
- 断路器是否打开
在命令结果没有缓存命中的时候,Hystrix 在执行命令前需要检查断路器是否为打开状态:
打开:Hystrix不执行命令,转到 fallback 处理逻辑(对应下面第8步)。
关闭:Hystrix 跳到第5步,检查是否有可用资源来执行命令。
5. 线程池 / 请求队列 / 信息量是否占满
如果与命令相关的线程池 / 请求队列 / 信息量已经占满,那么 Hystrix 不会执行命令,跳转到 fallback 处理逻辑(对应下面第8步)。
注意:此处的线程池并非容器的线程池,而是每个依赖服务的专有线程池。Hystrix 为了保证不会因为某个依赖服务的问题影响到其他依赖服务而采用了 “舱壁模式” 来隔离每个依赖的服务。
- 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、返回成功的响应