🏗️ Раздел 17 · Вопрос #17

Как обеспечить отказоустойчивость микросервисов

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


🟢 Junior Level

Отказоустойчивость (Fault Tolerance / Resilience) — это свойство распределённой системы продолжать корректное функционирование (возможно, с частичной деградацией функционала) при полном или частичном отказе одного или нескольких микросервисов, сетевого оборудования или узлов инфраструктуры.

Классический каскадный сбой (Без защиты):
[Клиент] ──► [Сервис A] ──► [Сервис B] ──► [Сервис C (УПАЛ 💥)]
                 │               │ (Зависание по таймауту 30 сек)
                 ▼               ▼
         [Сервис A УПАЛ 💥] ◄── [Сервис B УПАЛ 💥] (Все потоки исчерпаны)

Отказоустойчивая система (Graceful Degradation):
[Клиент] ──► [Сервис A] ──► [Сервис B] ──(Circuit Breaker: OPEN)──► [Сервис C (УПАЛ)]
                                 │
                                 └──► [Fallback: ответ из кэша / дефолт ✅]

«Design for Failure» (Проектирование под неизбежные сбои)

В монолите вызов метода в памяти завершается всегда успешно (или быстрым исключением). В микросервисах вызов идёт по ненадежной сети через кабели, свитчи и маршрутизаторы. Главный постулат: Любой микросервис, база данных или сеть могут упасть в любую секунду. Система обязана быть спроектирована так, чтобы падение сервиса отзывов не мешало пользователю положить товар в корзину и оплатить его.


🟡 Middle Level

1. Пять ключевых паттернов стабильности Resilience4j

Паттерн Принцип действия От чего защищает
1. Timeout (Таймаут) Прерывает ожидание ответа через заданный лимит времени (например, 1–2 сек). Защищает потоки исполнения от вечного зависания при сетевых задержках.
2. Circuit Breaker Размыкает цепь при росте процента ошибок/задержек, мгновенно отклоняя запросы (Fail-Fast). Защищает вызывающий сервис от истощения ресурсов, а падающий — от лавины запросов.
3. Retry with Backoff & Jitter Повторяет упавший запрос с экспоненциально растущей паузой и случайной добавкой времени. Сглаживает кратковременные сетевые сбои (Network Glitches), избегая эффекта толпы.
4. Bulkhead (Переборка) Изолирует пулы потоков или семафоры под разные внешние зависимости. Проблема в медленном сервисе рекомендаций не может занять потоки сервиса оплаты.
5. Fallback (Деградация) Запасной план: возврат данных из кэша, пустой список или вызов резервного сервиса. Обеспечивает приемлемый пользовательский опыт вместо экрана с 500 ошибкой.

2. Реализация комплексной защиты в Spring Boot

@Service
@RequiredArgsConstructor
@Slf4j
public class ProductCatalogClient {

    private final RestClient restClient;

    // Порядок аспектов Resilience4j критичен:
    // Bulkhead -> CircuitBreaker -> Retry -> TimeLimiter
    @Bulkhead(name = "catalogService", fallbackMethod = "fallbackProducts")
    @CircuitBreaker(name = "catalogService", fallbackMethod = "fallbackProducts")
    @Retry(name = "catalogService")
    @TimeLimiter(name = "catalogService")
    public CompletableFuture<List<ProductDto>> getTopProductsAsync() {
        return CompletableFuture.supplyAsync(() -> restClient.get()
            .uri("https://catalog-service/api/v1/top")
            .retrieve()
            .body(new ParameterizedTypeReference<List<ProductDto>>() {}));
    }

    // Fallback метод для плавной деградации
    public CompletableFuture<List<ProductDto>> fallbackProducts(Throwable ex) {
        log.warn("Каталог недоступен ({}), отдаем статический дефолтный список", ex.getMessage());
        return CompletableFuture.completedFuture(List.of(
            new ProductDto(0L, "Популярный товар (кэш)", BigDecimal.ZERO)
        ));
    }
}

🔴 Senior Level

1. Уровни обеспечения отказоустойчивости в Enterprise

Отказоустойчивость не достигается только кодом. Она строится эшелонированно:

[Уровень 1: Инфраструктура] ──► Multi-Region / Multi-AZ в облаке, резервные каналы сети
[Уровень 2: Платформа K8s]  ──► Liveness/Readiness, Pod Disruption Budgets, HPA автомасштабирование
[Уровень 3: Сеть / Mesh]     ──► Envoy Service Mesh, mTLS, автоматический переброс трафика
[Уровень 4: Приложение]      ──► Circuit Breaker, Bulkhead, Idempotency, Transactional Outbox
[Уровень 5: Данные]          ──► Репликация БД, Saga паттерн, Eventual Consistency

2. Load Shedding (Сброс нагрузки) vs Rate Limiting

Когда система подвергается аномальной перегрузке (DDoS, пик трафика):

  • Rate Limiting: Ограничивает клиентов по статическим квотам (например, 100 req/sec на пользователя). Не спасает, если миллион легитимных пользователей пришли одновременно.
  • Load Shedding (Сброс лишней нагрузки): Сервис динамически анализирует свое внутреннее состояние: глубину очереди задач, утилизацию CPU и время ожидания потоков.
    • По закону Литтла ($L = \lambda W$), если сервис перегружен, время ответа стремится к бесконечности.
    • Сервис начинает принудительно сбрасывать низкоприоритетные запросы с кодом HTTP 503 Service Unavailable, чтобы гарантировать быстрое и качественное обслуживание высокоприоритетных запросов (чекаут, оплата).

