Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке circuit breaker?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Circuit breaker** - це шаблон проектування, який зупиняє запити до сервісу що падає, замість того щоб чекати таймаут. Три стани: Closed (все проходить і рахуються помилки), Open (миттєве відхилення), Half-Open (пробні запити для перевірки відновлення). Використовують для захисту мікросервісів від каскадних збоїв коли частота помилок перевищує 10-20%. **Головне:** на відміну від retry, повністю зупиняє трафік до сигналу відновлення.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Circuit breaker** - це шаблон проектування, який виявляє повторні збої при зверненні до зовнішнього сервісу і тимчасово зупиняє туди надсилати запити. Один повільний сервіс не кладе всю систему. ## Теорія ### TL;DR - Аналогія: автоматичний вимикач у щитку - спрацьовує при перевантаженні, захищає проводку, потім скидається. - Три стани: Closed (все проходить), Open (миттєве відхилення), Half-Open (пробні запити для перевірки відновлення). - Головна різниця з retry: retry збільшує навантаження на сервіс що падає; circuit breaker зупиняє весь трафік до сигналу відновлення. - Використовуй при зверненнях до зовнішніх API, баз даних, мікросервісів де частота помилок перевищує 10-20%. - Завжди визначай fallback - кешовані дані або дефолтне значення замість голої помилки. ### Швидкий приклад ```javascript const CircuitBreaker = require('opossum'); const axios = require('axios'); const breaker = new CircuitBreaker(async () => { return axios.get('https://api.example.com/inventory'); }, { timeout: 1000, // помилка якщо немає відповіді за 1с errorThresholdPercentage: 50, // відкрити після 50% помилок resetTimeout: 5000 // пробний запит через 5с }); breaker.fallback(() => ({ available: false, source: 'cache' })); breaker.fire().then(console.log); ``` Після того як 50% запитів завершились помилкою, `breaker.fire()` одразу повертає fallback замість того щоб чекати на недоступний сервіс. ### Як працюють три стани **Closed** - нормальний стан. Запити проходять, breaker рахує помилки у ковзному вікні (останні N секунд через кільцевий буфер). Коли частка помилок перевищує поріг, стан змінюється на Open. **Open** - миттєве відхилення. Breaker навіть не намагається зробити виклик: повертає `Promise.reject()` або налаштований fallback одразу. Запускається таймер відліку до наступної перевірки. **Half-Open** - пробний режим. Після закінчення `resetTimeout` breaker пропускає 1-2 запити. Якщо вони успішні, стан повертається в Closed. Якщо ні - breaker знову відкривається і таймер починається заново. Тут ховається тонка проблема: в кластері зі 100 подів усі можуть почати пробні запити одночасно і добити сервіс що вже відновлюється. Рішення - розбити проби: наприклад, тільки поди де `podIndex % 10 === 0` роблять probe, або використати leader election. ### Як breaker відстежує збої всередині Breaker тримає ковзне вікно - зазвичай реалізоване як кільцевий буфер (у Hystrix це `RingBitSet`). Кожен запит записується як біт: 0 для успіху, 1 для помилки. На кожному запиті перераховується `(failures / total) * 100`. Якщо це число перевищує `errorThresholdPercentage` І загальна кількість викликів перевищує `volumeThreshold` - breaker відкривається. Параметр `volumeThreshold` легко пропустити, але він важливий. Без нього один збій з одного загального виклику дає 100% помилок і некоректно відкриває breaker. Бібліотеки `opossum` і Resilience4j обидві надають цей параметр. ### Коли використовувати - Виклики до зовнішніх сервісів з великою затримкою (БД, API, мікросервіси) - так, потрібен circuit breaker. - Service mesh з Istio або Linkerd - логіка breaker реалізована на рівні Envoy proxy, налаштовується декларативно. - Batch-задачі що запускаються раз на годину з малим об'ємом - достатньо простого retry з exponential backoff. - Внутрішні виклики всередині одного процесу - circuit breaker не потрібен. - Частота помилок стабільно менше 5-10% - overhead без користі. Кожен сервіс або endpoint отримує окремий екземпляр breaker. Один спільний breaker для всіх викликів означає що один повільний сервіс блокує платежі, профіль користувача і все інше разом. ### Типові помилки **Занадто низький errorThresholdPercentage без volumeThreshold** Один мережевий збій дає 100% помилок, відкриває breaker і блокує абсолютно здоровий сервіс на весь час `resetTimeout`. ```javascript // Неправильно - відкривається на 1 збій з 1 виклику { errorThresholdPercentage: 1 } // Правильно - чекає мінімум 10 викликів перед оцінкою { errorThresholdPercentage: 50, volumeThreshold: 10 } ``` **Немає fallback** Без fallback клієнт отримує голу 500 або необроблений rejection. Кешовані дані або дефолтне значення набагато краще ніж сторінка помилки. ```javascript // Неправильно - caller отримує unhandled exception breaker.fire(sku).then(use); // Правильно - повернути останні відомі дані з кешу breaker.fallback(async (sku) => { const cached = await redis.get(`inventory:${sku}`); return cached ? JSON.parse(cached) : { available: false }; }); ``` **Один глобальний breaker для всіх сервісів** Якщо сервіс інвентаризації деградує і відкриває спільний breaker, всі інші виклики (платежі, профіль, доставка) також блокуються. ```javascript // Неправильно - один breaker на все const globalBreaker = new CircuitBreaker(anyCall, options); // Правильно - окремий екземпляр на кожну залежність const inventoryBreaker = new CircuitBreaker(checkInventory, options); const paymentBreaker = new CircuitBreaker(chargeCard, options); ``` **Неправильний resetTimeout** 1 секунда - breaker занадто агресивно зондує сервіс що відновлюється. 5 хвилин - система залишається в деградованому стані довго після того як downstream сервіс вже здоровий. Спирайся на реальний час відновлення залежності, зазвичай 30 секунд до 2 хвилин. ### Де зустрічається в реальних системах - Netflix - Hystrix реалізував breaker на кожну команду всередині Zuul gateway, з дашбордом для моніторингу стану в реальному часі. - Spring Cloud - Resilience4j замінив Hystrix і тепер є стандартним circuit breaker у Spring Boot. - Node.js - `opossum` (розроблений у PayPal) є стандартною бібліотекою для Express-мікросервісів. - AWS - API Gateway з Lambda використовує вбудований throttling як проксі-breaker. - Istio - Envoy sidecar-и реалізують circuit breaking на рівні mesh через конфіг `DestinationRule`, без змін у коді застосунку. ### Follow-up питання **Q:** Поясни три стани та переходи між ними без коду. **A:** Closed рахує помилки у вікні; перевищення порогу переводить в Open, який відхиляє миттєво і запускає таймер. Після таймера Half-Open пропускає 1-2 пробні запити; успіх закриває, невдача відкриває знову. **Q:** Чим circuit breaker відрізняється від retry? **A:** Retry надсилає більше запитів до сервісу що вже падає і посилює проблему під навантаженням. Circuit breaker зупиняє весь трафік до сигналу відновлення. Вони доповнюють одне одного: використовуй exponential backoff retry всередині breaker для тимчасових помилок, і відкривай breaker при стійких збоях. **Q:** Як розподілити стан circuit breaker між 50 екземплярами одного сервісу? **A:** Зберігай стан у Redis через pub/sub або розподілений лічильник. Тільки один екземпляр (обраний consistent hashing або leader election) робить probe в Half-Open, щоб уникнути thundering herd. **Q:** Яка математика за errorThresholdPercentage? **A:** `(failures / total) * 100 > threshold`, обчислюється у ковзному вікні. Hystrix використовує `RingBitSet` з останніх N викликів. Обчислення спрацьовує тільки якщо `total >= volumeThreshold`. **Q:** В service mesh зі 100 подами - як уникнути thundering herd під час Half-Open? **A:** Обмеж пробу: тільки поди де `podIndex % N === 0` надсилають probe-запит. Або використай Envoy з `outlier_detection` - Istio через `consecutiveErrors` та `interval` робить це з коробки без змін у коді. ## Приклади ### Базовий circuit breaker через opossum ```javascript const CircuitBreaker = require('opossum'); const axios = require('axios'); const breaker = new CircuitBreaker(async () => { const { data } = await axios.get('https://api.example.com/status'); return data; }, { timeout: 1000, // таймаут виклику 1с errorThresholdPercentage: 50, // відкрити після 50% збоїв у вікні resetTimeout: 5000, // half-open probe через 5с volumeThreshold: 5 // мінімум 5 викликів перед оцінкою }); breaker.fallback(() => ({ status: 'unknown', source: 'fallback' })); // Event hooks для моніторингу breaker.on('open', () => console.log('Breaker OPEN - відхиляємо запити')); breaker.on('halfOpen', () => console.log('Breaker HALF-OPEN - зондуємо')); breaker.on('close', () => console.log('Breaker CLOSED - трафік відновлено')); breaker.fire() .then(result => console.log('Відповідь:', result)) .catch(err => console.error('Відхилено:', err.message)); ``` Event hooks зручно підключити до Prometheus або DataDog щоб бачити зміни стану breaker на дашборді в реальному часі. ### Production Express API з Redis fallback Патерн для e-commerce сервісу що звертається до мікросервісу інвентаризації. ```javascript const express = require('express'); const CircuitBreaker = require('opossum'); const axios = require('axios'); const redis = require('redis'); const redisClient = redis.createClient(); // Окремий breaker на кожну залежність const inventoryBreaker = new CircuitBreaker(async (sku) => { const { data } = await axios.post('http://inventory-service/check', { sku }); return data; }, { timeout: 200, // inventory повинен відповісти за 200мс errorThresholdPercentage: 25, // відкрити після 25% збоїв resetTimeout: 10000, // чекати 10с перед probe volumeThreshold: 10 }); inventoryBreaker.fallback(async (sku) => { const cached = await redisClient.get(`inventory:${sku}`); if (cached) return JSON.parse(cached); return { available: false, source: 'default' }; }); const app = express(); app.use(express.json()); app.post('/order', async (req, res) => { const { sku } = req.body; try { const inventory = await inventoryBreaker.fire(sku); res.json({ canOrder: inventory.available }); } catch (err) { res.status(503).json({ error: 'Сервіс недоступний' }); } }); app.listen(3000); ``` Я бачив як команди пропускали `volumeThreshold` в такій схемі і потім 20 хвилин дебажили чому breaker відкривається в staging після першого cold-start таймауту. Ставте його одразу. ### Half-open probe і volumeThreshold на практиці Деталь яка часто спливає на code review. ```javascript const breaker = new CircuitBreaker(apiCall, { errorThresholdPercentage: 50, volumeThreshold: 5, // потрібно 5 викликів перш ніж % щось означає halfOpenActionCount: 2, // рівно 2 probe-запити перед рішенням resetTimeout: 3000 }); // Без volumeThreshold: // Виклик 1 падає -> 1/1 = 100% -> breaker відкривається одразу // Неправильно для сервісу з одним cold-start таймаутом // З volumeThreshold: 5: // Виклики 1-4 падають -> недостатньо даних, залишається Closed // Виклик 5 падає -> 5/5 = 100% -> breaker відкривається // Набагато реалістичніша поведінка breaker.on('halfOpen', () => { console.log('Надсилаємо 2 probe-запити перед рішенням'); }); ``` `halfOpenActionCount` також важливий у розподіленому середовищі. Якщо два probe-запити йдуть одночасно і один успішний а інший ні, результат залежить від порядку завершення. Тому частина команд залишає це значення на 1 щоб уникнути неоднозначності.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.