Статьи

Когда вещи могут выйти из-под контроля: автоматические выключатели на практике.

Удивительно, насколько тесно связаны между собой современные программные системы. В большинстве случаев каждое простое приложение зависит от какого-либо внешнего сервиса или компонента, не говоря уже о стремительном развитии Интернета вещей (или просто IoT ). Это хорошо и не так в то же время, давайте посмотрим, почему …

Существует множество вариантов использования, когда использование других услуг, предоставляемых кем-то извне или внутри компании, имеет большой смысл (обмен сообщениями, выставление счетов, налоги, платежи, аналитика, логистика и т. Д.), Но каждая такая интеграция создает риск для наших приложений. : они становятся зависимыми от доступности и работоспособности этих услуг. Сетевая задержка, скачки нагрузки, просто банальные программные дефекты, каждое из этих неизвестных может поставить наши приложения на колени, мягко говоря, недовольные пользователи и партнеры.

Хорошая новость заключается в том, что существует схема, которую мы можем использовать для снижения рисков: автоматический выключатель . Сначала подробно объяснил в Release It! По словам Майкла Т. Найгарда , автоматические выключатели стали де-факто решением для работы с внешними сервисами. Идея довольно проста: отслеживать состояние внешнего сервиса за определенный промежуток времени, чтобы собрать сведения о его доступности. Если обнаруживается неисправность, размыкается автоматический выключатель, сигнализирующий, что внешнее обслуживание лучше не вызывать в течение некоторого времени.

Доступно множество реализаций автоматического выключателя, но поскольку мы работаем на JVM, мы поговорим о трех из них: Netflix Hystrix , Akka и Apache Zest . Чтобы сообщения были значительно короче, тема нашего обсуждения будет разделена на две части: Netflix Hystrix, а затем Akka и Apache Zest .

Чтобы продемонстрировать автоматические выключатели в действии, мы собираемся создать простой клиент на основе https://freegeoip.net/ : общедоступного веб-API HTTP для разработчиков программного обеспечения для поиска геолокации IP-адресов. Клиент вернет только краткую гео-информацию о конкретном IP или имени хоста, заключенную в класс GeoIpDetails :

1
2
3
4
5
6
7
8
9
@JsonIgnoreProperties(ignoreUnknown = true)
 
public final class GeoIpDetails {
    private String ip;
    @JsonProperty("country_code") private String countryCode;
    @JsonProperty("country_name") private String countryName;
    private double latitude;
    private double longitude;
}

Итак, давайте начнем …

Несомненно, Netflix Hystrix является самой продвинутой и тщательно проверенной реализацией автоматического выключателя в распоряжении разработчиков Java. Он построен с нуля, чтобы поддерживать парадигму асинхронного программирования (для этого интенсивно используется RxJava ) и имеет очень низкие накладные расходы. Это больше, чем просто автоматический выключатель , это полноценная библиотека, позволяющая выдерживать задержки и сбои в распределенных системах, но мы коснемся только основных концепций Netflix Hystrix .

Netflix Hystrix имеет удивительно простой дизайн и построен на основе командного шаблона , в основе которого лежит HystrixCommand . Команды идентифицируются по ключам и организованы в группы. Прежде чем мы собираемся реализовать нашу собственную команду, стоит поговорить о том, как Hystrix изолирует интеграцию внешних сервисов.

По сути, есть две основные стратегии, которые поддерживает Hystrix : перенести работу в другое место (используя выделенный пул потоков ) или выполнить работу в текущем потоке (полагаясь на семафоры ). Использование выделенных пулов потоков, также называемое шаблоном переборки , является правильной стратегией для использования в большинстве случаев: вызывающий поток разблокирован, а также могут быть установлены ожидания времени ожидания. Для семафоров текущий поток будет занят до тех пор, пока работа не будет завершена, успешно или нет (тайм-ауты также поддерживаются начиная с ветки выпуска 1.4.x, но есть определенные побочные эффекты).

На данный момент достаточно теории, давайте перейдем к коду, создав собственный командный класс Hystrix для доступа к https://freegeoip.net/ с использованием библиотеки Apache HttpClient :

