Як забезпечити відмовостійкість мікросервісів
У моноліті виклик методу в пам'яті завершується завжди успішно (або швидким винятком). У мікросервісах виклик іде ненадійною мережею через кабелі, світчі та маршрутизатори.
🟢 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 завжди повинна перезапускати контейнер негайно» (При перевантаженні це вб’є сервіс остаточно).