Spring Cloud Hystrix服务容错保护入门

前言

在微服务中,系统由许多服务单元构成,各单元的应用间通过服务注册与订阅的方式互相依赖。由于每个单元都在不同的进程中进行,依赖通过远程调用的方式执行,这样就有可能因为网络原因或者依赖服务自身问题出现调用故障或延迟,这些问题会直接导致调用方的对外服务也出现延迟,若此时调用方的请求不断增加,最后就会因等待出现故障的依赖方响应而形成任务积压,最终导致自身服务瘫痪。

由于微服务中存在众多服务单元,若一个单元出现故障,就很容易因依赖关系而引发故障的蔓延,最终导致整个系统瘫痪。未解决这样的问题,产生了断路器等一系列服务保护机制,而 Spring Cloud Hystrix 就是其中之一。

Spring Cloud Hystrix 基于 Netflix 开源框架 Hystrix 实现,具备 服务降级、服务熔断、线程和信号隔离、请求缓存、请求合并以及服务监控等强大功能。

接下来,我们从一个示例来了解它的使用及原理。

正文

由于内容较多,这篇文章我们先来了解下 Hystrix 的一个简单例子,然后再来了解两种模式:命令模式 和 观察者-订阅者模式。

Hystrix示例

在Eureka和Ribbon的例子里,我们创建了 eureka-server、sakura-service、sakura-consumer服务,简单实现了微服务的调用。

PS:详见此文章Eureka简介及简单使用

我们启动3个 eureka-server(高可用服务注册中心)端口号为8001、8002、8003,启动2个sakura-service 模拟服务提供方,端口号为9001、9002,启动1个sakura-service 模拟服务提供方,端口号7001。

正常情况下,我们访问 http://host:port/consumer 会提示HelloWorld信息,如下图。

upload successful

现在我们模拟服务提供方部分服务故障,停掉 sakura-service 的一个节点 9002 ,再次访问 http://host:port/consumer ,有时候正常,有时候会出现如下错误。

upload successful

同时反应缓慢,因为需要等到我们这边服务调用超时,这种错误信息当调用方请求不断增加后,易出现任务积压,从而导致调用方对外服务也变得不可用。

下面我们引入 Spring Cloud Hystrix。

  • 在 sakura-consumer 工程的 pom.xmldependency 节点中引入 spring-cloud-starter-netflix-hystrix依赖。

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
    </dependency>
  • 在 sakura-consumer 工程的主类SakuraConsumerApplication中使用@EnableCircuitBreaker开启断路器功能。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @EnableDiscoveryClient
    @SpringBootApplication
    @EnableCircuitBreaker
    @RibbonClient(name = "sakura")
    public class SakuraConsumerApplication {

    public static void main(String[] args) {
    SpringApplication.run(SakuraConsumerApplication.class, args);
    }

    @Bean
    @LoadBalanced
    RestTemplate restTemplate(){
    return new RestTemplate();
    }
    }
  • 改造服务消费方式,我们新增SakuraService类,注入RestTemplate。然后在SakuraController中注入SakuraService,并对其方法hello进行调用,同时,在hello方法上增加@HystrixCommand注解来指定回调方法。如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @Service
    public class SakuraService {
    @Autowired
    private RestTemplate restTemplate;

    @HystrixCommand(fallbackMethod = "helloFallback")
    public String hello(){
    return restTemplate.getForEntity("http://SAKURA-SERVICE/hello",String.class).getBody();
    }

    public String helloFallback(){
    return "error";
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @RestController
    public class SakuraController {

    @Autowired
    private SakuraService sakuraService;

    @RequestMapping(value = "/consumer",method = RequestMethod.GET)
    public String hello(){
    return sakuraService.hello();
    }
    }

这时候我们启动 sakura-consumer,同时停掉 sakura-service 的一个节点 9002 ,再次访问 http://host:port/consumer ,我们多次访问,可以看到有时候正常,有时候提示如下:

正常显示:

upload successful

访问到服务异常的实例后提示:

upload successful

上面的信息说明了当consumer访问到异常的实例后,不再返回之前的错误内容,而输出内容为error,也就是我们指定的 Hystrix 服务回调,即helloFallback里的内容。

这种是属于服务异常的情况,我们也可以模拟服务请求阻塞(长时间无响应)的情况,我们来看一下:

我们对sakura-service进行部分修改,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@RestController
public class SakuraController {

// 服务注册
@Qualifier("eurekaRegistration")
@Autowired
private Registration registration;

@RequestMapping(value = "/hello",method = RequestMethod.GET)
public String index() throws Exception{
int time = new Random().nextInt(3000);
System.out.println("sleepTime:"+time);
Thread.sleep(time);
System.out.println("Host:"+registration.getHost()
+";ServiceID:"+registration.getServiceId()
+";port:"+registration.getPort()
);
return "Hello World";
}

}

使其收到请求后随机0~3000ms在进行处理,而后重新启动 sakura-service (9001、9002)。

我们通过全局配置,在application.properties文件里设置 Hystrix 超时时间为 2000ms。

1
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=2000

然后我们调用 http://host:port/consumer ,可以看到当响应时间在 2000ms 内时,输出 Hello World,否则输出 error。

upload successful

上面就是服务消费者因调用的服务超时从而触发熔断请求,并调用回调逻辑返回结果。

在了解 Hystrix 原理及详情之前,我们需要先对一下部分内容有所熟悉。

命令模式

命令模式,它可以将来自客户端的请求封装成一个对象,从而让我们可以使用不同的请求对客户端进行参数化。它可以被用于实现“行为请求者”与“行为实现者”的解耦,以便使两者可以适应变化。

我们通过代码来看下命令模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
//命令接收者
public class Receiver {
public void action(){
//真正的业务逻辑
System.out.println("12345678");
}
}

//命令接口
public interface Command {
void execute();
}

//具体命令实现
public class ConcreteCommand implements Command {
private Receiver receiver;

public ConcreteCommand(Receiver receiver) {
this.receiver = receiver;
}

@Override
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();
}
}

上述代码对象示意如下:

  • Receiver:接收者,它知道如何处理具体的业务逻辑。
  • Command:抽象命令,它定义了一个命令对象应具备的一系列命令操作,如executeundoredo等。当命令操作被调用的时候就会触发接收者去做具体命令对应的业务逻辑。
  • ConcreteCommand:具体的命令实现,在这里它绑定了命令操作与接收者之间的关系,execute命令的实现委托给了Receiveraction函数。
  • Invoker:调用者,它持有一个命令对象,并且可以在需要的时候通过命令对象完成具体的业务逻辑。

从上面的例子我们可以看到,调用者Invoker与操作者Receiver通过Command命令接口实现了解耦。

对于调用者来说,我们可以为其注入多个命令操作,比如新建文件、复制文件、删除文件等操作,调用者只需在需要的时候调用即可,而不需要知道这些操作命令实际是如何实现的。

上面的示例中,我们可以发现,InvokerReceiver的关系非常类似于“请求-响应”模式,所以它比较适用于实现记录日志、撤销操作、队列请求等。

下面这些情况下应考虑使用命令模式。

  • 使用命令模式作为“回调(Callback)”在面向对象系统中的替代。“Callback”讲的便是先将一个函数登记上,然后在以后在调用此函数。
  • 需要在不同的时间指定请求、将请求排队。一个命令对象和原先的请求发出者可以有不同的生命期。换言之,原先的请求发出者可能已经不在了,而命令对象本身仍然是活动的。这时命令的接收者可以是在本地,也可以在网络的另一个地址。命令对象可以在序列化之后传送到另一台机器上去。
  • 系统需要支持命令的撤销。命令对象可以把状态存储起来,等到客户端需要撤销命令所产生的效果时,可以调用undo()方法,把命令所产生的效果撤销掉。命令对象还可以提高redo()方法,以供客户端在需要时再重新实施命令效果。
  • 如果要将系统中所有的数据更新到日志里,以便在系统崩溃是,可以根据日志读回所有的数据更新命令,重新调用execute()方法一条一条执行这些命令,从而恢复系统在崩溃前所做的数据更新。

在 Hystrix 中,共有两种命令对象,如下:

  • HystrixCommand : 用在依赖的服务返回单个操作结果的时候。
  • HystrixObservableCommand : 用在依赖的服务返回多个操作结果的时候。

上面两个命令对象分别有两种命令执行方式,如下:

HystrixCommand:

  • execute():同步执行,从依赖的服务返回一个单一的结果对象,或是在发生错误的时候抛出异常。
  • queue():异步执行,直接返回一个 Future 对象,其中包含了服务执行结束时要返回的单一结果对象。
1
2
R value = command.execute();
Future<R> fValue = command.queue();

HystrixObservableCommand:

  • observe():返回Observable对象,它代表了操作的多个结果,它是一个Hot Observable
  • toObservable():同样会返回Observable对象,也代表了操作的多个结果,但它返回的是一个Cold Observable
1
2
Observable<R> ohValue = command.observe();
Observable<R> ocValue = command.toObservable();

在Hystrix底层大量使用了RxJava,我们在介绍Hystrix原理时,先对RxJava的 观察者-订阅者 模式来做一个简单了解。

RxJava 观察者-订阅者模式

上面我们所提到的Observable对象就是RxJava的核心内容之一,可以把它理解为“事件源”或是“被观察者”,与其对应的Subscriber对象,可以理解为“订阅者”或是“观察者”。这两个对象是RxJava响应式编程的重要组成部分。

  • Observable用来向订阅者Subscriber对象发布事件,Subscriber对象则在接收到事件后对其进行处理,而在这里所指的的事件通常就是对依赖服务的调用。
  • 一个Observable可以发出多个事件,直到结束或者发生异常。
  • Observable对象每发出一个事件,就会调用对应观察者Subscriber对象的onNext()方法。
  • 每一个Observable的执行,最后一定会通过调用Subscriber.onCompleted()或者Subscriber.onError()来结束该事件的操作流。

我们通过一个例子来看一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class ObservableTest {
public static void main(String[] args) {
//创建事件源observable
Observable<String> observable = Observable.unsafeCreate(new Observable.OnSubscribe<String>() {
@Override
public void call(Subscriber<? super String> subscriber) {
subscriber.onNext("Hello World");
subscriber.onNext("I am sakuratears");
subscriber.onCompleted();
}
});

Subscriber<String> subscriber = new Subscriber<String>() {
@Override
public void onCompleted() {
System.out.println("completed");
}

@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 ObservableCold Observable。其中Hot Observable,它不论“事件源”是否有“订阅者”,都会在创建后对事件进行发布,所以对于Hot Observable的每一个“订阅者”都有可能是从“事件源”的中途开始的,并可能只是看到了整个操作过程的局部过程。而Cold Observable在没有“订阅者”的时候并不会发布事件,而是进行等待,直到有“订阅者”之后才进行发布事件,所以对于Cold Observable的订阅者,它可以保证从一开始看到整个操作的全部过程。

以上就是在熟悉 Hystrix 源码时,我们应该有所了解的部分内容。

我们将在下篇文章详细分析 Hystrix 的一些内容。

总结

本篇文章我们通过一个 Hystrix 的示例,对 Hystrix 有了简单的认识,同时简单介绍了两种模式,在 Hystrix 中,便使用了这两种模式,我们将在下篇文章详细分析,并进一步了解 Hystrix 的一些特点。




-------------文章结束啦 ~\(≧▽≦)/~ 感谢您的阅读-------------

您的支持就是我创作的动力!

欢迎关注我的其它发布渠道