01
02
03
04
05
06
07
08
09
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
public class GeoIpHystrixCommand extends HystrixCommand<String> {
    // Template: http://freegeoip.net/{format}/{host}
    private static final String URL = "http://freegeoip.net/";
    private final String host;
   
    public GeoIpHystrixCommand(final String host) {
        super(
            Setter
                .withGroupKey(HystrixCommandGroupKey.Factory.asKey("GeoIp"))
                .andCommandKey(HystrixCommandKey.Factory.asKey("GetDetails"))
                .andCommandPropertiesDefaults(
                    HystrixCommandProperties.Setter()
                        .withExecutionTimeoutInMilliseconds(5000)
                        .withMetricsHealthSnapshotIntervalInMilliseconds(1000)
                        .withMetricsRollingStatisticalWindowInMilliseconds(20000)
                        .withCircuitBreakerSleepWindowInMilliseconds(10000)
                    )
                .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("GeoIp"))
                .andThreadPoolPropertiesDefaults(
                    HystrixThreadPoolProperties.Setter()
                        .withCoreSize(4)
                        .withMaxQueueSize(10)
                )
        );
        this.host = host;
    }
   
    @Override
    protected String run() throws Exception {
        return Request
            .Get(new URIBuilder(URL).setPath("/json/" + host).build())
            .connectTimeout(1000)
            .socketTimeout(3000)
            .execute()
            .returnContent()
            .asString();
    }
}

Первое, что можно извлечь из этого фрагмента, — это то, что команды Hystrix имеют множество различных свойств, которые инициализируются в конструкторе. Группа команд и ключ, установленные в «GeoIp» и «GetDetails» соответственно, мы уже упоминали. Ключ пула потоков, установленный в «GeoIp» , и свойства пула потоков (например, размер основного пула и максимальный размер очереди) позволяют настроить конфигурацию пула потоков, стратегию изоляции выполнения по умолчанию, используемую Hystrix . Обратите внимание, что несколько команд могут ссылаться на один и тот же пул потоков (например, для снижения нагрузки), но семафоры не являются общими.

Другие свойства команды GeoIpHystrixCommand , возможно, наиболее важные, потребуют некоторого объяснения:

  • executeTimeoutInMilliseconds устанавливает жесткий предел общего выполнения команды до истечения времени ожидания
  • metricsHealthSnapshotIntervalInMilliseconds указывает, как часто следует пересчитывать состояние базового автоматического выключателя.
  • metricsRollingStatisticalWindowInMilliseconds определяет продолжительность скользящего окна для сохранения метрик для автоматического выключателя
  • circuitBreakerSleepWindowInMilliseconds устанавливает количество времени, чтобы отклонить запросы на отключенный выключатель перед повторной попыткой

Стоит отметить, что Hystrix имеет разумное значение по умолчанию для каждого свойства, поэтому вы не обязаны предоставлять их. Однако эти значения по умолчанию довольно агрессивны (в очень хорошем смысле), поэтому вам, возможно, придется немного расслабиться. Hystrix имеет потрясающую документацию, в которой подробно рассказывается обо всех свойствах (и их значениях по умолчанию).

Еще одна опция, которую включает в себя Hystrix, — это резервный вариант в случае, если выполнение команды не было успешным, истекло время или отключился автоматический выключатель. Хотя откат не является обязательным, очень хорошая идея иметь его, в случае https://freegeoip.net/ мы можем просто вернуть пустой ответ.

1
2
3
4
@Override
protected String getFallback() {
    return "{}"; /* empty response */
}

Отлично, у нас есть команда, и что теперь? Существует несколько способов вызова команды Hystrix . Самый простой способ — это просто синхронное выполнение с использованием метода execute () , например:

1
2
3
4
5
6
7
8
public class GeoIpService {
    private final ObjectMapper mapper = new ObjectMapper();
   
    public GeoIpDetails getDetails(final String host) throws IOException {
        return mapper.readValue(new GeoIpHystrixCommand(host).execute(),
            GeoIpDetails.class);
    }
}

В случае асинхронного выполнения, у Hystrix есть несколько вариантов, начиная с чистого будущего Java и заканчивая наблюдаемым в RxJava , например:

