Обзор различных вариантов дизайна приложения

Когда мы разрабатываем программное обеспечение, мы постоянно думаем о случаях ошибок. Ошибки имеют огромное влияние на то, как мы проектируем и разрабатываем решение. Настолько, что существует философия, известная как Let It Crash.

Разрешить сбой - это способ устранения сбоев в Erlang, просто позволяя приложению аварийно завершить работу и позволяя супервизору перезапустить сбойный процесс из чистого состояния.

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

Виды отказов

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

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

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

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

Выявление временных ошибок

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

Устранение ошибок

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

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

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

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

ПРОФИ

  • Тривиальная реализация.
  • Без сохранения состояния (каждый запрос на повторную попытку изолирован, и вам не нужна дополнительная информация).

МИНУСЫ

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

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

Итак, давайте обсудим более гибкий подход.

Похищение идеи из IEEE

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

Концепция экспоненциального отката напрямую исходит из сетевого протокола Ethernet (IEEE 802.3), где он используется для разрешения конфликтов пакетов.

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

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

После коллизий * c * выбирается случайное количество слотов от 0 до 2 * c * - 1. Для первой коллизии каждый отправитель будет ждать 0 или 1 слот раз. После второй коллизии отправители будут ждать от 0 до 3 временных интервалов. После третьей коллизии отправители будут ждать от 0 до 7 временных интервалов (включительно) и так далее. По мере увеличения количества попыток повторной передачи количество возможностей задержки увеличивается экспоненциально - Экспоненциальная отсрочка - Википедия

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

Retry vs Exponential Backoff

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

Возможно, нам повезет и мы получим ответ после пары повторных попыток, или мы можем попасть в бесконечный цикл retry-wait-retry-wait… и никогда не получить ответ.
Вы знаете, закон Мерфи всегда здесь: « Все, что может пойти не так, пойдет не так ».

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

Нам нужна более сильная стратегия для поддержания устойчивости инфраструктуры.

Электроника может нам помочь

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

От электроники к информатике

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

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

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

Давайте посмотрим на реальный пример из мира электронной коммерции. Мы собираемся использовать метод автоматического выключателя для защиты вызова API со списком товаров.

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

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

Заключение

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

Вот некоторые из хорошо известных тактик для создания настоящего надежного приложения: