1. 概述

典型的分布式系统由多个协作服务组成。这些服务容易出现故障或响应延迟。当某个服务失败时,可能影响其他服务,降低系统性能,甚至导致部分功能不可用,最坏情况下会拖垮整个应用。

当然,现有解决方案能帮助提升应用的弹性和容错能力——Hystrix 就是这样的框架。

Hystrix 框架库通过提供容错和延迟容错能力,控制服务间的交互。它通过隔离故障服务并阻止故障级联传播,提升系统整体弹性。

本系列文章将首先探讨 Hystrix 在服务或系统故障时的救援机制,以及它能实现哪些功能。

2. 简单示例

Hystrix 通过隔离和包装远程服务调用来实现容错和延迟容错。在这个简单示例中,我们将调用封装在 HystrixCommandrun() 方法中:

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-corerxjava-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 的示例客户端。服务调用被隔离并封装在 HystrixCommandrun() 方法中。正是这种封装提供了前文提到的弹性能力:

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 的设计目标是:

  1. ✅ 保护并控制通过网络访问的服务故障与延迟
  2. ✅ 阻止因部分服务宕机导致的故障级联
  3. ✅ 快速失败并迅速恢复
  4. ✅ 在可能时优雅降级
  5. ✅ 实时监控故障并向指挥中心告警

下一篇文章将探讨如何结合 Hystrix 与 Spring 框架的优势。

完整项目代码和所有示例可在 GitHub 项目 中找到。


原始标题:Introduction to Hystrix