01
02
03
04
05
06
07
08
09
10
11
public Observable<GeoIpDetails> getDetailsObservable(final String host) {
    return new GeoIpHystrixCommand(host)
        .observe()
        .map(result -> {
             try {
                 return mapper.readValue(result, GeoIpDetails.class);
              } catch(final IOException ex) {
                  throw new RuntimeException(ex);
              }
        });
}

Полные источники примера проекта доступны на Github .

Если ваш проект построен на основе очень популярной Spring Framework , есть потрясающая встроенная поддержка Hystrix с использованием удобной (автоматической) конфигурации и аннотаций. Давайте кратко рассмотрим реализацию той же команды, используя проект Spring Cloud Netflix (конечно, вместе с Spring Boot ):

01
02
03
04
05
06
07
08
09
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
@Component
public class GeoIpClient {
    @Autowired private RestTemplate restTemplate;
  
    @HystrixCommand(
        groupKey = "GeoIp",
        commandKey = "GetDetails",
        fallbackMethod = "getFallback",
        threadPoolKey = "GeoIp",
        commandProperties = {
            @HystrixProperty(
                name = "execution.isolation.thread.timeoutInMilliseconds",
                value = "5000"
            ),
            @HystrixProperty(
                name = "metrics.healthSnapshot.intervalInMilliseconds",
                value = "1000"
            ),
            @HystrixProperty(
                name = "metrics.rollingStats.timeInMilliseconds",
                value = "20000"
            ),
            @HystrixProperty(
                name = "circuitBreaker.sleepWindowInMilliseconds",
                value = "10000"
            )
        },
        threadPoolProperties = {
            @HystrixProperty(name = "coreSize", value = "4"),
            @HystrixProperty(name = "maxQueueSize", value = "10")
        }
    )
    public GeoIpDetails getDetails(final String host) {
        return restTemplate.getForObject(
            UriComponentsBuilder
                .fromHttpUrl("http://freegeoip.net/{format}/{host}")
                .buildAndExpand("json", host)
                .toUri(),
            GeoIpDetails.class);
    }
   
    public GeoIpDetails getFallback(final String host) {
        return new GeoIpDetails();
    }
}

В этом случае присутствие команды Hystrix действительно скрыто, поэтому клиент просто набирает с помощью простого инъецируемого Spring- компонента, аннотируемого с помощью @HystrixCommand и инструментирующего с помощью аннотации @EnableCircuitBreaker .

И наконец, что немаловажно, есть еще несколько дополнительных материалов для Hystrix , доступных в рамках проекта Hystrix Contrib . Сначала мы поговорим о Hystrix-Servo-Metrics-Publisher, который предоставляет множество очень полезных метрик для JMX . По сути, это плагин, который должен быть явно зарегистрирован в Hystrix , например, вот один из способов сделать это:

1
2
3
HystrixPlugins
    .getInstance()
    .registerMetricsPublisher(HystrixServoMetricsPublisher.getInstance());

Когда наше приложение запущено и работает, вот как оно выглядит в JVisualVM (обратите внимание, что MBean com.netflix.servo будет появляться только после первого выполнения команды Hystrix или вызова инструментированного метода, поэтому вы можете не увидеть его сразу же в запуск приложения):

Hystrix-JMX

Говоря о Hystrix , нельзя не упомянуть Hystrix Dashboard : потрясающий веб-интерфейс для мониторинга показателей Hystrix в режиме реального времени.

Hystrix-панель

Еще раз спасибо Spring Cloud Netflix , его очень легко интегрировать в ваши приложения, используя только аннотацию @EnableHystrixDashboard и еще один проект из портфеля Hystrix Contrib , hystrix-metrics-event-stream, который предоставляет метрики Hystrix над потоком событий. Полная версия примера проекта на основе Spring доступна на Github .

Надеемся, что в этот момент вы согласитесь, что, по сути, каждая интеграция с внешними службами (которые в большинстве случаев являются просто черными ящиками) привносит нестабильность в наши приложения и может привести к каскадным сбоям и серьезным сбоям. В связи с этим Netflix Hystrix может стать спасением жизни, достойным принятия.

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

Все проекты доступны в репозитории Github .