Пошук уроків, статей та іншого контенту
Спроєктуєте спрощений режим роботи, який зберігає ключові функції за недоступності другорядних залежностей.
Graceful degradation — це проєктування системи так, щоб під час недоступності другорядних компонентів вона не припиняла роботу повністю, а переходила у спрощений режим.
Наприклад, інтернет-магазин може:
показати картку товару, навіть якщо сервіс рекомендацій недоступний;
дозволити читати вже завантажені дані, якщо сервіс аналітики не відповідає;
прийняти замовлення, навіть якщо сервіс надсилання email тимчасово недоступний;
показати кешовану інформацію замість актуальної, якщо основне джерело даних недоступне.
Головна ідея:
Відмова другорядної залежності не повинна блокувати критичний користувацький сценарій.
Graceful degradation не означає, що помилки потрібно приховувати. Система має:
швидко виявити проблему;
перейти до заздалегідь визначеного fallback-сценарію;
повідомити про обмеження, якщо це важливо для користувача;
зафіксувати помилку в логах і метриках.
Перед реалізацією потрібно розділити компоненти за важливістю.
Без неї основний сценарій не має сенсу або може призвести до некоректної операції.
Приклади:
перевірка автентифікації;
отримання ціни перед оформленням замовлення;
перевірка доступного балансу;
запис платежу;
збереження замовлення.
Для критичної залежності зазвичай краще повернути помилку, ніж продовжити з неправильними даними.
Вона покращує досвід, але не є необхідною для основної операції.
Приклади:
рекомендації товарів;
персоналізація;
відправлення email після оформлення;
збір аналітики;
відображення рейтингу або додаткових відгуків.
Недоступність такої функції має переводити систему у спрощений режим.
Основний сценарій:
отримати товар;
показати назву, ціну та залишок.
Другорядний сценарій:
отримати рекомендації;
показати персоналізований блок;
завантажити додаткові відгуки.
Якщо сервіс рекомендацій недоступний, сторінка все одно повинна відкритися.
Fallback — це альтернативна поведінка у випадку помилки або недоступності залежності.
Поширені варіанти fallback:
повернути порожній список;
використати кеш;
показати стандартне значення;
використати неперсоналізований результат;
відкласти операцію до відновлення залежності;
приховати необов’язковий блок інтерфейсу.
Важливо, щоб fallback був безпечним. Наприклад, для рекомендацій порожній список може бути коректним fallback:
{
"product": {
"id": "p-42",
"name": "Механічна клавіатура",
"price": 3499
},
"recommendations": []
}Але для перевірки платежу відповідь «платіж успішний» не може бути fallback-значенням. У такому випадку операцію потрібно зупинити.
Сервіс може бути не повністю недоступним, а просто повільним. Якщо основний запит чекатиме на повільну другорядну залежність, користувач усе одно побачить повільну або невдалу сторінку.
Тому для необов’язкової залежності потрібно встановити окремий короткий тайм-аут.
Наприклад:
загальний бюджет відповіді: 1000 мс;
на сервіс рекомендацій: 200 мс;
після 200 мс використовується fallback.
Тайм-аут має бути частиною контракту залежності, а не випадковим значенням у коді.
Нижче наведено мінімальний HTTP-сервер на Node.js. Він:
завжди повертає основні дані товару;
намагається отримати рекомендації з окремого сервісу;
чекає на нього не довше 200 мс;
повертає порожній список рекомендацій у разі помилки;
додає заголовок, який показує, що відповідь була деградована.
Для запуску потрібен Node.js 18 або новіший, оскільки приклад використовує вбудований fetch.
const http = require("node:http");
const PORT = 3000;
const RECOMMENDER_URL = process.env.RECOMMENDER_URL;
const RECOMMENDER_TIMEOUT_MS = 200;
const product = {
id: "p-42",
name: "Механічна клавіатура",
price: 3499,
currency: "UAH"
};
function timeoutSignal(milliseconds) {
const controller = new AbortController();
const timer = setTimeout(() => {
controller.abort();
}, milliseconds);
// Таймер не повинен утримувати процес після завершення запиту.
timer.unref?.();
return controller;
}
async function loadRecommendations() {
if (!RECOMMENDER_URL) {
return {
items: [],
degraded: true,
reason: "dependency_not_configured"
};
}
const controller = timeoutSignal(RECOMMENDER_TIMEOUT_MS);
try {
const response = await fetch(
`${RECOMMENDER_URL}/recommendations?productId=${product.id}`,
{
signal: controller.signal,
headers: {
Accept: "application/json"
}
}
);
if (!response.ok) {
throw new Error(`Recommender returned ${response.status}`);
}
const data = await response.json();
return {
items: Array.isArray(data.items) ? data.items : [],
degraded: false
};
} catch (error) {
// Другорядна залежність не повинна блокувати основний сценарій.
console.error("Recommendations unavailable:", error.message);
return {
items: [],
degraded: true,
reason: error.name === "AbortError" ? "timeout" : "dependency_error"
};
} finally {
controller.abort();
}
}
const server = http.createServer(async (request, response) => {
if (request.method !== "GET" || request.url !== "/products/p-42") {
response.writeHead(404, {
"Content-Type": "application/json; charset=utf-8"
});
response.end(JSON.stringify({
error: "Not found"
}));
return;
}
const recommendations = await loadRecommendations();
const result = {
product,
recommendations: recommendations.items
};
const headers = {
"Content-Type": "application/json; charset=utf-8",
"Cache-Control": "no-store"
};
if (recommendations.degraded) {
headers["X-Response-Mode"] = "degraded";
} else {
headers["X-Response-Mode"] = "full";
}
response.writeHead(200, headers);
response.end(JSON.stringify(result));
});
server.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});Запуск:
node server.jsПеревірка:
curl -i http://localhost:3000/products/p-42Якщо RECOMMENDER_URL не заданий, відповідь буде успішною, але без рекомендацій:
{
"product": {
"id": "p-42",
"name": "Механічна клавіатура",
"price": 3499,
"currency": "UAH"
},
"recommendations": []
}Заголовок X-Response-Mode: degraded дозволяє відрізнити спрощену відповідь від повної. У реальному production-сервісі подібний стан також варто фіксувати в метриках, але не обов’язково показувати користувачу технічну причину.
Сформулюйте, що саме користувач повинен мати змогу зробити навіть під час часткового збою.
Наприклад:
Користувач повинен бачити товар і додавати його до кошика, навіть якщо персональні рекомендації недоступні.
Це допомагає не зробити fallback занадто широким або занадто обмеженим.
Для кожної залежності визначте:
чи є вона критичною;
який її максимальний час відповіді;
що робити при тайм-ауті;
що робити при некоректній відповіді;
чи можна використати кеш;
чи потрібно повідомляти користувача.
Приклад:
сервіс каталогу — критичний;
сервіс рекомендацій — другорядний;
сервіс аналітики — другорядний;
база замовлень — критична;
email-сервіс після замовлення — другорядний для завершення покупки.
Недоступність сервісу рекомендацій не повинна:
блокувати весь HTTP-запит;
займати всі worker-и очікуванням;
спричиняти повторні запити без обмеження;
перевести основний сервіс у стан перевантаження.
Для цього використовують:
короткі тайм-аути;
обмеження кількості повторів;
ізоляцію другорядних викликів;
fallback;
окремі ліміти ресурсів.
Спрощена відповідь має бути видимою не лише користувачу, а й команді, яка підтримує систему.
Корисні сигнали:
лічильник fallback-відповідей;
кількість тайм-аутів;
частка деградованих запитів;
час відповіді залежності;
кількість некоректних відповідей.
Лог повинен містити контекст:
назву залежності;
тип помилки;
ідентифікатор запиту;
час очікування;
endpoint або операцію.
Не варто логувати кожен технічний збій на рівні error, якщо це створює мільйони однакових записів. Рівень логування та сповіщення потрібно узгодити з важливістю проблеми.
Деградація може бути не лише двійковою — «працює» або «не працює».
Наприклад, сервіс рекомендацій може мати такі режими:
персональні рекомендації;
загальні популярні товари;
кешовані рекомендації;
порожній блок рекомендацій.
Це дозволяє зберігати частину функціональності навіть під час проблем із персоналізацією.
Водночас кожен додатковий fallback збільшує складність. Не потрібно створювати багато режимів без чіткої користі. Для кожного режиму має бути зрозуміло:
коли він вмикається;
які дані використовує;
наскільки вони актуальні;
як система повертається до повного режиму.
Кеш може зберегти доступність другорядної функції, якщо основне джерело тимчасово не відповідає.
Наприклад, для рекомендацій можна:
спочатку спробувати отримати актуальні дані;
якщо залежність не відповіла, використати кеш;
якщо кеш порожній, повернути порожній список.
При цьому потрібно враховувати актуальність даних. Для ціни, залишку або статусу платежу застарілий кеш може бути небезпечним. Для рекомендацій або популярних товарів він зазвичай прийнятніший.
Fallback на кеш має явно визначати:
максимальний вік даних;
поведінку при пошкодженому кеші;
момент видалення застарілих записів;
чи можна показувати такі дані користувачу.
Не слід продовжувати операцію зі штучними або припущеними даними, якщо це може спричинити неправильний результат.
Небезпечні приклади:
підтверджувати оплату без відповіді платіжного сервісу;
зменшувати залишок товару без успішного запису;
дозволяти доступ без перевірки прав;
вважати транзакцію виконаною через тайм-аут;
показувати неправильну ціну замість помилки.
У таких випадках краще повернути зрозумілу помилку та запропонувати повторити операцію пізніше.
Спрощений режим має бути узгоджений між backend і frontend.
Backend може:
повернути успішну відповідь із порожнім необов’язковим полем;
додати ознаку режиму;
повернути окремий статус або повідомлення для другорядного блоку.
Frontend може:
не показувати необов’язковий блок;
показати нейтральний текст «Рекомендації тимчасово недоступні»;
не блокувати основну кнопку;
не показувати технічний stack trace користувачу.
Контракт повинен бути стабільним. Якщо поле recommendations іноді є масивом, а іноді відсутнє або має інший тип, клієнт отримує додаткові помилки саме під час аварійного сценарію.
Fallback потрібно тестувати так само, як і звичайний шлях.
Перевірте щонайменше такі випадки:
залежність відповідає успішно;
залежність відповідає із запізненням;
залежність не встановлює з’єднання;
залежність повертає HTTP-помилку;
залежність повертає некоректний JSON;
кеш містить актуальні дані;
кеш порожній;
основна функція залишається доступною;
відновлення залежності повертає систему до повного режиму.
Особливо важливо перевірити тайм-аут під навантаженням. Велика кількість одночасних повільних запитів до залежності може вичерпати ресурси основного сервісу.
Мережевий виклик може зависнути надовго. У результаті необов’язкова функція стає причиною недоступності всієї сторінки.
Порожній список рекомендацій зазвичай безпечний. Але значення «платіж підтверджено» або «доступ дозволено» не можна використовувати як fallback.
Якщо система повертає fallback, але не створює логів і метрик, команда не знає, що основна залежність несправна.
Коли всі залежності викликаються однаково й обробляються однаково, помилка другорядного сервісу може зупинити критичний сценарій.
Велика кількість режимів ускладнює тестування та підтримку. Починайте з простого і безпечного fallback, а нові рівні додавайте лише за потреби.
Повідомлення на кшталт ECONNRESET або назва внутрішнього сервісу не допомагають користувачу. Технічна причина має залишатися в логах, а інтерфейс — показувати зрозумілий стан.
Graceful degradation зберігає критичні функції під час недоступності другорядних залежностей.
Спочатку потрібно визначити основний користувацький сценарій і класифікувати залежності.
Для необов’язкових викликів необхідні короткі тайм-аути та безпечні fallback-сценарії.
Кеш, стандартні значення або порожні результати можуть бути fallback, якщо вони не створюють хибних бізнес-операцій.
Критичні операції не можна підтверджувати без достовірної відповіді необхідної залежності.
Деградацію потрібно моніторити, тестувати та явно описувати в контрактах API.
Хороший спрощений режим зменшує вплив часткових збоїв, але не приховує їх від команди підтримки.