在微服务架构中,我们将系统拆分成了很多服务单元,各单元的应用间通过服务注册与订阅的方式互相依赖。由于每个单元都在不同的进程中运行,依赖通过远程调用的方式执行,这样就有可能因为网络原因或是依赖服务自身问题出现调用故障或延迟,而这些问题会直接导致调用方的对外服务也出现延迟,若此时调用方的请求不断增加,最后就会因等待出现故障的依赖方响应形成任务积压,最终导致自身服务的瘫痪。
举个例子,在一个电商网站中,我们可能会将系统拆分成用户、订单、库存、积分、评论等一系列服务单元。用户创建一个订单的时候,客户端将调用订单服务的创建订单接口,此时创建订单接口又会向库存服务来请求出货(判断是否有足够库存来出货)。此时若库存服务因自身处理逻辑等原因造成响应缓慢,会直接导致创建订单服务的线程被挂起,以等待库存申请服务的响应,在漫长的等待之后用户会因为请求库存失败而得到创建订单失败的结果。如果在高并发情况之下,因这些挂起的线程在等待库存服务的响应而未能释放,使得后续到来的创建订单请求被阻塞,最终导致订单服务也不可用。
在微服务架构中,存在着那么多的服务单元,若一个单元出现故障,就很容易因依赖关系而引发故障的蔓延,最终导致整个系统的瘫痪,这样的架构相较传统架构更加不稳定。为了解决这样的问题,产生了断路器等一系列的服务保护机制。
断路器模式源于Martin Fowler的Circuit Breaker一文。“断路器”本身是一种开关装置, 用于在电路上保护线路过载,当线路中有电器发生短路时,“断路器”能够及时切断故障电路,防止发生过载、发热甚至起火等严重后果。
在分布式架构中,断路器模式的作用也是类似的,当某个服务单元发生故障(类似用电器发生短路)之后,通过断路器的故障监控( 类似熔断保险丝),向调用方返回一个错误响应,而不是长时间的等待。这样就不会使得线程因调用故障服务被长时间占用不释放,避免了故障在分布式系统中的蔓延。
针对上述问题,Spring Cloud Hystrix实现了断路器、线程隔离等一系列服务保护功能。它也是基于Netflix的开源框架Hystrix 实现的,该框架的目标在于通过控制那些访问远程系统、服务和第三方库的节点,从而对延迟和故障提供更强大的容错能力。Hystrix 具备服务降级、服务熔断、线程和信号隔离、请求缓存、请求合并以及服务监控等强大功能。接下来,我们就从一个简单示例开始对Spring Cloud Hystrix的学习与使用。
快速入门
在开始使用Spring Cloud Hystrix实现断路器之前,我们先用之前实现的一些内容作为基础,构建一个如下图架构所示的服务调用关系。
需要启动的工程有:
- eureka-server工程:服务注册中心。
- eureka-client工程:具体服务的提供者者。
- ribbon-consumer工程:使用Ribbon实现的服务消费者
服务注册中心
spring:
application:
name: eureka-server
server:
port: 1111
eureka:
instance:
hostname: eureka-server
client:
register-with-eureka: false
fetch-registry: false
service-url:
defaultZone: http://127.0.0.1:1111/eureka/
@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
服务提供者
spring:
application:
name: eureka-client
server:
port: 2222
eureka:
client:
serviceUrl:
defaultZone: http://127.0.0.1:1111/eureka/
@EnableDiscoveryClient
@SpringBootApplication
public class EurekaClientApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaClientApplication.class, args);
}
}
@RestController
public class HelloController {
private final Logger logger = LoggerFactory.getLogger(HelloController.class);
@Autowired
private DiscoveryClient client;
@GetMapping(value = "/hello")
public String index() {
List<String> list = client.getServices();
listToString(list);
return "hello world";
}
private void listToString(List<String> list) {
for (String str : list) {
logger.info("str: {}", str);
}
}
}
服务消费中心
spring:
application:
name: ribbon-consumer
server:
port: 9000
eureka:
client:
serviceUrl:
defaultZone: http://127.0.0.1:1111/eureka/
@EnableDiscoveryClient
@SpringBootApplication
public class ConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
@Bean
@LoadBalanced
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}
@RestController
public class ConsumerController {
@Autowired
RestTemplate restTemplate;
@RequestMapping(value = "/ribbon-consumer", method = RequestMethod.GET)
public String helloConsumer() {
return restTemplate.getForEntity("http://eureka-client/hello", String.class).getBody();
}
}
在未加入断路器之前,关闭1111的实例,发送GET请求到http://localhost:9000/ribbon-consumer,可以获得下面的输出:
java.net.ConnectException: Connection refused: connect
下面我们开始引入Spring Cloud Hystrix。
- 在ribbon-consumer工程的pom.xml的dependency 节点中引入spring cloud hystrix依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
- 在ribbon-consumer工程的主类ConsumerApplication 中使用@EnableCircuitBreaker注解开启断路器功能:
@EnableCircuitBreaker
@EnableDiscoveryClient
@SpringBootApplication
public class ConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
@Bean
@LoadBalanced
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}
注意:这里还可以使用Spring Cloud 应用中的@SpringCloudApplication
注解来修饰应用主类,该注解的具体定义如下所示。可以看到,该注解中包含了上述我们所引用的三个注解,这也意味着一个Spring Cloud 标准应用应包含服务发现以及断路器。
@Deprecated
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootApplication
@EnableDiscoveryClient
public @interface SpringCloudApplication {
}
- 改造服务消费方式,新增HelloService类,注入RestTemplate实例。然后,将在ConsumerController中对RestTemplate的使用迁移到helloService函数中,最后,在helloService函数.上增加@Hystri xCommand注解来指定回调方法:
@Service
public class HelloService {
@Autowired
private RestTemplate restTemplate;
@HystrixCommand(fallbackMethod = "helloFallBack")
public String helloService() {
return restTemplate.getForEntity("http://eureka-client/hello", String.class).getBody();
}
public String helloFallBack() {
return "Error";
}
}
- 修改ConsumerController 类,注入上面实现的HelloService实例,并在
helloConsumer中进行调用:
@RestController
public class ConsumerController {
@Autowired
private HelloService helloService;
@RequestMapping(value = "/ribbon-consumer", method = RequestMethod.GET)
public String helloConsumer() {
return helloService.helloService();
}
}
下面,我们来验证一下通过断路器实现的服务回调逻辑,重新启动之前关闭的8081端口的客户端服务,确保此时服务注册中心、服务提供者、服务消费者均已启动,访问http://localhost:9000/ribbon-consumer,此时我们继续断开8081的端口服务,然后访问http://localhost:9000/ribbon-consumer,输出内容为error,不再是之前的错误内容,Hystrix的服务回调生效。除了通过断开具体的服务实例来模拟某个节点无法访问的情况之外,我们还可以模拟一下服务阻塞(长时间未响应)的情况。我们对/hello接口做一些修改,具体如下:
@RestController
public class ConsumerController {
private final Logger logger = LoggerFactory.getLogger(ConsumerController.class);
@Autowired
private HelloService helloService;
@RequestMapping(value = "/ribbon-consumer", method = RequestMethod.GET)
public String helloConsumer() throws InterruptedException {
int sleepTime = new Random().nextInt(3000);
logger.info("sleepTime: {}", sleepTime);
Thread.sleep(sleepTime);
return helloService.helloService();
}
}
通过Thread. sleep ()函数可让/hello接口的处理线程不是马上返回内容,而是在阻塞几秒之后才返回内容。由于Hystrix默认超时时间为2000毫秒,所以这里采用了0至3000的随机数以让处理过程有一定概率发生超时来触发断路器。
服务消费者因调用的服务超时从而触发熔断请求,并调用回调逻辑返回结果。
原理分析
通过上面的快速入门示例,我们对Hystrix的使用场景和使用方法已经有了一一个基础的认识。接下来我们通过解读Netix Hystrix官方的流程图来详细了解一下:当一个请求调用了相关服务依赖之后Hystix是如何工作的。
工作流程
下面我们根据图中标记的数字顺序来解释每一个环 节的详细内容。
1. 创建HystrixCommand或HystrixObservableCommand对象
首先,构建一个HystrixCommand
或是HystrixObservableCommand
对象,用来表示对依赖服务的操作请求,同时传递所有需要的参数。从其命名中我们就能知道它采了“命令模式”来实现对服务调用操作的封装。而这两个Command对象分别针对不同的应用场景。
HystrixCommand
:用在依赖的服务返回单个操作结果的时候。HystrixObservableCommand
:用在依赖的服务返回多个操作结果的时候。
命令模式,将来自客户端的请求封装成-一个对象,从而让你可以使用不同的请求对客户端进行参数化。它可以被用于实现“行为请求者”与“行为实现者”的解耦,以便使两者可以适应变化。下面的示例是对命令模式的简单实现:
/**
* 命令接口,声明执行的操作
*/
public interface Command {
/**
* 执行命令对应的操作
*/
void execute();
}
/**
* 具体的命令实现对象
*/
public class ConcreteCommand implements Command {
/**
* 持有相应的接收者对象
*/
private final Receiver receiver;
/**
* 示意,命令对象可以有自己的状态
*/
private String state;
public ConcreteCommand(Receiver receiver) {
this.receiver = receiver;
}
/**
* 执行命令对应的操作
*/
public void execute() {
/*
通常会转调接收者对象相应的方法,让接收者来真正执行功能
*/
receiver.action();
}
}
/**
* 命令调用者
*/
public class Invoker {
/**
* 持有命令对象
*/
private Command command;
public void setCommand(Command command) {
this.command = command;
}
/**
* 示意方法,要求命令执行请求
*/
public void runCommand() {
command.execute(); // 调用命令对象的执行方法
}
}
/**
* 接收者对象
*/
public class Receiver {
private final Logger logger = Logger.getLogger(Receiver.class.getName());
/**
* 执行命令
*/
public void action() {
logger.info("接收者对象执行命令");
}
}
public class Client {
public static void assemble() {
Receiver receiver = new Receiver(); // 创建接收者
Command command = new ConcreteCommand(receiver); // 创建命令对象,设置它的接收者
Invoker invoker = new Invoker(); // 创建调用者,把命令对象设置进去
invoker.setCommand(command);
invoker.runCommand(); // 调用命令
}
public static void main(String[] args) {
assemble();
}
}
- Receiver:接收者,它知道如何处理具体的业务逻辑。
- Command:抽象命令,它定义了一个命令对象应具备的一系列命令操作,比如execute().undo()、redo()等。当命令操作被调用的时候就会触发接收者去做具体命令对应的业务逻辑。
- ConcreteCommand:具体的命令实现,在这里它绑定了命令操作与接收者之间的关系,execute ()命令的实现委托给了Receiver的action()函数。
- Invoker:调用者,它持有一个命令对象,并且可以在需要的时候通过命令对象完成具体的业务逻辑。
从上面的示例中,我们可以看到,调用者Invoker与操作者Receiver通过Command命令接口实现了解耦。对于调用者来说,我们可以为其注入多个命令操作,比如新建文件、复制文件、删除文件这样三个操作,调用者只需在需要的时候直接调用即可,而不需要知道这些操作命令实际是如何实现的。而在这里所提到HystrixCommand 和HystrixobservableCommand则是在Hystrix中对Command的进一步抽象定义。 在后续的内容中,会逐步展开介绍它的部分内容来帮助理解其运作原理。
从上面的示例中我们也可以发现,Invoker和Receiver的关系非常类似于“请求-响应”模式,所以它比较适用于实现记录日志、撤销操作、队列请求等。
在下面这些情况下应考虑使用命令模式。
- 使用命令模式作为“回调(CallBack)”在面向对象系统中的替代。“CallBack"讲的便是先将一个函数登记上,然后在以后调用此函数。
- 需要在不同的时间指定请求、将请求排队。一个命令对象和原先的请求发出者可以有不同的生命期。换言之,原先的请求发出者可能已经不在了,而命令对象本身仍然是活动的。这时命令的接收者可以是在本地,也可以在网络的另外一个地址。命令对象可以在序列化之后传送到另外一台机器上去。
- 系统需要支持命令的撤销。命令对象可以把状态存储起来,等到客户端需要撤销命令所产生的效果时,可以调用undo()方法,把命令所产生的效果撤销掉。命令对象还可以提供redo()方法,以供客户端在需要时再重新实施命令效果。
- 如果要将系统中所有的数据更新到日志里,以便在系统崩溃时,可以根据日志读回所有的数据更新命令,重新调用execute()方法一条一条执行这些命令, 从而恢复系统在崩溃前所做的数据更新。
2. 命令执行
从图中我们可以看到一共存在4种命令的执行方式,而Hystrix在执行时会根据创的Command对象以及具体的情况来选择一个执行。其中HystrixCommand实现了下面两个执行方式。
- execute():同步执行,从依赖的服务返回一个单一的结果对象,或是在发生错误
的时候抛出异常。 - queue():异步执行,直接返回一个Future对象,其中包含了服务执行结束时要返回的单一结果对象。
/**
* Used for synchronous execution of command.
*
* @return R
* Result of {@link HystrixCommand} execution
* @throws HystrixRuntimeException
* if an error occurs and a fallback cannot be retrieved
* @throws HystrixBadRequestException
* if the {@link HystrixCommand} instance considers request arguments to be invalid and needs to throw an error that does not represent a system failure
*/
public R execute();
/**
* Used for asynchronous execution of command.
* <p>
* This will queue up the command on the thread pool and return an {@link Future} to get the result once it completes.
* <p>
* NOTE: If configured to not run in a separate thread, this will have the same effect as {@link #execute()} and will block.
* <p>
* We don't throw an exception in that case but just flip to synchronous execution so code doesn't need to change in order to switch a circuit from running a separate thread to the calling thread.
*
* @return {@code Future<R>} Result of {@link HystrixCommand} execution
* @throws HystrixRuntimeException
* if an error occurs and a fallback cannot be retrieved
* @throws HystrixBadRequestException
* if the {@link HystrixCommand} instance considers request arguments to be invalid and needs to throw an error that does not represent a system failure
*/
public Future<R> queue();
而HystrixObservableCommand实现了另外两种执行方式。
- observe(): 返回Observable对象,它代表了操作的多个结果,它是一个HotObservable。
- toobservable():同样会返回Observable对象,也代表了操作的多个结果,但它返回的是一个Cold Observable。
/**
* Used for asynchronous execution of command with a callback by subscribing to the {@link Observable}.
* <p>
* This eagerly starts execution of the command the same as {@link HystrixCommand#queue()} and {@link HystrixCommand#execute()}.
* <p>
* A lazy {@link Observable} can be obtained from {@link #toObservable()}.
* <p>
* See https://github.com/Netflix/RxJava/wiki for more information.
*
* @return {@code Observable<R>} that executes and calls back with the result of command execution or a fallback if the command fails for any reason.
* @throws HystrixRuntimeException
* if a fallback does not exist
* <p>
* <ul>
* <li>via {@code Observer#onError} if a failure occurs</li>
* <li>or immediately if the command can not be queued (such as short-circuited, thread-pool/semaphore rejected)</li>
* </ul>
* @throws HystrixBadRequestException
* via {@code Observer#onError} if invalid arguments or state were used representing a user failure, not a system failure
* @throws IllegalStateException
* if invoked more than once
*/
public Observable<R> observe() {
// us a ReplaySubject to buffer the eagerly subscribed-to Observable
ReplaySubject<R> subject = ReplaySubject.create();
// eagerly kick off subscription
final Subscription sourceSubscription = toObservable().subscribe(subject);
// return the subject that can be subscribed to later while the execution has already started
return subject.doOnUnsubscribe(new Action0() {
@Override
public void call() {
sourceSubscription.unsubscribe();
}
});
}
/**
* Used for asynchronous execution of command with a callback by subscribing to the {@link Observable}.
* <p>
* This lazily starts execution of the command once the {@link Observable} is subscribed to.
* <p>
* An eager {@link Observable} can be obtained from {@link #observe()}.
* <p>
* See https://github.com/ReactiveX/RxJava/wiki for more information.
*
* @return {@code Observable<R>} that executes and calls back with the result of command execution or a fallback if the command fails for any reason.
* @throws HystrixRuntimeException
* if a fallback does not exist
* <p>
* <ul>
* <li>via {@code Observer#onError} if a failure occurs</li>
* <li>or immediately if the command can not be queued (such as short-circuited, thread-pool/semaphore rejected)</li>
* </ul>
* @throws HystrixBadRequestException
* via {@code Observer#onError} if invalid arguments or state were used representing a user failure, not a system failure
* @throws IllegalStateException
* if invoked more than once
*/
public Observable<R> toObservable() {
final AbstractCommand<R> _cmd = this;
// doOnCompleted handler already did all of the SUCCESS work
// doOnError handler already did all of the FAILURE/TIMEOUT/REJECTION/BAD_REQUEST work
final Action0 terminateCommandCleanup = new Action0() {
@Override
public void call() {
if (_cmd.commandState.compareAndSet(CommandState.OBSERVABLE_CHAIN_CREATED, CommandState.TERMINAL)) {
handleCommandEnd(false); //user code never ran
} else if (_cmd.commandState.compareAndSet(CommandState.USER_CODE_EXECUTED, CommandState.TERMINAL)) {
handleCommandEnd(true); //user code did run
}
}
};
// mark the command as CANCELLED and store the latency (in addition to standard cleanup)
final Action0 unsubscribeCommandCleanup = new Action0() {
@Override
public void call() {
if (_cmd.commandState.compareAndSet(CommandState.OBSERVABLE_CHAIN_CREATED, CommandState.UNSUBSCRIBED)) {
if (!_cmd.executionResult.containsTerminalEvent()) {
_cmd.eventNotifier.markEvent(HystrixEventType.CANCELLED, _cmd.commandKey);
try {
executionHook.onUnsubscribe(_cmd);
} catch (Throwable hookEx) {
logger.warn("Error calling HystrixCommandExecutionHook.onUnsubscribe", hookEx);
}
_cmd.executionResultAtTimeOfCancellation = _cmd.executionResult
.addEvent((int) (System.currentTimeMillis() - _cmd.commandStartTimestamp), HystrixEventType.CANCELLED);
}
handleCommandEnd(false); //user code never ran
} else if (_cmd.commandState.compareAndSet(CommandState.USER_CODE_EXECUTED, CommandState.UNSUBSCRIBED)) {
if (!_cmd.executionResult.containsTerminalEvent()) {
_cmd.eventNotifier.markEvent(HystrixEventType.CANCELLED, _cmd.commandKey);
try {
executionHook.onUnsubscribe(_cmd);
} catch (Throwable hookEx) {
logger.warn("Error calling HystrixCommandExecutionHook.onUnsubscribe", hookEx);
}
_cmd.executionResultAtTimeOfCancellation = _cmd.executionResult
.addEvent((int) (System.currentTimeMillis() - _cmd.commandStartTimestamp), HystrixEventType.CANCELLED);
}
handleCommandEnd(true); //user code did run
}
}
};
final Func0<Observable<R>> applyHystrixSemantics = new Func0<Observable<R>