JavaRush /Курсы /Модуль 5. Spring /Лекция 240: Как избежать перегрузки системы и обеспечить ...

Лекция 240: Как избежать перегрузки системы и обеспечить отказоустойчивость

Модуль 5. Spring
24 уровень , 9 лекция
Открыта

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


Основные стратегии предотвращения перегрузок в микросервисах

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

Ограничение запросов (Rate Limiting)

Rate Limiting — это механизм защиты серверов от перегрузки, который работает как шлагбаум для API. Например:

  • API курса валют может позволить каждому пользователю не более 100 запросов в минуту
  • Платёжный сервис ограничивает количество транзакций с одного аккаунта
  • Сервис авторизации блокирует попытки подбора пароля, ограничивая число входов

Цель — предотвратить:

  • Случайную перегрузку системы
  • Намеренные атаки типа DDoS
  • Неэффективное использование ресурсов

Технически это как светофор для входящих запросов: пропускает определённое количество, остальные ждут или блокируются.

Пример настройки Rate Limiting на уровне Gateway с использованием Spring Cloud Gateway:


@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
    return builder.routes()
            .route("rate_limit_route", r -> r.path("/api/**")
                    .filters(f -> f.requestRateLimiter(config -> {
                        config.setRateLimiter(redisRateLimiter());
                    }))
                    .uri("http://localhost:8080"))
            .build();
}

@Bean
public RedisRateLimiter redisRateLimiter() {
    return new RedisRateLimiter(10, 20);
}

Здесь 10 — это сколько запросов может обрабатываться за секунду, а 20 — сколько запросов можно "накопить" в пике. Это работает как "запасной карман" для пиков нагрузки.


Управление очередями

Управление очередями в распределенных системах — это механизм буферизации и упорядочивания запросов. Например:

  • В брокерах сообщений (RabbitMQ, Kafka) обеспечивает последовательную обработку задач
  • Позволяет системе не терять данные при высокой нагрузке
  • Помогает балансировать нагрузку между сервисами
  • Предотвращает потерю критически важных операций

Технически это как диспетчерская для входящих задач: каждая задача встает в очередь, ожидает своей очереди и гарантированно обрабатывается.


Circuit Breaker — как средство защиты

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

Пример настройки Circuit Breaker с Resilience4j:


@Bean
public Customizer<Resilience4JCircuitBreakerFactory> defaultCustomizer() {
    return factory -> factory.configureDefault(id -> new Resilience4JConfigBuilder(id)
            .circuitBreakerConfig(CircuitBreakerConfig.custom()
                    .failureRateThreshold(50)
                    .waitDurationInOpenState(Duration.ofSeconds(5))
                    .slidingWindowSize(10)
                    .build())
            .build());
}

Здесь failureRateThreshold — процент упавших запросов, после которых Circuit Breaker откроется, а waitDurationInOpenState — пауза перед повторными попытками.


Комбинирование подходов для отказоустойчивости

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

Сценарий №1. Пиковая нагрузка (Black Friday)

  1. Rate Limiter ограничивает количество запросов от одного пользователя.
  2. Circuit Breaker активируется для защиты критически важного микросервиса при достижении порога отказов.
  3. Fallback подключается, чтобы вместо полного отказа предоставить пользователю "заглушку" (например, сообщение "Временно недоступно, попробуйте позже").
  4. Retry периодически пытается снова обработать запрос, если проблема временная.

Сценарий №2. Сбой внешнего API

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

  1. На уровне вызова внешнего API добавляется Timeout, чтобы запросы не висели вечно.
  2. Retry предпринимает дополнительные попытки, чтобы справиться с временными сбоями.
  3. Если сбой становится устойчивым, вступает в силу Circuit Breaker.
  4. Fallback возвращает пользователю ответ: "Сервис оплаты временно недоступен. Попробуйте позже."
  5. Логирование и оповещение помогают вашей команде оперативно узнать о сбое.

Практическая реализация в Spring Boot

Рассмотрим, как объединить Retry, Circuit Breaker и Fallback в одном приложении с использованием Resilience4j.

Допустим, наша задача — интегрировать отказоустойчивые механизмы в микросервис, который отправляет запросы к другому сервису (например, сервису пользователя).

Добавим зависимость в pom.xml:


<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-spring-boot2</artifactId>
    <version>2.0.2</version>
</dependency>

Настроим Circuit Breaker и Retry в файле application.yml:


resilience4j:
  circuitbreaker:
    configs:
      default:
        failure-rate-threshold: 50
        sliding-window-size: 10
        wait-duration-in-open-state: 5s
  retry:
    configs:
      default:
        max-attempts: 3
        wait-duration: 2s

Реализация вызова с Fallback


@RestController
@RequestMapping("/api/users")
public class UserController {

    private final WebClient userClient;

    public UserController(WebClient.Builder webClientBuilder) {
        this.userClient = webClientBuilder.baseUrl("http://user-service").build();
    }

    @GetMapping("/{id}")
    @CircuitBreaker(name = "userService", fallbackMethod = "fallbackUser")
    @Retry(name = "userService")
    @Timeout(name = "userService", fallbackMethod = "timeoutFallback")
    public Mono<String> getUserById(@PathVariable Long id) {
        return userClient.get()
                         .uri("/users/" + id)
                         .retrieve()
                         .bodyToMono(String.class);
    }

    private Mono<String> fallbackUser(Long id, Throwable ex) {
        return Mono.just("Fallback response for user " + id);
    }

    private Mono<String> timeoutFallback(Long id, Throwable ex) {
        return Mono.just("Request timeout for user " + id);
    }
}

Здесь мы используем:

  • @CircuitBreaker для включения защиты.
  • @Retry для повторных попыток.
  • @Timeout для ограничения времени ожидания.
  • fallbackMethod для обработки сбоев.

Примеры из реальной практики

Netflix, который изобрел Hystrix, широко практикует использование Circuit Breaker. Например, если один из их микросервисов по рекомендациям фильмов падает, вместо этого они показывают популярное содержание. Это fallback в действии.

Amazon тоже активно использует Rate Limiting для управления запросами от партнеров, предотвращая перегрузку их API.


Таким образом, объединяя подходы Rate Limiting, Circuit Breaker, Retry, Timeout и Fallback, вы создаёте систему, которая не только выдерживает нагрузки, но и остаётся отзывчивой даже во время критических отказов. Эти знания помогут вам в построении надёжных и масштабируемых приложений для реального мира, где отказоустойчивость — это не привилегия, а необходимость.

Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