Статьи

Микросервисы для разработчиков Java: микросервисы и ошибки распределенных вычислений

1. Введение

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

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

Любая сложная система может (и будет) работать неожиданно… — https://queue.acm.org/detail.cfm?id=2353017

2. Локальный! = Распределенный

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

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

3. SLA

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

Почему это так важно? Прежде всего, это дает команде разработчиков определенную свободу выбора правильного технологического стека. И, во-вторых, он намекает потребителям сервиса, что ожидать с точки зрения времени отклика и доступности (чтобы потребители могли получать собственные SLA ).

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

4. Проверки здоровья

Существует ли быстрый способ проверить, работает ли служба, даже до того, как приступить к потенциально сложной бизнес-транзакции? Проверка работоспособности является стандартной практикой, когда служба сообщает о своей готовности приступить к работе.

Все сервисы платформы JCG Car Rentals по умолчанию предоставляют конечные точки проверки работоспособности . Ниже выбрана служба поддержки клиентов для демонстрации проверки работоспособности в действии:

01
02
03
04
05
06
07
08
09
10
11
$ curl http://localhost:18800/health
{
  "checks": [
    {
      "data": {},
      "name": "db",
      "state": "UP"
    }
  ],
  "outcome": "UP"
}

Как мы увидим позже, проверки работоспособности активно используются уровнями инфраструктуры и оркестровки для проверки службы, оповещения и / и применения компенсирующих действий.

5. Тайм-ауты

Когда одна сторона вызывает другую по беспроводной сети, настройка правильных тайм-аутов (соединение, чтение, запись, запрос и т. Д.), Вероятно, является самой простой, но наиболее эффективной стратегией для использования. Мы уже видели это в предыдущей части урока , вот только короткий остаток.

1
2
3
4
5
6
final CompletableFuture customer = client
    .prepareGet("http://localhost:8080/api/customers/" + uuid)
    .setRequestTimeout(500)
    .setReadTimeout(100)
    .execute()
    .toCompletableFuture();

Когда другая сторона не отвечает или каналы связи ненадежны, ждать неопределенно долго в надежде, что ответ может наконец прийти, не лучший вариант. Теперь вопрос в том, какие тайм-ауты должны быть установлены? Не существует единого магического числа, подходящего для всех, но сервисные соглашения об уровне обслуживания, которые мы обсуждали ранее, являются ключевым источником информации для ответа на этот вопрос.

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

6. Повторные попытки

С точки зрения потребителя повторение запроса к сервису в случае периодических сбоев является самым простым делом. В этих целях такие библиотеки, как Spring Retry , failsafe или resilience4j , очень помогают, предлагая широкий спектр политик повторения и возврата. Например, приведенный ниже фрагмент демонстрирует подход Spring Retry .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
final SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(5);
final ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy();
backOffPolicy.setInitialInterval(1000);
backOffPolicy.setMaxInterval(20000);
        
final RetryTemplate template = new RetryTemplate();
template.setRetryPolicy(retryPolicy);
template.setBackOffPolicy(backOffPolicy);
            
final Result result = template.execute(new RetryCallback<Result, IOException>() {
    public Result doWithRetry(RetryContext context) throws IOException {
        // Any logic which needs retry here
        return ...;
    }
});

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

