«1. Обзор

Типичная распределенная система состоит из множества совместно работающих сервисов.

Эти службы подвержены сбоям или задержке ответов. Если служба выходит из строя, это может повлиять на другие службы, влияющие на производительность и, возможно, сделать другие части приложения недоступными или, в худшем случае, привести к остановке всего приложения.

Конечно, существуют решения, которые помогают сделать приложения отказоустойчивыми и отказоустойчивыми — одной из таких сред является Hystrix.

Библиотека инфраструктуры Hystrix помогает контролировать взаимодействие между службами, обеспечивая отказоустойчивость и устойчивость к задержкам. Это повышает общую отказоустойчивость системы, изолируя неисправные службы и предотвращая каскадный эффект сбоев.

В этой серии статей мы начнем с рассмотрения того, как Hystrix приходит на помощь в случае сбоя службы или системы и что Hystrix может сделать в этих обстоятельствах.

2. Простой пример

Способ, которым Hystrix обеспечивает отказоустойчивость и устойчивость к задержкам, заключается в изоляции и переносе вызовов к удаленным службам.

В этом простом примере мы заключаем вызов в метод run() команды HystrixCommand:

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

Для использования Hystrix в проектах Maven нам нужна зависимость hystrix-core и rxjava-core от Netflix в проекте pom.xml:

<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.

Вызов службы изолирован и помещен в метод run() команды HystrixCommand. Именно эта оболочка обеспечивает устойчивость, о которой мы говорили выше:

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

Вызов выполняется путем вызова метода execute() для экземпляра объекта RemoteServiceTestCommand.

Следующий тест демонстрирует, как это делается:

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

В приведенном выше тесте мы задержали ответ службы, установив тайм-аут на 500 мс. Мы также устанавливаем время ожидания выполнения в HystrixCommand равным 10 000 мс, что дает достаточно времени для ответа удаленной службы.

Теперь давайте посмотрим, что происходит, когда время ожидания выполнения меньше, чем время ожидания вызова службы:

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

Обратите внимание, как мы понизили планку и установили время ожидания выполнения на 5000 мс.

Мы ожидаем, что служба ответит в течение 5 000 мс, тогда как мы настроили службу на ответ через 15 000 мс. Если вы заметите, что при выполнении теста тест завершится через 5000 мс вместо ожидания 15 000 мс и вызовет исключение HystrixRuntimeException.

Это демонстрирует, как Hystrix не ожидает ответа дольше установленного тайм-аута. Это помогает сделать систему, защищенную Hystrix, более отзывчивой.

В следующих разделах мы рассмотрим настройку размера пула потоков, который предотвращает исчерпание потоков, и обсудим его преимущества.

5.2. Защитное программирование с ограниченным пулом потоков

Установка времени ожидания для вызова службы не решает всех проблем, связанных с удаленными службами.

«Когда удаленная служба начинает отвечать медленно, обычное приложение будет продолжать вызывать эту удаленную службу.

Приложение не знает, исправна ли удаленная служба или нет, и новые потоки создаются каждый раз, когда приходит запрос. Это приведет к использованию потоков на уже проблемном сервере.

Мы не хотим, чтобы это произошло, так как нам нужны эти потоки для других удаленных вызовов или процессов, работающих на нашем сервере, и мы также хотим избежать резкого увеличения загрузки ЦП.

Давайте посмотрим, как установить размер пула потоков в 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"));
}

В приведенном выше тесте мы устанавливаем максимальный размер очереди, размер основной очереди и размер отклонения очереди. Hystrix начнет отклонять запросы, когда максимальное количество потоков достигнет 10, а очередь задач достигнет размера 10.

Размер ядра — это количество потоков, которые всегда остаются активными в пуле потоков.

5.3. Защитное программирование с шаблоном прерывателя короткого замыкания

Тем не менее, мы все еще можем улучшить удаленные вызовы службы.

Давайте рассмотрим случай, когда удаленный сервис начал давать сбой.

Мы не хотим продолжать посылать ему запросы и тратить ресурсы. В идеале мы хотели бы прекратить делать запросы на определенное время, чтобы дать службе время восстановиться, прежде чем возобновлять запросы. Это так называемый паттерн короткого замыкания.

Давайте посмотрим, как 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, для которого установлено значение 4000 мс. Это настраивает окно прерывателя цепи и определяет интервал времени, после которого запрос к удаленной службе будет возобновлен настройки на месте, наша команда HystrixCommand теперь будет открываться после двух неудачных запросов. Третий запрос даже не попадет в удаленную службу, даже несмотря на то, что мы установили задержку службы на 500 мс, Hystrix замкнется, и наш метод вернет null в качестве ответа.

Впоследствии мы добавим Thread.sleep(5000), чтобы выйти за пределы установленного нами окна сна. Это приведет к тому, что Hystrix закроет цепь, и последующие запросы будут проходить успешно.

6. Заключение

Подводя итоги, Hystrix предназначен для:

  1. Provide protection and control over failures and latency from services typically accessed over the network
  2. Stop cascading of failures resulting from some of the services being down
  3. Fail fast and rapidly recover
  4. Degrade gracefully where possible
  5. Real time monitoring and alerting of command center on failures

В следующем посте мы увидим, как объединить преимущества Hystrix с фреймворком Spring.

Полный код проекта и все примеры можно найти в проекте github.