Пошук уроків, статей та іншого контенту
Навчимося класифікувати мережеві, апаратні й програмні відмови та проєктувати системи з урахуванням часткових збоїв.
Розподілена система складається з кількох компонентів, які взаємодіють через мережу:
клієнтів;
серверів;
баз даних;
черг повідомлень;
кешів;
зовнішніх сервісів.
У такій системі відмова одного компонента не обов’язково зупиняє всю систему. Часто частина компонентів продовжує працювати, а частина — ні. Такий стан називають частковим збоєм або частковою відмовою.
Наприклад, інтернет-магазин може:
успішно показувати каталог;
не завантажувати відгуки;
тимчасово не перевіряти бонусний баланс;
приймати замовлення, але із затримкою підтверджувати оплату.
Для користувача важливо, щоб система поводилася передбачувано навіть у такій ситуації.
Повна відмова означає, що система або великий її компонент повністю недоступний.
Приклади:
сервер не запускається;
база даних не приймає жодного з’єднання;
дата-центр втратив живлення.
Повна відмова часто легша для виявлення: компонент або доступний, або ні.
Часткова відмова складніша. Компонент може:
відповідати лише на частину запитів;
відповідати дуже повільно;
повертати помилки для окремих операцій;
втрачати лише частину мережевих пакетів;
працювати на одному сервері, але бути недоступним на іншому;
повертати застарілі або неповні дані.
Наприклад, сервіс рекомендацій може відповідати через 10 секунд замість звичних 100 мілісекунд. Формально він працює, але очікування його відповіді може заблокувати весь запит користувача.
Тому в розподіленій системі недостатньо перевіряти лише факт доступності сервісу. Потрібно враховувати:
помилки;
затримки;
тайм-аути;
неповні відповіді;
повторну доставку запитів.
Мережа між компонентами не є надійним каналом. Запит може не дійти до сервера, а відповідь може не дійти назад до клієнта.
Втрата пакетів — частина даних не доходить до адресата.
Затримка — відповідь приходить пізніше, ніж очікується.
Розрив з’єднання — активне з’єднання переривається.
Недоступність DNS — ім’я сервісу не перетворюється на IP-адресу.
Мережеве розділення — частини системи не можуть обмінюватися даними між собою.
Перевантаження — мережеві черги зростають, а відповіді стають повільними.
Особливо небезпечно те, що клієнт не завжди може визначити причину проблеми. Якщо відповідь не надійшла, це може означати:
запит не дійшов до сервера;
сервер отримав запит, але не встиг його обробити;
сервер обробив запит, але відповідь загубилася;
відповідь просто затрималася в мережі.
Тому повторення запиту може бути небезпечним. Наприклад, повторна оплата замовлення потенційно може списати кошти двічі.
Апаратні відмови виникають через фізичні компоненти інфраструктури.
Приклади:
несправність диска;
перегрів процесора;
відмова блока живлення;
втрата мережевої карти;
вимкнення сервера;
пошкодження мережевого комутатора;
втрата живлення в дата-центрі.
Апаратна відмова одного сервера не повинна зупиняти сервіс, якщо система спроєктована з надлишковістю. Для цього використовують:
кілька екземплярів сервісу;
резервні диски;
репліки баз даних;
балансувальники навантаження;
резервне живлення;
розподіл компонентів між різними зонами доступності.
Важливо: резервний компонент теж може бути налаштований неправильно. Наявність копії сама по собі не гарантує автоматичного відновлення.
Програмні відмови спричинені кодом, конфігурацією або неправильними діями під час експлуатації.
Приклади:
помилка в програмі;
нескінченний цикл;
витік пам’яті;
перевищення кількості з’єднань із базою даних;
несумісна зміна формату даних;
помилка в конфігурації;
невдале розгортання нової версії;
неправильний SQL-запит;
переповнення черги повідомлень.
Програмна відмова може поширюватися. Наприклад:
сервіс починає працювати повільніше;
його клієнти чекають довше;
зростає кількість одночасних з’єднань;
вичерпуються ресурси;
інші компоненти також починають повертати помилки.
Такий ланцюговий ефект називають каскадною відмовою.
Якщо клієнт чекає на відповідь без обмеження часу, завислий сервіс може поступово зайняти всі доступні ресурси.
Для кожного мережевого виклику потрібно визначити тайм-аут — максимальний час очікування відповіді.
function withTimeout(promise, timeoutMs) {
const timeout = new Promise((_, reject) => {
setTimeout(() => {
reject(new Error("Запит перевищив допустимий час"));
}, timeoutMs);
});
return Promise.race([promise, timeout]);
}
function callRecommendationService() {
return new Promise((resolve) => {
// Імітуємо повільний зовнішній сервіс
setTimeout(() => {
resolve(["Ноутбук", "Миша"]);
}, 1500);
});
}
async function getRecommendations() {
try {
const recommendations = await withTimeout(
callRecommendationService(),
500
);
return {
recommendations,
source: "recommendation-service"
};
} catch (error) {
// Основна сторінка не повинна падати через необов'язкову функцію
return {
recommendations: [],
source: "fallback"
};
}
}
getRecommendations().then((result) => {
console.log(result);
});У цьому прикладі сервіс рекомендацій відповідає через 1,5 секунди, але клієнт чекає лише 0,5 секунди. Після перевищення тайм-ауту система повертає порожній список замість того, щоб блокувати всю сторінку.
Не всі компоненти однаково важливі для відповіді.
Наприклад, для сторінки товару:
база даних товарів — обов’язкова;
сервіс цін — можливо, обов’язковий;
сервіс рекомендацій — необов’язковий;
сервіс відгуків — може бути необов’язковим;
система аналітики — не повинна блокувати відповідь користувачу.
Для необов’язкової залежності можна використати запасний сценарій:
порожній список;
кешовані дані;
стандартне значення;
повідомлення про тимчасову недоступність функції.
Кожен виклик іншого сервісу має завершуватися одним із результатів:
успішна відповідь;
помилка;
тайм-аут.
Без тайм-ауту неможливо гарантувати, що ваш сервіс звільнить ресурси.
Тайм-аут повинен бути узгоджений із загальним часом відповіді. Якщо весь HTTP-запит має тривати не більше секунди, не можна встановлювати внутрішній виклик із тайм-аутом у п’ять секунд.
Повторний запит може допомогти при короткочасній мережевій проблемі. Але він збільшує навантаження і може повторно виконати операцію.
Повторення зазвичай безпечніше для операцій читання, наприклад:
отримання профілю;
завантаження каталогу;
читання статусу замовлення.
Для операцій зміни стану потрібно особливо уважно ставитися до повторів:
створення платежу;
оформлення замовлення;
списання коштів;
надсилання повідомлення.
Для таких операцій застосовують ідемпотентність: повторне виконання того самого запиту не повинно створювати додатковий ефект. Наприклад, запит на оплату може мати унікальний ідентифікатор операції, за яким сервер розуміє, що платіж уже оброблявся.
Нескінченні повтори можуть перетворити тимчасову проблему на перевантаження системи.
Зазвичай потрібно:
обмежити кількість спроб;
додавати паузу між спробами;
збільшувати паузу після кожної невдалої спроби;
припиняти повтори після перевищення загального дедлайну.
Якщо сервіс недоступний тривалий час, краще швидко повернути контрольований результат, ніж постійно створювати нові запити.
Граціозна деградація означає, що система зберігає основну функціональність, навіть якщо другорядний компонент недоступний.
Приклад для відеосервісу:
відео продовжує відтворюватися;
рекомендації тимчасово не показуються;
історія перегляду може синхронізуватися пізніше.
Приклад для магазину:
каталог доступний;
пошук працює;
відгуки тимчасово приховані;
персональні рекомендації замінені популярними товарами.
Деградація має бути передбаченою частиною дизайну, а не випадковою поведінкою під час аварії.
Якщо один екземпляр сервісу відмовив, запит можна спрямувати до іншого.
Для цього потрібні:
кілька екземплярів сервісу;
механізм перевірки їхнього стану;
балансувальник або інший компонент маршрутизації;
узгоджена конфігурація екземплярів.
Резервний екземпляр повинен регулярно перевірятися. Інакше під час аварії може виявитися, що він також непрацездатний.
Одна залежність не повинна забирати всі ресурси сервісу.
Наприклад, можна окремо обмежувати:
кількість з’єднань із кожним зовнішнім сервісом;
кількість одночасних запитів;
розмір черги;
час обробки;
кількість повторних спроб.
Тоді повільний сервіс рекомендацій не заблокує обробку платежів.
Система повинна не лише переживати відмови, а й повідомляти про них.
Корисні сигнали:
кількість помилок;
кількість тайм-аутів;
середня та максимальна затримка;
кількість повторних спроб;
кількість активних з’єднань;
довжина черг;
кількість недоступних екземплярів.
Логи повинні містити достатньо даних для пошуку проблеми:
час події;
назву компонента;
тип операції;
ідентифікатор запиту;
результат;
тривалість виконання.
Під час проєктування корисно поставити питання:
Що станеться, якщо ця залежність перестане відповідати?
Для кожної залежності визначте:
чи є вона обов’язковою;
який у неї тайм-аут;
чи дозволені повтори;
який запасний результат;
чи може її перевантаження вплинути на інші компоненти;
як команда дізнається про проблему.
Це допомагає обмежити радіус відмови. Якщо рекомендації недоступні, це не повинно робити недоступними платежі та каталог.
Розглянемо запит для сторінки профілю:
profile-service повертає основні дані;
notification-service повертає кількість непрочитаних повідомлень;
avatar-service повертає URL аватара.
Основні дані профілю є обов’язковими. Інші два сервіси — необов’язкові.
Можлива стратегія:
якщо profile-service недоступний, повернути помилку сторінки;
якщо notification-service недоступний, показати нуль або приховати лічильник;
якщо avatar-service недоступний, показати стандартний аватар;
для кожного виклику встановити окремий тайм-аут;
не чекати другорядні сервіси довше, ніж дозволяє загальний дедлайн.
Так система не ототожнює відмову одного сервісу з відмовою всієї функції.
Під час проєктування компонента можна діяти так:
Перелічити всі його залежності.
Розділити їх на обов’язкові та необов’язкові.
Для кожної залежності описати мережеві, апаратні та програмні відмови.
Визначити тайм-аут.
Вирішити, чи дозволені повторні спроби.
Описати запасний сценарій.
Перевірити, чи не виникає каскадна відмова.
Додати метрики та логи для виявлення проблеми.
Мережевий виклик може бути повільним або завершитися без відповіді навіть між компонентами в одному дата-центрі.
Очікування без обмеження часу може призвести до накопичення завислих запитів і вичерпання ресурсів.
Повторна спроба може дублювати операцію зміни стану або збільшити навантаження на вже несправний сервіс.
Якщо рекомендації або аналітика недоступні, основна функція не повинна автоматично ставати недоступною.
Резервний сервер, копія даних або запасний маршрут повинні регулярно перевірятися. Інакше відмова основного компонента може виявити проблеми і в резервному.
Користувачеві можна показати спрощене повідомлення, але система повинна розрізняти тайм-аут, мережеву помилку, помилку програми та недоступність залежності.
Розподілені системи можуть зазнавати мережевих, апаратних і програмних відмов.
Частковий збій означає, що частина системи працює, а частина — ні.
Мережа може втрачати пакети, затримувати відповіді або розділяти компоненти.
Апаратні відмови зменшують доступність окремих серверів і пристроїв.
Програмні помилки можуть спричинити каскадні відмови.
Тайм-аути захищають систему від завислих викликів.
Повторні спроби потрібно обмежувати та використовувати обережно.
Необов’язкові залежності повинні мати запасний сценарій.
Резерви, обмеження ресурсів, моніторинг і деградація функціональності допомагають системі продовжувати роботу під час часткових збоїв.