
В мире, где распределенные системы являются основой современных приложений, важны их надежность и отказоустойчивость. Микросервисы, несмотря на свою мощь, часто сталкиваются с проблемами вроде сетевых сбоев, тайм-аутов и недоступности сервисов. Устойчивые системы создаются при помощи двух популярных шаблонов: «Повтор» и «Выключатель».
Изучим эти шаблоны, варианты их использования и эффективной реализации.
Трудности распределенных систем
Микросервисы «общаются» по сети, которая не надежна в принципе. Вот типичные проблемы:
- Временные сбои — это ошибки вроде отказа подключений к базе данных из-за кратковременного скачка трафика.
- Перегруженность сервисов — это когда последующий сервис перегружен запросами.
- Каскадные сбои — это когда сбой одного сервиса чреват эффектом домино в системе.
Шаблонами «Повтор» и «Выключатель» смягчаются последствия этих проблем, повышается надежность системы.
Шаблон «Повтор»
В рамках этого шаблона невыполненная операция выполняется повторно при условии, что сбой является временным и операция, скорее всего, успешно выполнится при последующих попытках.
«Повтор» используют, когда:
- Сбой, по-видимому, временный. Например, при проблемах с сетью.
- Операция идемпотентная, то есть многократно повторяемая.
Ключевые соображения
- Стратегия задержки: во избежание перегрузки сервиса используется экспоненциальная задержка.
- Ограничения повторов: для предотвращения бесконечных циклов задается максимальное количество повторов.
- Тайм-ауты: во избежание неопределенной блокировки ресурсов повторы сочетаются с тайм-аутами.
Пример реализации на 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")
}
}
Шаблон «Выключатель»
А этим шаблоном в системе предотвращается отправка запросов в сервис, где случился сбой. Принцип работы аналогичен электрической цепи:
- Закрытое состояние: передача запросов разрешается.
- Открытое состояние: во избежание перегрузки сервиса, где случился сбой, запросы немедленно отклоняются.
- Полуоткрытое состояние: тестированием ограниченного количества запросов проверяется, не восстановился ли сервис.
«Выключатель» используют, когда:
- Последующий сервис постоянно «сбоит» или находится в ожидании.
- Нужно защитить систему от каскадных сбоев.
Ключевые соображения
- Пороговые значения отказа: ими определяется, когда выключатель откроется.
- Механизм возвращения в исходное состояние: указывается, когда пробовать восстанавливаться — переходить в полуоткрытое состояние.
- Нейтрализации неисправности: когда выключатель открыт, предоставляется альтернативный ответ.
Пример 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());
}
}
}
}
Сочетание «Повтора» и «Выключателя»
Во многих случаях эти шаблоны друг другом дополняются. Например:
- «Повтором» обрабатываются временные сбои в рамках вызова одного сервиса.
- «Выключателем» предотвращается перегруженность последующего сервиса во время продолжительных сбоев.
Практический сценарий
Система обработки платежей, в которой:
- Для повтора невыполненных вызовов API к платежному шлюзу используется «Повтор».
- Чтобы прекратить вызовы шлюза после повторяющихся сбоев и избежать каскада сбоев в остальной системе, используется «Выключатель».
Рекомендации по реализации
- Мониторинг показателей: для отслеживания эффективности «Повтора» и «Выключателя» используйте инструменты вроде Prometheus или Elastic APM.
- Настройка конфигураций: корректируйте пороговые значения, время задержки и ограничения повторов в зависимости от потребностей системы.
- Корректный сбой: предоставляйте пользователям содержательные сообщения об ошибках или резервные ответы.
Заключение
Шаблоны «Повтор» и «Выключатель» — незаменимые инструменты для разработки отказоустойчивых микросервисов. Благодаря комбинированию этих шаблонов с надежными мониторингом и тестированием, создаются устойчивые системы, способные корректно справляться со сбоями.
Читайте также:
- Почему микросервисы нужны каждому разработчику
- Шаблон «запрос-ответ» в RabbitMQ: подход на основе EventListener в Node.js
- Микрофронтенды: 9 шаблонов для каждого разработчика
Читайте нас в Telegram, VK и Дзен
Перевод статьи Muhammad Al Ichsan Nur Rizqi Said: Designing Fault-Tolerant Microservices with Retry and Circuit Breaker Patterns