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

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

Основные ингредиенты блюда «Автоматический выключатель»

В Resilience4j автоматический выключатель реализован через конечный автомат с тремя состояниями: ЗАКРЫТО, ОТКРЫТО и HALF_OPEN. Каждое состояние имеет собственное, независимо настраиваемое хранилище метрик, которое используется для отслеживания частоты отказов и проверки ее на соответствие настроенному пороговому значению.

CircuitBreaker сам по себе ничего не знает о состоянии серверной части, но использует информацию, предоставленную декораторами CircuitBreaker :: onSuccess via и CircuitBreaker :: onError. Пример оформления:

Состояние автоматического выключателя меняется с ЗАКРЫТО на ОТКРЫТО, когда частота отказов превышает (настраиваемый) порог. Затем весь доступ к защищенной операции блокируется на (настраиваемый) период времени. CircuitBreaker :: isCallPermitted генерирует CircuitBreakerOpenException, если автоматический выключатель находится в положении ОТКРЫТО.

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

Итак, нам нужно решить только две основные проблемы:

  1. Как управлять переходами между состояниями потокобезопасным способом.
  2. Как отследить частоту отказов в скользящем окне недавних операций.

Переходы атомных состояний

Вся логика переходов инкапсулирована внутри класса CircuitBreakerStateMachine, он имеет

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

CircuitBreakerState - абстрактный класс с 3 реализациями.

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

Храните тысячи статусов операций и не взрывайте кучу

В отличие от реализации Hystrix, автоматический выключатель Resilience4j не привязан ко времени, вы можете настроить его для расчета текущей интенсивности отказов при последних N записанных операциях. Мы используем структуру данных RingBitSet для хранения успешных вызовов как бит 0, а неудачные вызовы сохраняются как бит 1.

RingBitSet имеет настраиваемый размер и использует модифицированную версию BitSet для хранения битов, которые экономят память, по сравнению с логическим массивом. BitSet использует массив long [] для хранения битов. Это означает, что BitSet нужен только массив из 16 длинных (64-битных) значений для хранения статуса 1024 вызовов.

В нашей первой реализации мы использовали BitSet из стандартной библиотеки, и это дало нам разумную производительность с несколькими последовательными операциями BitSet :: set, BitSet :: cardinality и CircuitBreakerState :: checkFailureRate. Но мы решили еще больше оптимизировать эти операции, просто чтобы убедиться, что мы вносим как можно меньше накладных расходов.

И здесь мы создали небольшую модификацию стандартной реализации BitSet. Ключевые отличия:

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

После всех изменений следующие методы

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

Эти простые приемы освободили нас от пересчета количества элементов, и теперь наши операции обновления автоматического выключателя имеют сложность O (1).

Изначально опубликовано для DZone.