01
02
03
04
05
06
07
08
09
10
11
12
final WebClient client = WebClient
    .builder()
    .clientConnector(new ReactorClientHttpConnector(httpClient))
    .baseUrl(“http://localhost:8080/api/customers”)
    .build();
final Mono customer = client
    .get()
    .uri("/{uuid}", uuid)
    .retrieve()
    .bodyToMono(Customer.class)
    .retryBackoff(5, Duration.ofSeconds(1));

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

И, наконец, что не менее важно, одним из серьезных соображений при использовании любой стратегии повторных попыток является идемпотентность : профилактические меры следует принимать как со стороны потребителя, так и со стороны обслуживания, чтобы избежать неожиданных побочных эффектов.

7. Навальный заголовок

Концепция переборки заимствована у судостроительной отрасли и нашла свою прямую аналогию в практике разработки программного обеспечения.

Переборки используются на судах для создания отдельных водонепроницаемых отсеков, которые служат для ограничения эффекта отказа — в идеале, предотвращая затопление судна. https://skife.org/architecture/fault-tolerance/2009/12/31/bulkheads.html

Хотя мы строим не корабли, а программное обеспечение, основная идея остается неизменной: минимизировать влияние сбоев в приложениях, в идеале предотвращая их сбои или становясь безответственными. Давайте обсудим несколько сценариев, где переборка проявляется, особенно в микросервисах .

Службу бронирования , являющуюся частью платформы JCG Car Rentals , можно попросить восстановить все бронирования для конкретного клиента. Для этого он сначала консультируется со службой поддержки, чтобы убедиться, что клиент существует, и в случае успешного ответа извлекает доступные резервирования из основного хранилища данных, ограничивая результаты первыми 20 записями.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
@Autowired private WebClient customers;
@Autowired private ReservationsByCustomersRepository repository;
@Autowired private ConversionService conversion;
    
@GetMapping("/customers/{uuid}/reservations")
public Flux findByCustomerId(@PathVariable UUID uuid) {
    return customers
        .get()
        .uri("/{uuid}", uuid)
        .retrieve()
        .bodyToMono(Customer.class)
        .flatMapMany(c -> repository
            .findByCustomerId(uuid)
            .take(20)
            .map(entity -> conversion.convert(entity, Reservation.class)));
}

Краткость стека Spring Reactive удивительна, не так ли? Так в чем может быть проблема с этим фрагментом кода? Все зависит от repository , правда. Если вызов блокируется, катастрофа может произойти, поскольку четный цикл также будет заблокирован (помните, шаблон Reactor ). Вместо этого блокирующий вызов должен быть изолирован и выгружен в выделенный пул (с помощью subscribeOn ).

01
02
03
04
05
06
07
08
09
10
return customers
    .get()
    .uri("/{uuid}", uuid)
    .retrieve()
    .bodyToMono(Customer.class)
    .flatMapMany(c -> repository
        .findByCustomerId(uuid)
        .take(20)
        .map(entity -> conversion.convert(entity, Reservation.class))
        .subscribeOn(Schedulers.elastic()));

Возможно, это один из примеров переборки для использования выделенных пулов потоков, очередей или процессов, чтобы минимизировать влияние на критические части приложения. Развертывание и балансировка между несколькими экземплярами службы, изоляция арендаторов в приложениях с несколькими арендаторами, расстановка приоритетов при обработке запросов, согласование использования ресурсов между фоновыми и передними рабочими — это лишь краткий список интересных задач, с которыми вы можете столкнуться.

8. Автоматические выключатели

Удивительно, поэтому мы узнали о стратегиях повторных попыток и переборках , мы знаем, как применять эти принципы, чтобы изолировать сбои и постепенно выполнить работу. Однако наша цель на самом деле не в этом, мы должны оставаться отзывчивыми и выполнять обещания SLA . И даже если у вас их нет, ответ в течение разумного периода времени является обязательным. Шаблон автоматического выключателя , популяризированный Майклом Найгардом в потрясающем и очень рекомендуемый для чтения Release It! книга, это то, что нам действительно нужно.

Реализация автоматического выключателя может стать довольно сложной, но мы собираемся сосредоточиться на двух ее основных функциях: возможность отслеживать состояние удаленного вызова и использовать запасной вариант в случае сбоев или тайм-аутов. Есть немало отличных библиотек, которые предоставляют реализации автоматического выключателя . Помимо failsafe и resilience4j, о которых мы упоминали ранее, существуют также Hystrix , Apache Polygene и Akka . Hystrix , вероятно, является самой известной и проверенной в эксплуатации реализацией выключателя на сегодняшний день, и мы также собираемся ее использовать.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
public Flux findByCustomerId(@PathVariable UUID uuid) {
    final Publisher customer = customers
        .get()
        .uri("/{uuid}", uuid)
        .retrieve()
        .bodyToMono(Customer.class);
    final Publisher fallback = HystrixCommands
        .from(customer)
        .eager()
        .commandName("get-customer")
        .fallback(Mono.empty())
        .build();
        
    return Mono
        .from(fallback)
        .flatMapMany(c -> repository
            .findByCustomerId(uuid)
            .take(20)
            .map(entity -> conversion.convert(entity, Reservation.class)));
}

Мы не настраивали никакую конфигурацию Hystrix в этом примере, но если вам интересно узнать больше о внутренних компонентах, ознакомьтесь с этой статьей .

Использование автоматического выключателя не только помогает потребителю принимать разумные решения на основе оперативной статистики, но и потенциально может помочь поставщику услуг быстрее восстановиться после прерывистой нагрузки.

9. Бюджеты

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

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

10. Постоянные очереди

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

Давайте вернемся к примеру отправки электронного письма с подтверждением после успешной регистрации клиента, который мы реализовали с использованием асинхронных событий CDI 2.0 .

1
2
3
4
5
6
7
customerRegisteredEvent
    .fireAsync(new CustomerRegistered(entity.getUuid()))
    .whenComplete((r, ex) -> {
        if (ex != null) {
            LOG.error("Customer registration post-processing failed", ex);
        }
    });

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

Для случаев, когда потеря таких событий или сообщений нежелательна, одним из вариантов является использование постоянной очереди в процессе, как, например, Chronicle Queue . Но в долгосрочной перспективе лучше использовать выделенный брокер сообщений или хранилище данных.

11. Ограничители скорости

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

Ограничение скорости — это эффективный метод контроля скорости запросов от конкретного источника и снижения нагрузки в случае нарушения ограничений.

Хотя можно запекать ограничение скорости для каждого сервиса (используя, например, Redis для координации всех экземпляров сервиса), имеет смысл переложить такую ​​ответственность на уровни шлюза API и уровня оркестровки. Мы вернемся к этой теме более подробно позже в руководстве.

12. Саги

Давайте на минутку забудем об отдельных микроуслугах и посмотрим на общую картину. Типичный бизнес-процесс обычно представляет собой многоэтапный процесс и опирается на несколько микросервисов, чтобы успешно выполнять свою роль. Поток бронирования, который реализует JCG Car Rentals, является хорошим примером этого. В этом участвуют как минимум три службы:

  • Служба инвентаризации должна подтвердить наличие автомобиля
  • Служба бронирования должна проверить, что автомобиль еще не забронирован, и сделать заказ
  • Платежная служба должна обработать платежи (или возмещения)

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

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

Сага — это последовательность локальных транзакций. Каждая локальная транзакция обновляет базу данных и публикует сообщение или событие для запуска следующей локальной транзакции в саге. Если локальная транзакция терпит неудачу из-за нарушения бизнес-правила, тогда сага выполняет серию компенсирующих транзакций, которые отменяют изменения, сделанные предыдущими локальными транзакциями. https://microservices.io/patterns/data/saga.html

Весьма вероятно, что вам, возможно, придется полагаться на саги при реализации бизнес-потоков, охватывающих несколько микросервисов . Axon и Eventuate Tram Saga являются двумя примерами фреймворков, которые поддерживают саги, но шансы оказаться в ситуации DIY очень высоки.

13. Хаос

В этот момент может показаться, что создание микросервисов — это борьба с хаосом: все, что угодно, может сломаться, и вам придется как-то с этим бороться. В некотором смысле это правда, и, вероятно, именно поэтому родилась дисциплина хаоса .

Chaos Engineering — это дисциплина экспериментов на распределенной системе , чтобы укрепить уверенность в способности системы противостоять турбулентным условиям производства. https://principlesofchaos.org/

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

14. Выводы

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

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

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

15. Что дальше

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