3. Chaos Engineering: Проверка боем

Нельзя утверждать, что система отказоустойчива, пока она не испытана намеренным разрушением в контролируемой среде:

  • Chaos Monkey / Netflix Simian Army: Случайное убийство подов и виртуальных машин в продакшене в рабочие часы.
  • Chaos Mesh / LitmusChaos: Искусственная инъекция задержек сети (Network Latency 500ms), потеря 20% сетевых пакетов (Packet Loss), симуляция заполнения диска (Disk Fill) и заморозка JVM (JVM Pause). Цель — убедиться, что системы мониторинга (Prometheus/Grafana) поднимают алерты, а Circuit Breaker и Fallback срабатывают штатно без участия человека.

4 Tricky Questions

1. Почему неправильная настройка Liveness Probe в Kubernetes может убить перегруженный сервис вместо его спасения?

Ответ: Liveness Probe проверяет, жив ли процесс внутри контейнера. Если процесс завис в дедлоке — Kubelet перезапускает под. Ловушка перегрузки: Когда сервис захлебывается от входящего трафика, его CPU утилизирован на 100%, а все потоки заняты. Liveness HTTP-запрос от Kubernetes не успевает выполниться за таймаут (например, 1 сек). K8s решает, что под мертв, и убивает его. При рестарте холодный под тратит секунды на запуск JVM и JIT-компиляцию. На него снова обрушивается трафик, он снова не отвечает на пробу и уходит в бесконечный цикл перезапуска (CrashLoopBackOff / Death Spiral). Правило: Liveness Probe должна быть примитивной (проверка запуска процесса или in-memory флага), а снимать трафик с перегруженного пода должна Readiness Probe.

2. В чем разница между показателями RTO (Recovery Time Objective) и RPO (Recovery Point Objective)?

Ответ:

  • RTO (Время восстановления): Максимально допустимое время, в течение которого сервис может оставаться недоступным после катастрофы (например, RTO = 15 минут означает, что через 15 минут резервный кластер обязан подняться).
  • RPO (Точка восстановления / Потеря данных): Максимально допустимый объем потерянных данных, измеряемый во времени (например, если бэкапы делаются раз в сутки, при аварии в 23:59 RPO составляет 24 часа потерянных транзакций). В Highload Enterprise финансовые системы требуют $RPO \approx 0$ (синхронная репликация без потери данных) и $RTO < 1$ минуты (горячий standby в другом дата-центре).

3. Что произойдет, если в Resilience4j аспект @Retry применится поверх @CircuitBreaker?

Ответ: Если @CircuitBreaker находится внутри @Retry: Каждая неудачная попытка метода будет фиксироваться Circuit Breaker как отдельная ошибка в кольцевом буфере. Если настроено maxAttempts = 3, то всего один сбойный запрос пользователя добавит в метрики Circuit Breaker сразу 3 ошибки подряд. Это приведет к искусственному трехкратному завышению метрики failureRate и преждевременному ложному размыканию предохранителя. Правило: @CircuitBreaker обязан быть внешним по отношению к @Retry (или использоваться порядок по умолчанию в resilience4j-spring-boot).

4. Почему паттерн Fallback опасен при работе с транзакционными операциями изменения данных (Write / Mutation)?

Ответ: Fallback идеален для операций чтения (вернуть кэш или пустой список). Но в операциях записи (POST /api/v1/orders/charge): Если списание упало, нельзя в Fallback-методе молча возвращать success = false или дефолтный ответ, так как вызывающий сервис посчитает операцию завершенной. На самом деле деньги могли списаться в банке, но ответ упал по сетевому таймауту. В операциях мутации Fallback должен либо выбрасывать строго типизированную доменную ошибку, либо инициировать компенсирующую транзакцию в рамках Saga, но никогда не маскировать сбой.


🎯 Шпаргалка для интервью

Главное:

  • Отказоустойчивость — способность системы работать при падении ее частей (принцип Graceful Degradation).
  • Пять столпов Resilience4j: Timeout, Circuit Breaker, Retry с Jitter, Bulkhead, Fallback.
  • Всегда ограничивайте время ожидания: сетевые вызовы без таймаутов — гарантия падения в проде.
  • Fail-Fast: Лучше мгновенно отказать клиенту ошибкой через Circuit Breaker, чем копить зависшие потоки.
  • Не путайте Rate Limiting (квота клиента) и Load Shedding (сброс лишнего трафика для спасения CPU сервиса).
  • В Kubernetes: не используйте тяжелые вычисления в Liveness Probe, чтобы избежать CrashLoop спирали смерти.
  • Отказоустойчивость проверяется на практике через Chaos Engineering (Chaos Mesh).

Красные флаги (Чего говорить НЕЛЬЗЯ):

  • ❌ «Мы не настраиваем таймауты, пусть запрос выполняется столько, сколько нужно» (Исчерпает весь пул потоков за 10 секунд).
  • ❌ «Fallback должен тихо ловить все ошибки и возвращать null при списании денег» (Приведет к рассогласованию финансов и потере заказов).
  • ❌ «Если сервис упал, Liveness Probe в K8s всегда должна перезапускать контейнер немедленно» (При перегрузке это убьет сервис окончательно).

Связанные темы: