🏗️ Розділ 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 завжди повинна перезапускати контейнер негайно» (При перевантаженні це вб’є сервіс остаточно).

Пов’язані теми: