1. 概述
典型的分布式系统由多个协作服务组成。这些服务容易出现故障或响应延迟。当某个服务失败时,可能影响其他服务,降低系统性能,甚至导致部分功能不可用,最坏情况下会拖垮整个应用。
当然,现有解决方案能帮助提升应用的弹性和容错能力——Hystrix 就是这样的框架。
Hystrix 框架库通过提供容错和延迟容错能力,控制服务间的交互。它通过隔离故障服务并阻止故障级联传播,提升系统整体弹性。
本系列文章将首先探讨 Hystrix 在服务或系统故障时的救援机制,以及它能实现哪些功能。
2. 简单示例
Hystrix 通过隔离和包装远程服务调用来实现容错和延迟容错。在这个简单示例中,我们将调用封装在 HystrixCommand
的 run()
方法中:
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 + "!";
}
}
执行调用的方式如下:
@Test
public void givenInputBobAndDefaultSettings_whenCommandExecuted_thenReturnHelloBob(){
assertThat(new CommandHelloWorld("Bob").execute(), equalTo("Hello Bob!"));
}
3. Maven 配置
在 Maven 项目中使用 Hystrix,需要在 pom.xml
中添加 Netflix 的 hystrix-core
和 rxjava-core
依赖:
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-core</artifactId>
<version>1.5.4</version>
</dependency>
最新版本可在 这里 查找。
<dependency>
<groupId>com.netflix.rxjava</groupId>
<artifactId>rxjava-core</artifactId>
<version>0.20.7</version>
</dependency>
此库的最新版本可在 这里 查找。
4. 搭建远程服务
我们先模拟一个真实场景。在下面的例子中,RemoteServiceTestSimulator
类代表远程服务器上的服务。它包含一个方法,在指定延迟后返回消息。可以想象这个等待是模拟远程系统耗时操作导致的响应延迟:
class RemoteServiceTestSimulator {
private long wait;
RemoteServiceTestSimulator(long wait) throws InterruptedException {
this.wait = wait;
}
String execute() throws InterruptedException {
Thread.sleep(wait);
return "Success";
}
}
以下是调用 RemoteServiceTestSimulator
的示例客户端。服务调用被隔离并封装在 HystrixCommand
的 run()
方法中。正是这种封装提供了前文提到的弹性能力:
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()
方法执行调用。以下测试演示了具体操作:
@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
中设置超时,以及它如何通过短路机制提供保护:
@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"));
}
上述测试中,我们将服务响应延迟设为 500ms,同时将 HystrixCommand
的执行超时设为 10,000ms,为远程服务响应留出充足时间。
现在看看当执行超时小于服务超时的情况:
@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,000ms。我们预期服务在 5,000ms 内响应,但实际服务被设置为 15,000ms 后响应。执行测试时会发现,测试在 5,000ms 后就退出了,而不是等待 15,000ms,并抛出 HystrixRuntimeException
。
这证明了 Hystrix 不会超过配置的超时时间等待响应。这种机制使受 Hystrix 保护的系统响应更迅速。
在后续章节中,我们将探讨如何设置线程池大小以防止线程耗尽,并分析其优势。
5.2 基于有限线程池的防御编程
仅设置服务调用超时并不能解决所有远程服务问题。当远程服务响应变慢时,典型应用会持续调用该服务。 应用无法判断远程服务是否健康,每次请求都会创建新线程。这会导致本已负载过重的服务器线程被耗尽。
我们不希望这种情况发生,因为这些线程需要用于服务器上的其他远程调用或进程,同时也要避免 CPU 使用率飙升。看看如何在 HystrixCommand
中设置线程池大小:
@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 基于短路器模式的防御编程
不过,远程服务调用仍有改进空间。假设远程服务开始持续失败,我们不希望持续发送请求浪费资源。理想情况下,应在一段时间内停止请求,给服务恢复时间,然后再恢复请求。这就是所谓的 短路器模式(Circuit Breaker Pattern)。
看看 Hystrix 如何实现此模式:
@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
设为 4,000ms:配置短路器窗口,定义恢复请求的时间间隔CircuitBreakerRequestVolumeThreshold
设为 1:定义触发短路的最小请求数
✅ 基于以上设置,HystrixCommand
在两次失败请求后会触发短路。即使服务延迟设为 500ms,第三次请求也不会到达远程服务——Hystrix 会直接短路,方法返回 null
。
⚠️ 随后我们添加 Thread.sleep(5000)
以超过睡眠窗口限制。这将使 Hystrix 关闭短路器,后续请求将成功执行。
6. 总结
Hystrix 的设计目标是:
- ✅ 保护并控制通过网络访问的服务故障与延迟
- ✅ 阻止因部分服务宕机导致的故障级联
- ✅ 快速失败并迅速恢复
- ✅ 在可能时优雅降级
- ✅ 实时监控故障并向指挥中心告警
下一篇文章将探讨如何结合 Hystrix 与 Spring 框架的优势。
完整项目代码和所有示例可在 GitHub 项目 中找到。