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

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

Трудности распределенных систем

Микросервисы «общаются» по сети, которая не надежна в принципе. Вот типичные проблемы:

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

Шаблонами «Повтор» и «Выключатель» смягчаются последствия этих проблем, повышается надежность системы.

Шаблон «Повтор»

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

«Повтор» используют, когда:

  • Сбой, по-видимому, временный. Например, при проблемах с сетью.
  • Операция идемпотентная, то есть многократно повторяемая.

Ключевые соображения

  1. Стратегия задержки: во избежание перегрузки сервиса используется экспоненциальная задержка.
  2. Ограничения повторов: для предотвращения бесконечных циклов задается максимальное количество повторов.
  3. Тайм-ауты: во избежание неопределенной блокировки ресурсов повторы сочетаются с тайм-аутами.

Пример реализации на Go

package main

import (
"errors"
"fmt"
"math/rand"
"time"
)

func unreliableOperation() error {
if rand.Intn(3) > 0 {
return errors.New("temporary failure")
}
return nil
}

func retry(attempts int, sleep time.Duration, operation func() error) error {
for i := 0; i < attempts; i++ {
err := operation()
if err == nil {
return nil
}
fmt.Printf("Attempt %d failed: %s\n", i+1, err)
time.Sleep(sleep)
sleep *= 2 // Экспоненциальная задержка
}
return errors.New("all retries failed")
}

func main() {
rand.Seed(time.Now().UnixNano())
err := retry(3, time.Second, unreliableOperation)
if err != nil {
fmt.Println("Operation failed:", err)
} else {
fmt.Println("Operation succeeded")
}
}

Шаблон «Выключатель»

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

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

«Выключатель» используют, когда:

  • Последующий сервис постоянно «сбоит» или находится в ожидании.
  • Нужно защитить систему от каскадных сбоев.

Ключевые соображения

  1. Пороговые значения отказа: ими определяется, когда выключатель откроется.
  2. Механизм возвращения в исходное состояние: указывается, когда пробовать восстанавливаться  —  переходить в полуоткрытое состояние.
  3. Нейтрализации неисправности: когда выключатель открыт, предоставляется альтернативный ответ.

Пример Java-реализации на Resilience4j

import io.github.resilience4j.circuitbreaker.*;
import java.util.function.Supplier;

public class CircuitBreakerExample {
public static void main(String[] args) {
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(50) // Выключатель открывается после 50 % отказов
.waitDurationInOpenState(Duration.ofSeconds(5))
.build();

CircuitBreaker circuitBreaker = CircuitBreaker.of("example", config);

Supplier<String> riskyCall = CircuitBreaker.decorateSupplier(circuitBreaker, () -> {
if (Math.random() > 0.5) {
throw new RuntimeException("Service failed!");
}
return "Success!";
});

for (int i = 0; i < 10; i++) {
try {
System.out.println(riskyCall.get());
} catch (Exception e) {
System.out.println("Request failed: " + e.getMessage());
}
}
}
}

Сочетание «Повтора» и «Выключателя»

Во многих случаях эти шаблоны друг другом дополняются. Например:

  1. «Повтором» обрабатываются временные сбои в рамках вызова одного сервиса.
  2. «Выключателем» предотвращается перегруженность последующего сервиса во время продолжительных сбоев.

Практический сценарий

Система обработки платежей, в которой:

  • Для повтора невыполненных вызовов API к платежному шлюзу используется «Повтор».
  • Чтобы прекратить вызовы шлюза после повторяющихся сбоев и избежать каскада сбоев в остальной системе, используется «Выключатель».

Рекомендации по реализации

  1. Мониторинг показателей: для отслеживания эффективности «Повтора» и «Выключателя» используйте инструменты вроде Prometheus или Elastic APM.
  2. Настройка конфигураций: корректируйте пороговые значения, время задержки и ограничения повторов в зависимости от потребностей системы.
  3. Корректный сбой: предоставляйте пользователям содержательные сообщения об ошибках или резервные ответы.

Заключение

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

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Muhammad Al Ichsan Nur Rizqi Said: Designing Fault-Tolerant Microservices with Retry and Circuit Breaker Patterns

Предыдущая статьяУсловия в CSS: что делать, если они нужны уже сейчас?
Следующая статьяКак выводятся векторы на C++