Как создать надежное приложение

1656547334 kak sozdat nadezhnoe prilozhenie

Обзор различных вариантов дизайна программы

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

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

1*lVLik3M6m33YZ52n0iHclQ
Супервайзеры перезапустят аварийный процесс

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

Виды неудач

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

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

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

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

1*SjWLLvJNhNtqAjuGviDYBQ

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

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

Лечение ошибок

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

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

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

1*KaOlLRtoOIQXgxnIXVZzcQ

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

ЗА

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

ПРОТИВ

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

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

Давайте обсудим более устойчивый подход.

Кража идеи из IEEE

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

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

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

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

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

1*y1jkyHboflo5rqixjfh7Kw

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

<?php
/**
 * Assume that we're using a message bus which is able to
 * retry failed messages with a custom retry delay.
 */
class FetchCarMessageHandler
{
  public function handle(Message $msg)
  {
    try {
      $id = (int)$msg->getContent();
      $cars = $client->get('/car/'.$id);
  
      return Result::success($cars);
    } catch (TimeoutException $e) {
      $lastBackoff = $msg->getLastBackoff();
      // The infrastructure layer will automagically retry the message after XYZ seconds
      return Result::retryAfter($lastBackoff * 2, $msg);
    }
  }
}

Повторная попытка против экспоненциального отступления

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

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

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

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

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

1*qnvQ3GiF27TlIZbFCr2omA
Источник: https://pixabay.com/en/circuit-breakers-rcds-fault-current-1167327/

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

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

Автоматический выключатель — это компонент, который оборачивает защищенный вызов во внешнюю службу и может контролировать ответы, проверяя исправность службы. Точно так же как электронный компонент может быть программный выключатель ОТКРЫТО или ЗАКРЫТ. An ОТКРЫТО статус будет означать, что служба по цепи не работает, и a ЗАКРЫТ статус означает, что услуга запущена.

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

В случае an ОТКРЫТО цепи, мы могли бы решить быстро ответить клиенту резервным ответом. К примеру, кэшированные данные, данные по умолчанию или все, что имеет смысл для конкретной программы.

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

<?php
class CircuitBreaker
{
  private $maxFailures;
  private $service;
  private $redisClient;
  
  public function __construct(int $maxFailures, callable $service)
  {
    $this->maxFailures = $maxFailures;
    $this->service = $service;
    $this->redisClient = new RedisClient();
  }
  private function isUp(string $key)
  {
    return (int)$this->redisClient->get($key) < $this->maxFailures;
  }
  private function fail(string $key, int $ttl)
  {
    $this->redisClient->incr($key, 1);
    $this->redisClient->expire($key, $ttl);
  }
  
  public function __invoke()
  {
    [$arguments, $defaultResponse] = func_get_args();
    $key = md5($arguments);
    if (!$this->isUp($key)) {
        return $defaultResponse;
    }
    try {
      $result = call_user_func_array($this->service, $arguments);
      
      return $result;
    } catch (\Throwable $e) {
      $this->fail($key, 10);
      
      return $defaultResponse;
    }
  }
}

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

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

<?php
$productListing = new CircuitBreaker(
    10, 
    function($searchKey) {
        // $result is given from the api call
        return $result;
    }
);
$productsToShow = $productListing(['t-shirt'], []);

Вывод

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

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

Добавить комментарий

Ваш адрес email не будет опубликован.