Hystrix简介

1. 概述

典型的分布式系统由许多协作在一起的服务组成。

这些服务容易出现故障或延迟响应。如果服务失败,它可能会影响影响性能的其他服务,并可能使应用程序的其他部分无法访问,或者在最坏的情况下会导致整个应用程序崩溃。

当然,有一些解决方案可以帮助提高应用程序的弹性和容错能力 - 其中一个框架就是Hystrix。

Hystrix框架库通过提供容错和延迟容忍来帮助控制服务之间的交互。它通过隔离故障服务和停止故障的级联效应来提高系统的整体弹性。

在本系列文章中,我们将首先了解Hystrix在服务或系统出现故障时如何解决问题以及Hystrix在这些情况下可以完成的任务。

2. 简单的例子

Hystrix提供故障和延迟容忍的方式是隔离和包装对远程服务的调用。

在这个简单的例子中,我们在HystrixCommandrun()方法中包装一个调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class CommandHelloWorld extends HystrixCommand<String> {

private String name;

CommandHelloWorld(String name) {
super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"));
this.name = name;
}

@Override
protected String run() {
return "Hello " + name + "!";
}
}

我们按如下方式执行调用:

1
2
3
4
@Test
public void givenInputBobAndDefaultSettings_whenCommandExecuted_thenReturnHelloBob(){
assertThat(new CommandHelloWorld("Bob").execute(), equalTo("Hello Bob!"));
}

3. Maven设置

要在Maven项目中使用Hystrix,我们需要在项目pom.xml中使用 Netflix的hystrix-corerxjava-core依赖项:

1
2
3
4
5
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-core</artifactId>
<version>1.5.4</version>
</dependency>

最新版本总是可以在这里找到。

1
2
3
4
5
<dependency>
<groupId>com.netflix.rxjava</groupId>
<artifactId>rxjava-core</artifactId>
<version>0.20.7</version>
</dependency>

可以在此处找到此库的最新版本。

4. 设置远程服务

让我们从模拟现实世界的例子开始。

在下面的示例中RemoteServiceTestSimulator类表示远程服务器上的服务。它有一种方法,在给定的时间段后响应消息。我们可以想象,这种等待模拟了远程系统中耗时的过程,导致对呼叫服务的响应延迟:

1
2
3
4
5
6
7
8
9
10
11
12
13
class RemoteServiceTestSimulator {

private long wait;

RemoteServiceTestSimulator(long wait) throws InterruptedException {
this.wait = wait;
}

String execute() throws InterruptedException {
Thread.sleep(wait);
return "Success";
}
}

这是我们调用RemoteServiceTestSimulator的**示例客户端**。

对服务的调用是隔离的,并包装在HystrixCommandrun()方法中它的这种包装提供了我们上面提到的弹性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class RemoteServiceTestCommand extends HystrixCommand<String> {

private RemoteServiceTestSimulator remoteService;

RemoteServiceTestCommand(Setter config, RemoteServiceTestSimulator remoteService) {
super(config);
this.remoteService = remoteService;
}

@Override
protected String run() throws Exception {
return remoteService.execute();
}
}

通过在RemoteServiceTestCommand对象的实例上调用execute()方法来执行调用。

以下测试演示了如何完成此操作:

1
2
3
4
5
6
7
8
9
10
11
@Test
public void givenSvcTimeoutOf100AndDefaultSettings_whenRemoteSvcExecuted_thenReturnSuccess()
throws InterruptedException {

HystrixCommand.Setter config = HystrixCommand
.Setter
.withGroupKey(HystrixCommandGroupKey.Factory.asKey("RemoteServiceGroup2"));

assertThat(new RemoteServiceTestCommand(config, new RemoteServiceTestSimulator(100)).execute(),
equalTo("Success"));
}

到目前为止,我们已经了解了如何在HystrixCommand对象中包装远程服务调用。在下面的部分中,我们将看看如何处理远程服务开始恶化的情况。

5. 使用远程服务和防御性编程

5.1 具有超时的防御性编程

通常的编程习惯是为远程服务的呼叫设置超时。

让我们首先看看如何在HystrixCommand上设置超时以及它如何通过短路来帮助:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
public void givenSvcTimeoutOf5000AndExecTimeoutOf10000_whenRemoteSvcExecuted_thenReturnSuccess()
throws InterruptedException {

HystrixCommand.Setter config = HystrixCommand
.Setter
.withGroupKey(HystrixCommandGroupKey.Factory.asKey("RemoteServiceGroupTest4"));

HystrixCommandProperties.Setter commandProperties = HystrixCommandProperties.Setter();
commandProperties.withExecutionTimeoutInMilliseconds(10_000);
config.andCommandPropertiesDefaults(commandProperties);

assertThat(new RemoteServiceTestCommand(config, new RemoteServiceTestSimulator(500)).execute(),
equalTo("Success"));
}

在上面的测试中,我们通过将超时设置为500毫秒来延迟服务的响应。我们还将HystrixCommand的执行超时设置为10,000 ms,从而为远程服务提供足够的响应时间。

现在让我们看看当执行超时小于服务超时调用时会发生什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test(expected = HystrixRuntimeException.class)
public void givenSvcTimeoutOf15000AndExecTimeoutOf5000_whenRemoteSvcExecuted_thenExpectHre()
throws InterruptedException {

HystrixCommand.Setter config = HystrixCommand
.Setter
.withGroupKey(HystrixCommandGroupKey.Factory.asKey("RemoteServiceGroupTest5"));

HystrixCommandProperties.Setter commandProperties = HystrixCommandProperties.Setter();
commandProperties.withExecutionTimeoutInMilliseconds(5_000);
config.andCommandPropertiesDefaults(commandProperties);

new RemoteServiceTestCommand(config, new RemoteServiceTestSimulator(15_000)).execute();
}

请注意我们如何降低条形并将执行超时设置为5,000毫秒。

我们期望服务在5,000毫秒内响应,而我们已将服务设置为在15,000毫秒后响应。如果您在执行测试时发现,测试将在5,000毫秒后退出,而不是等待15,000毫秒,并将抛出HystrixRuntimeException

这演示了Hystrix如何等待超过响应的配置超时。这有助于使受Hystrix保护的系统更具响应性。

在下面的部分中,我们将研究设置线程池大小,以防止线程耗尽,我们将讨论它的好处。

**5.2 有限线程池的防御性编程

设置服务调用的超时并不能解决与远程服务相关的所有问题。

当远程服务开始响应缓慢时,典型应用程序将继续调用该远程服务。

应用程序不知道远程服务是否健康,并且每次请求进入时都会生成新线程。这将导致已经在使用的服务器上的线程被使用。

我们不希望这种情况发生,因为我们需要这些线程用于在我们的服务器上运行的其他远程调用或进程,并且我们还希望避免CPU利用率飙升。

让我们看看如何在HystrixCommand中设置线程池大小:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
public void givenSvcTimeoutOf500AndExecTimeoutOf10000AndThreadPool_whenRemoteSvcExecuted
_thenReturnSuccess() throws InterruptedException {

HystrixCommand.Setter config = HystrixCommand
.Setter
.withGroupKey(HystrixCommandGroupKey.Factory.asKey("RemoteServiceGroupThreadPool"));

HystrixCommandProperties.Setter commandProperties = HystrixCommandProperties.Setter();
commandProperties.withExecutionTimeoutInMilliseconds(10_000);
config.andCommandPropertiesDefaults(commandProperties);
config.andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
.withMaxQueueSize(10)
.withCoreSize(3)
.withQueueSizeRejectionThreshold(10));

assertThat(new RemoteServiceTestCommand(config, new RemoteServiceTestSimulator(500)).execute(),
equalTo("Success"));
}

在上面的测试中,我们设置了最大队列大小,核心队列大小和队列拒绝大小。当最大线程数达到10且任务队列大小达到10时,Hystrix将开始拒绝请求。

核心大小是始终在线程池中保持活动状态的线程数。

5.3 短路断路器模式的防御性编程

但是,我们可以对远程服务调用进行改进。

让我们考虑一下远程服务已经开始失败的情况。

我们不想继续向它发出请求并浪费资源。理想情况下,我们希望停止在一定时间内发出请求,以便在恢复请求之前提供恢复服务的时间。这就是所谓的短路断路器模式。

让我们看看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
49
50
51
52
@Test
public void givenCircuitBreakerSetup_whenRemoteSvcCmdExecuted_thenReturnSuccess()
throws InterruptedException {

HystrixCommand.Setter config = HystrixCommand
.Setter
.withGroupKey(HystrixCommandGroupKey.Factory.asKey("RemoteServiceGroupCircuitBreaker"));

HystrixCommandProperties.Setter properties = HystrixCommandProperties.Setter();
properties.withExecutionTimeoutInMilliseconds(1000);
properties.withCircuitBreakerSleepWindowInMilliseconds(4000);
properties.withExecutionIsolationStrategy
(HystrixCommandProperties.ExecutionIsolationStrategy.THREAD);
properties.withCircuitBreakerEnabled(true);
properties.withCircuitBreakerRequestVolumeThreshold(1);

config.andCommandPropertiesDefaults(properties);
config.andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
.withMaxQueueSize(1)
.withCoreSize(1)
.withQueueSizeRejectionThreshold(1));

assertThat(this.invokeRemoteService(config, 10_000), equalTo(null));
assertThat(this.invokeRemoteService(config, 10_000), equalTo(null));
assertThat(this.invokeRemoteService(config, 10_000), equalTo(null));

Thread.sleep(5000);

assertThat(new RemoteServiceTestCommand(config, new RemoteServiceTestSimulator(500)).execute(),
equalTo("Success"));

assertThat(new RemoteServiceTestCommand(config, new RemoteServiceTestSimulator(500)).execute(),
equalTo("Success"));

assertThat(new RemoteServiceTestCommand(config, new RemoteServiceTestSimulator(500)).execute(),
equalTo("Success"));
}

public String invokeRemoteService(HystrixCommand.Setter config, int timeout)
throws InterruptedException {

String response = null;

try {
response = new RemoteServiceTestCommand(config,
new RemoteServiceTestSimulator(timeout)).execute();
} catch (HystrixRuntimeException ex) {
System.out.println("ex = " + ex);
}

return response;
}

在上述测试中,我们设置了不同的断路器属性。最重要的是:

  • CircuitBreakerSleepWindow被设置为4000毫秒。这将配置断路器窗口并定义将恢复对远程服务的请求的时间间隔
  • 所述CircuitBreakerRequestVolumeThreshold其被设置为1,定义了故障率将被认为之前所需的请求的最小数目

在上述设置到位后,我们的HystrixCommand将在两次失败请求后立即打开。即使我们将服务延迟设置为500毫秒,第三个请求甚至不会命中远程服务,Hystrix会短路,我们的方法将返回null作为响应。

我们随后将添加一个Thread.sleep(5000)以跨越我们设置的睡眠窗口的限制。这将导致Hystrix关闭环路,后续请求将成功流过。

6 结论

总之,Hystrix旨在:

  1. 提供对通常通过网络访问的服务的故障和延迟的保护和控制
  2. 停止因某些服务中断而导致的故障级联
  3. 快速失败并迅速恢复
  4. 尽可能优雅地降级
  5. 对故障进行实时监控和指挥中心警报