Пошук уроків, статей та іншого контенту
Навчитеся контрольовано відкидати низькопріоритетне навантаження, щоб зберегти критичні операції системи.
Load shedding — це контрольоване відкидання частини запитів під час перевантаження системи.
Коли вхідний потік запитів перевищує пропускну здатність сервісу, система може:
збільшувати чергу та затримки;
вичерпати пам’ять;
перевантажити базу даних;
почати повертати помилки для всіх клієнтів;
повністю зупинитися через каскадне перевантаження.
Load shedding змінює поведінку системи: замість спроби обробити всі запити вона відкидає менш важливі, щоб зберегти критичні операції.
Наприклад, під час пікового навантаження можна:
приймати платежі та операції входу в систему;
відкидати генерацію рекомендацій;
відкладати аналітичні події;
повертати спрощену відповідь для некритичних сторінок.
Головний принцип:
Краще контрольовано відмовити в низькопріоритетній операції, ніж зробити недоступною всю систему.
Rate limiting обмежує кількість запитів від клієнта або групи клієнтів за певний період.
Наприклад:
не більше 100 запитів на хвилину для одного користувача;
не більше 1000 запитів на секунду для одного API-ключа.
Rate limiting захищає систему від надмірного трафіку конкретного джерела.
Load shedding реагує на поточний стан системи:
кількість активних операцій;
довжину черги;
час відповіді;
використання CPU або пам’яті;
кількість з’єднань із базою даних.
Навіть дозволений rate limit може створити перевантаження, якщо запити стали повільнішими. Load shedding враховує саме доступну ємність системи.
Circuit breaker зазвичай зупиняє виклики до несправної залежності, наприклад бази даних або зовнішнього API.
Load shedding відкидає вхідне навантаження сервісу, щоб сервіс не перевантажив себе або свої залежності.
Щоб відкидати запити контрольовано, потрібно класифікувати їх за важливістю.
Приклад пріоритетів:
Критичні
створення платежу;
підтвердження замовлення;
автентифікація;
аварійні команди.
Важливі
перегляд балансу;
отримання статусу замовлення;
оновлення профілю.
Низькопріоритетні
рекомендації;
побудова звітів;
збір необов’язкової аналітики;
другорядні фонові операції.
Пріоритет має визначатися на стороні сервера. Не варто безумовно довіряти заголовку від клієнта:
X-Priority: criticalЯкщо клієнт може сам оголосити будь-який запит критичним, він обійде механізм захисту. Пріоритет краще визначати за маршрутом, типом операції, правами користувача або внутрішньою політикою сервісу.
Коли система наближається до межі, спочатку відхиляються низькопріоритетні запити.
Наприклад:
до 50 активних операцій приймаються всі запити;
від 50 до 80 — відхиляються низькопріоритетні;
від 80 до 100 — приймаються лише критичні;
після 100 — відхиляються всі нові запити.
Для критичних операцій можна зарезервувати частину ресурсів.
Наприклад, із 100 слотів:
80 доступні всім операціям;
20 залишаються для критичних операцій.
Так некритичне навантаження не займе всі ресурси.
Необмежена черга лише відкладає проблему. Запит, який чекає кілька хвилин, часто вже не має практичної цінності.
Краще мати:
обмежену кількість активних операцій;
обмежену чергу;
явну відмову після досягнення межі.
Це називають bounded concurrency або обмеженою паралельністю.
Не кожну операцію потрібно повністю відкидати. Іноді можна зменшити її вартість:
повернути кешовані дані;
не завантажувати рекомендації;
повернути коротку відповідь замість повного звіту;
відкласти необов’язкову обробку в чергу.
Це називають graceful degradation — контрольованим погіршенням функціональності.
Для тимчасової відмови через перевантаження зазвичай використовують:
503 Service UnavailableКорисно додати:
Retry-After: 1Це повідомляє клієнту, що повторну спробу варто зробити приблизно через одну секунду.
429 Too Many Requests більше підходить для обмеження частоти запитів конкретного клієнта. Якщо причина відмови — загальне перевантаження сервісу, доречніший статус 503.
Відповідь може містити машинно-читаний код:
{
"error": "overloaded",
"message": "Сервіс тимчасово перевантажений"
}Клієнт не повинен негайно повторювати такий запит у циклі. Для повторних спроб потрібні:
експоненційна затримка;
випадкове розсіювання затримки, або jitter;
максимальна кількість спроб;
перевірка, чи операція є безпечною для повторення.
Нижче наведено самодостатній HTTP-сервер без сторонніх бібліотек. Він:
обмежує кількість одночасних операцій;
приймає всі пріоритети при низькому навантаженні;
відкидає низькопріоритетні запити раніше;
резервує частину ємності для критичних операцій;
повертає 503 для відкинутих запитів.
const http = require("node:http");
const PORT = 3000;
const MAX_ACTIVE_REQUESTS = 4;
// Перші два слоти доступні всім пріоритетам.
// Останні два фактично резервуються для важливіших операцій.
const LOW_PRIORITY_LIMIT = 2;
const NORMAL_PRIORITY_LIMIT = 3;
let activeRequests = 0;
let rejectedRequests = 0;
function getPriority(request) {
const value = request.headers["x-priority"];
if (value === "critical") {
return "critical";
}
if (value === "normal") {
return "normal";
}
return "low";
}
function canAccept(priority) {
if (priority === "critical") {
return activeRequests < MAX_ACTIVE_REQUESTS;
}
if (priority === "normal") {
return activeRequests < NORMAL_PRIORITY_LIMIT;
}
return activeRequests < LOW_PRIORITY_LIMIT;
}
function sendJson(response, statusCode, body, headers = {}) {
response.writeHead(statusCode, {
"Content-Type": "application/json; charset=utf-8",
...headers
});
response.end(JSON.stringify(body));
}
const server = http.createServer((request, response) => {
if (request.url !== "/operation" || request.method !== "GET") {
sendJson(response, 404, {
error: "not_found"
});
return;
}
const priority = getPriority(request);
if (!canAccept(priority)) {
rejectedRequests += 1;
console.log(
`Відкинуто запит: priority=${priority}, active=${activeRequests}`
);
sendJson(
response,
503,
{
error: "overloaded",
message: "Сервіс тимчасово перевантажений"
},
{
"Retry-After": "1"
}
);
return;
}
activeRequests += 1;
console.log(
`Прийнято запит: priority=${priority}, active=${activeRequests}`
);
// Імітуємо повільну операцію: запит займає слот 1 секунду.
setTimeout(() => {
activeRequests -= 1;
sendJson(response, 200, {
status: "completed",
priority,
activeRequests
});
console.log(
`Завершено запит: priority=${priority}, active=${activeRequests}`
);
}, 1000);
});
setInterval(() => {
console.log(
`Метрики: active=${activeRequests}, rejected=${rejectedRequests}`
);
}, 5000);
server.listen(PORT, () => {
console.log(`Сервер запущено на http://localhost:${PORT}`);
});Збережіть код у файл server.js і запустіть:
node server.jsВ іншому терміналі можна надіслати кілька запитів:
curl -i -H "X-Priority: low" http://localhost:3000/operation
curl -i -H "X-Priority: normal" http://localhost:3000/operation
curl -i -H "X-Priority: critical" http://localhost:3000/operationЯкщо одночасно вже виконуються дві операції, новий запит із пріоритетом low отримає 503, а критичний запит ще може бути прийнятий.
Це спрощений приклад. У реальному сервісі межі можуть залежати від:
конкретного маршруту;
кількості з’єднань із базою даних;
середньої тривалості операцій;
поточного часу відповіді;
доступної пам’яті;
типу користувача або операції.
Розглянемо систему без резервування:
Низькопріоритетні запити займають усі робочі слоти.
Надходить критична операція.
Для неї немає вільного ресурсу.
Система відкидає як некритичні, так і критичні запити.
Резервування не гарантує успіх критичної операції, якщо всі слоти вже зайняті. Але воно не дозволяє некритичному трафіку використати весь доступний ресурс заздалегідь.
Для складніших систем можна використовувати окремі пули:
окремий пул для критичних операцій;
окремий пул для звичайних операцій;
окрему чергу для фонових задач.
Таке розділення зменшує взаємний вплив різних типів навантаження.
Механізм відкидання запитів без метрик важко налаштувати й діагностувати.
Варто збирати щонайменше:
кількість прийнятих запитів за пріоритетами;
кількість відкинутих запитів за пріоритетами;
частку відповідей 503;
кількість активних операцій;
довжину черги;
час очікування до початку обробки;
тривалість операцій;
кількість помилок залежностей.
Особливо важливо розрізняти причини відмови:
overloaded — сервіс перевантажений;
dependency_unavailable — недоступна залежність;
rate_limited — перевищено ліміт клієнта;
timeout — операція перевищила допустимий час.
Це допомагає зрозуміти, чи load shedding справді захищає систему, а не приховує іншу проблему.
Load shedding може погіршити ситуацію, якщо клієнти масово повторюють відхилені запити.
Небезпечний сценарій:
Сервіс перевантажений.
Він повертає 503.
Тисячі клієнтів негайно повторюють запит.
Навантаження зростає ще більше.
Сервіс продовжує відкидати запити.
Клієнтські повторні спроби мають використовувати backoff:
async function requestWithRetry(url, maxAttempts = 4) {
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
const response = await fetch(url);
if (response.ok) {
return response.json();
}
if (response.status !== 503) {
throw new Error(`Запит завершився зі статусом ${response.status}`);
}
const baseDelay = 200 * 2 ** attempt;
const jitter = Math.floor(Math.random() * 100);
await new Promise((resolve) => {
setTimeout(resolve, baseDelay + jitter);
});
}
throw new Error("Не вдалося виконати запит після повторних спроб");
}Важливо повторювати лише операції, для яких це безпечно. Наприклад, повторна спроба читання зазвичай безпечніша за повторне створення платежу.
Для операцій, які можуть виконатися більше одного разу, потрібен окремий механізм ідемпотентності. Інакше повторна спроба може створити дубль.
Практичний алгоритм можна сформулювати так:
Визначити критичні та некритичні операції.
Встановити обмеження активної роботи або розмір черги.
Визначити пороги для кожного пріоритету.
Перевіряти можливість прийняття запиту до початку дорогої обробки.
Відкидати низькопріоритетні запити першими.
Повертати зрозумілий статус і код помилки.
Додавати метрики та структуровані логи.
Перевірити поведінку клієнтів під час отримання 503.
Протестувати систему під поступовим і раптовим навантаженням.
Випадкове відкидання не гарантує збереження критичних операцій. Відмова має базуватися на пріоритеті, вартості або джерелі навантаження.
Недовірений клієнт зможе позначити всі свої запити як критичні. Політика пріоритетів має застосовуватися сервером.
Черга без обмеження споживає пам’ять і збільшує час очікування. Потрібна максимальна довжина черги та стратегія відмови після її заповнення.
Якщо запит уже зайняв з’єднання, пам’ять або з’єднання з базою даних, пізня відмова може не захистити систему. Перевірку потрібно виконувати на етапі admission control — до початку дорогої роботи.
503 сигналізує про тимчасову недоступність сервісу через перевантаження. 429 призначений переважно для обмеження частоти запитів клієнта.
Агресивний retry без backoff може перетворити тимчасове перевантаження на тривалу аварію.
Занадто великий резерв зменшує загальну пропускну здатність. Його потрібно налаштовувати на основі метрик і навантажувальних тестів.
Load shedding контрольовано відкидає частину навантаження під час перевантаження.
Спочатку потрібно відкидати низькопріоритетні операції, зберігаючи критичні.
Для критичних операцій корисно резервувати частину ресурсів.
Обмежені черги та обмежена паралельність безпечніші за необмежене накопичення запитів.
Для тимчасової відмови через перевантаження зазвичай використовується 503 Service Unavailable.
Клієнти мають повторювати запити з backoff і jitter, а не негайно надсилати їх знову.
Метрики відкинутих запитів, активних операцій і затримок необхідні для правильного налаштування механізму.