Шаблон размыкателя цепи

Azure

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

Контекст и проблема

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

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

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

Решение

Шаблон автоматического выключения, популяризированный Майклом Найгардом (Michael Nygard) в книге Release It! Design and Deploy Production-Ready Software (Выпускаем в свет! Разработка и внедрение ПО, готового к выпуску), может помешать повторной попытке приложения выполнить операцию, которая, скорее всего, завершится со сбоем. Разрешите ему продолжить выполнение, не ожидая устранения ошибки или расхода ресурсов процессора на определение того, что предполагается долгий сбой. Шаблон автоматического выключения также позволяет приложению определять, была ли устранена неисправность. Если проблема устранена, приложение может попытаться вызвать операцию.

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

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

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

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

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

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

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

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

Состояния автоматического выключения

На рисунке показан счетчик сбоев на основе времени, используемый состоянием Закрытый. Он сбрасывается через периодические интервалы. Это позволяет предотвратить переход автоматического выключения в состояние Открытый при случайных ошибках. Порог сбоев, который переводит автоматическое выключение в состояние Открытый, достигается, только если указанное количество сбоев произошло в течение заданного интервала. Счетчик, используемый состоянием Полуоткрытый, записывает количество успешных попыток вызвать операцию. Автоматическое выключение возвращается в состояние Закрытый после определенного числа последовательных успешных вызовов операций. Если вызов завершается со сбоем, автоматическое выключение немедленно переходит в состояние Открытый, а счетчик успешных выполнений будет сброшен до следующего раза, когда автоматический выключатель перейдет в состояние Полуоткрытый.

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

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

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

Проблемы и рекомендации

При выборе схемы реализации этого шаблона следует учитывать следующие моменты.

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

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

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

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

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

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

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

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

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

Примечание.

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

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

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

Когда следует использовать этот шаблон

Используйте этот шаблон в следующих случаях:

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

Этот шаблон не рекомендуется использовать в следующих случаях:

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

Проектирование рабочей нагрузки

Архитектор должен оценить, как шаблон разбиения цепи можно использовать в проектировании рабочей нагрузки для решения целей и принципов, описанных в основных принципах Платформы Azure Well-Architected Framework. Например:

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

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

- Pe:07 Code и инфраструктура
- Ответы pe:11 Live-issues

Как и любое решение по проектированию, рассмотрите любые компромиссы по целям других столпов, которые могут быть представлены с этим шаблоном.

Пример

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

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

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

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

Класс CircuitBreaker сохраняет информацию о состоянии автоматического выключения в объекте, который реализует интерфейс ICircuitBreakerStateStore, как показано в следующем коде.

interface ICircuitBreakerStateStore
{
  CircuitBreakerStateEnum State { get; }

  Exception LastException { get; }

  DateTime LastStateChangedDateUtc { get; }

  void Trip(Exception ex);

  void Reset();

  void HalfOpen();

  bool IsClosed { get; }
}

Свойство State указывает на текущее состояние автоматического выключения (Открытый, Полуоткрытый и Закрытый, как определено в соответствии с перечислением CircuitBreakerStateEnum). Свойство IsClosed должно иметь значение true, если автоматическое выключение закрыто, и false, если оно открыто или полуоткрыто. Метод Trip переводит автоматическое выключение в состояние "Открытый" и записывает исключение, которое вызывает изменения в состоянии, а также время и дату создания исключения. Свойства LastException и LastStateChangedDateUtc возвращают эти сведения. Метод Reset закрывает автоматическое выключение, а HalfOpen — переводит его в состояние "Полуоткрытый".

Класс InMemoryCircuitBreakerStateStore в примере содержит реализацию интерфейса ICircuitBreakerStateStore. Класс CircuitBreaker создает экземпляр этого класса для хранения состояния автоматического выключения.

Метод ExecuteAction в классе CircuitBreaker помещает операцию, указанную как делегат Action, в оболочку. Если автоматическое выключение закрыто, ExecuteAction вызывает делегат Action. Если операция завершается со сбоем, обработчик исключений вызывает TrackException, который устанавливает автоматическое выключение в состояние "Открытый". Эта процедура представлена в следующем примере кода.

public class CircuitBreaker
{
  private readonly ICircuitBreakerStateStore stateStore =
    CircuitBreakerStateStoreFactory.GetCircuitBreakerStateStore();

  private readonly object halfOpenSyncObject = new object ();
  ...
  public bool IsClosed { get { return stateStore.IsClosed; } }

  public bool IsOpen { get { return !IsClosed; } }

  public void ExecuteAction(Action action)
  {
    ...
    if (IsOpen)
    {
      // The circuit breaker is Open.
      ... (see code sample below for details)
    }

    // The circuit breaker is Closed, execute the action.
    try
    {
      action();
    }
    catch (Exception ex)
    {
      // If an exception still occurs here, simply
      // retrip the breaker immediately.
      this.TrackException(ex);

      // Throw the exception so that the caller can tell
      // the type of exception that was thrown.
      throw;
    }
  }

  private void TrackException(Exception ex)
  {
    // For simplicity in this example, open the circuit breaker on the first exception.
    // In reality this would be more complex. A certain type of exception, such as one
    // that indicates a service is offline, might trip the circuit breaker immediately.
    // Alternatively it might count exceptions locally or across multiple instances and
    // use this value over time, or the exception/success ratio based on the exception
    // types, to open the circuit breaker.
    this.stateStore.Trip(ex);
  }
}

В следующем примере показан код (отсутствующий в предыдущем примере), который выполняется, если автоматическое выключение не закрыто. Сначала в нем проверяется, было ли открыто автоматическое выключение в течение периода, большего, чем задано в локальном поле OpenToHalfOpenWaitTime в классе CircuitBreaker. Если это так, метод ExecuteAction задает автоматическому выключению состояние "Полуоткрытый", а затем пытается выполнить операцию, заданную делегатом Action.

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

Если автоматическое выключение находилось в открытом состоянии в течение короткого времени (меньшем, чем значение OpenToHalfOpenWaitTime), метод ExecuteAction просто вызывает исключение CircuitBreakerOpenException и возвращает сообщение об ошибке, которая вызвала переход автоматического выключения в состояние "Открытый".

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

    ...
    if (IsOpen)
    {
      // The circuit breaker is Open. Check if the Open timeout has expired.
      // If it has, set the state to HalfOpen. Another approach might be to
      // check for the HalfOpen state that had be set by some other operation.
      if (stateStore.LastStateChangedDateUtc + OpenToHalfOpenWaitTime < DateTime.UtcNow)
      {
        // The Open timeout has expired. Allow one operation to execute. Note that, in
        // this example, the circuit breaker is set to HalfOpen after being
        // in the Open state for some period of time. An alternative would be to set
        // this using some other approach such as a timer, test method, manually, and
        // so on, and check the state here to determine how to handle execution
        // of the action.
        // Limit the number of threads to be executed when the breaker is HalfOpen.
        // An alternative would be to use a more complex approach to determine which
        // threads or how many are allowed to execute, or to execute a simple test
        // method instead.
        bool lockTaken = false;
        try
        {
          Monitor.TryEnter(halfOpenSyncObject, ref lockTaken);
          if (lockTaken)
          {
            // Set the circuit breaker state to HalfOpen.
            stateStore.HalfOpen();

            // Attempt the operation.
            action();

            // If this action succeeds, reset the state and allow other operations.
            // In reality, instead of immediately returning to the Closed state, a counter
            // here would record the number of successful operations and return the
            // circuit breaker to the Closed state only after a specified number succeed.
            this.stateStore.Reset();
            return;
          }
        }
        catch (Exception ex)
        {
          // If there's still an exception, trip the breaker again immediately.
          this.stateStore.Trip(ex);

          // Throw the exception so that the caller knows which exception occurred.
          throw;
        }
        finally
        {
          if (lockTaken)
          {
            Monitor.Exit(halfOpenSyncObject);
          }
        }
      }
      // The Open timeout hasn't yet expired. Throw a CircuitBreakerOpen exception to
      // inform the caller that the call was not actually attempted,
      // and return the most recent exception received.
      throw new CircuitBreakerOpenException(stateStore.LastException);
    }
    ...

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

var breaker = new CircuitBreaker();

try
{
  breaker.ExecuteAction(() =>
  {
    // Operation protected by the circuit breaker.
    ...
  });
}
catch (CircuitBreakerOpenException ex)
{
  // Perform some different action when the breaker is open.
  // Last exception details are in the inner exception.
  ...
}
catch (Exception ex)
{
  ...
}

Следующие шаблоны также могут быть полезными при реализации этого шаблона:

  • Шаблон надежного веб-приложения показывает, как применить шаблон разбиения каналов к веб-приложениям, конвергентным в облаке.

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

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