Питання на співбесіді: Масштабування й продуктивність
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
14 питань
Вертикальне масштабування (scale up) - зробити сервер потужнішим: більше процесорів, пам'яті, швидші диски.
Горизонтальне (scale out) - додати ще сервери й розподілити навантаження між ними.
| Вертикальне | Горизонтальне | |
|---|---|---|
| зміни в коді | майже не потрібні | застосунок має бути готовим |
| межа | найпотужніший доступний сервер | практично немає |
| відмовостійкість | одна точка відмови | відмова одного сервера не зупиняє систему |
| вартість | дорожчає нелінійно | лінійна, дешеві сервери |
| складність | низька | балансувальник, спільний стан, деплой на кілька машин |
Почати варто з вертикального. Сучасний сервер з 32 ядрами й 128 ГБ пам'яті витримує дуже багато - для більшості Laravel-проєктів це роки росту без зміни архітектури. Передчасне горизонтальне масштабування додає складність без потреби.
Горизонтальне потрібне, коли:
- вертикальне вперлося в межу чи стало непропорційно дорогим;
- потрібна відмовостійкість - навіть невеликий сервіс з вимогою «працювати під час оновлення сервера» потребує щонайменше двох екземплярів;
- навантаження стрибкоподібне (розпродажі, пікові години) - простіше додати й прибрати сервери.
Що треба зробити в застосунку, щоб масштабуватися горизонтально - прибрати стан з окремого сервера:
- сесії - у Redis чи базі, а не у файлах;
- завантажені файли - в S3/R2, а не на локальний диск;
- кеш і блокування - у спільному Redis;
- черги - Redis/SQS, воркери на окремих машинах;
- планувальник - запуск на одному сервері (
onOneServer()).
Різні шари масштабуються по-різному: вебсервери горизонтально прості (вони без стану), а база даних - найскладніша частина: її зазвичай спершу масштабують вертикально, потім репліками для читання, і лише в крайньому разі - шардингом.
Застосунок без стану (stateless) - будь-який запит може обробити будь-який сервер, бо жоден сервер не зберігає даних, потрібних для наступних запитів. Тоді балансувальник може розподіляти запити довільно, а сервери - додавати, прибирати й перезапускати без втрати даних.
Що зазвичай «прилипає» до сервера в Laravel-застосунку:
1. Сесії. Драйвер file зберігає сесії на локальному диску. Запит на інший сервер - користувач «розлогінений».
Рішення: SESSION_DRIVER=redis чи database.
2. Завантажені файли. Storage::disk('local') пише на диск конкретного сервера - файл, завантажений на сервер A, недоступний на сервері B.
Рішення: спільне сховище - S3, R2, MinIO (FILESYSTEM_DISK=s3).
3. Кеш. Драйвер file чи array - у кожного сервера свій кеш: інвалідація на одному не діє на інших, а атомарні блокування (Cache::lock) не працюють між серверами.
Рішення: CACHE_STORE=redis.
4. Черги. Драйвер sync виконує задачі в запиті; database - працює, але під навантаженням краще Redis чи SQS.
5. Планувальник. schedule:run на кожному сервері запустить задачу N разів.
Рішення: ->onOneServer() (потребує спільного кешу для блокування) або окремий сервер для планувальника.
6. Локальні змінні середовища й файли конфігурації - однакові на всіх серверах, деплой з одного артефакту (Docker-образ).
7. Ліміти частоти (throttle) - лічильники в кеші: з локальним кешем кожен сервер рахує окремо, і реальний ліміт множиться на кількість серверів.
«Липкі сесії» (sticky sessions) на балансувальнику - обхідний шлях: користувача завжди відправляють на той самий сервер. Працює, але сервер стає точкою відмови для своїх користувачів, а навантаження розподіляється нерівномірно.
Перевірка готовності: запустити два екземпляри за балансувальником і пройти основні сценарії - вхід, завантаження файлу, черги, кеш. Те, що зламається, і є прихованим станом.
Балансувальник навантаження приймає запити клієнтів і розподіляє їх між кількома серверами застосунку. Для клієнта це одна адреса, а за нею - пул серверів.
Що він дає:
- масштабування - навантаження ділиться між серверами;
- відмовостійкість - перевірки стану (health checks) виключають несправний сервер з пулу;
- оновлення без простою - сервери оновлюються по черзі, поки решта обслуговує запити;
- завершення TLS - шифрування обробляється на балансувальнику, а не на кожному сервері.
Рівні балансування:
- L4 (транспортний) - розподіляє TCP-з'єднання за IP і портом, не дивлячись у вміст. Швидко й просто;
- L7 (прикладний) - бачить HTTP: може маршрутизувати за шляхом (
/api- на одні сервери,/- на інші), заголовками, cookie, кешувати, стискати.
Алгоритми розподілу:
- round robin - по черзі; найпростіший, добрий для однакових серверів і схожих запитів;
- weighted round robin - потужніші сервери отримують більше запитів;
- least connections - на сервер з найменшою кількістю активних з'єднань; кращий, коли запити дуже різні за тривалістю;
- IP hash / consistent hashing - той самий клієнт потрапляє на той самий сервер (корисно для кешів на сервері, але це «липкість»);
- random with two choices - вибрати два випадкові сервери й узяти менш завантажений: просто й ефективно у великих пулах.
Health checks - балансувальник регулярно запитує, наприклад, /up (у Laravel 11+ такий маршрут налаштовано за замовчуванням). Сервер, що не відповідає, тимчасово виключається.
Приклади: Nginx, HAProxy, Caddy, Traefik, хмарні (AWS ALB/NLB), Cloudflare Load Balancing.
Що треба налаштувати в застосунку за балансувальником: довірені проксі (trustProxies), щоб Laravel бачив справжній IP клієнта й протокол https, а не адресу балансувальника; спільні сесії, кеш і файли - сервери мають бути без стану.
Балансувальник сам може стати точкою відмови - у продакшені їх резервують (пара з перемиканням чи керований хмарний сервіс).
Cache-aside (ліниве кешування) - найпоширеніша стратегія: застосунок сам керує кешем.
- Шукаємо значення в кеші.
- Знайшли - повертаємо.
- Не знайшли - читаємо з бази, кладемо в кеш, повертаємо.
$stats = Cache::remember("dashboard:stats:{$teamId}", now()->plus(minutes: 10), function () use ($teamId) {
return Order::where('team_id', $teamId)->selectRaw('count(*) as total, sum(amount) as revenue')->first();
});
При зміні даних - інвалідувати кеш (видалити ключ), щоб наступне читання взяло свіжі дані:
Cache::forget("dashboard:stats:{$teamId}");
Інші стратегії:
- write-through - при записі в базу одночасно оновлюється кеш. Кеш завжди актуальний, але кожен запис дорожчий, а в кеш потрапляють і дані, які ніхто не читає;
- write-behind - запис спершу в кеш, у базу - пізніше пакетами. Швидко, але ризик втрати даних;
- read-through - кеш сам завантажує дані з бази (зазвичай у спеціалізованих системах).
Як обрати TTL (час життя):
- як довго застарілі дані прийнятні для бізнесу? Курс валют - хвилина, список категорій - година чи доба, статистика для дашборду - кілька хвилин;
- як часто змінюються дані і чи є надійна інвалідація при змінах. З інвалідацією TTL може бути довгим - він лише страховка;
- ціна обчислення: дороге обчислення варто кешувати довше;
- випадковий розкид (jitter) - щоб тисячі ключів, створених одночасно, не застаріли в одну мить і не вдарили по базі разом.
Що кешувати: дорогі обчислення й запити, що повторюються; дані, однакові для багатьох користувачів. Що не кешувати: дешеві запити за первинним ключем, дані, що мають бути абсолютно точними (баланс при оплаті).
Найскладніше в кешуванні - інвалідація: кожне місце, що змінює дані, має знати, які ключі скинути. Теги кешу (Cache::tags(['team:5'])->flush()) у Redis спрощують групову інвалідацію.
CDN (content delivery network) - мережа серверів у різних країнах, що кешують вміст ближче до користувачів. Запит з Києва обслуговує вузол у Варшаві, а не сервер у Франкфурті чи Вірджинії.
Що дає CDN:
- швидкість - менша мережева затримка, особливо для далеких користувачів;
- розвантаження сервера - кешовані запити взагалі не доходять до застосунку;
- стійкість - CDN поглинає піки трафіку й частину DDoS-атак;
- оптимізації - стиснення, HTTP/3, перетворення зображень.
Що кешувати на CDN:
1. Статичні ресурси з хешем у назві (app-3f9a1c.js, logo-8b2e.png) - «назавжди»:
Cache-Control: public, max-age=31536000, immutable
2. Публічні сторінки для гостей (статті, каталог, лендинги) - короткий термін плюс фонове оновлення:
Cache-Control: public, s-maxage=300, stale-while-revalidate=600
s-maxage - лише для спільних кешів (CDN), браузер його ігнорує.
3. Публічні відповіді API, однакові для всіх (довідники, курси).
Що НЕ кешувати:
- сторінки для автентифікованих користувачів - інакше один користувач побачить персональні дані іншого. Найнебезпечніша помилка з CDN;
- відповіді з
Set-Cookie; - форми з CSRF-токеном у HTML (токен «прилипне» до кешованої сторінки).
Як розрізняти гостей і автентифікованих: правило CDN «обходити кеш, якщо є cookie сесії» плюс заголовок Cache-Control: private, no-store від застосунку для персональних відповідей. Головне джерело правди - заголовки відповіді від застосунку, а не наявність cookie в запиті.
Інвалідація. Хешовані ресурси не потребують інвалідації (нова версія - нова назва). Для сторінок - короткий TTL або очищення через API CDN після публікації.
Пастка деплою: CDN може тримати старий HTML, що посилається на нові файли, або навпаки. Старі хешовані файли варто зберігати якийсь час після деплою.
Лавина кешу (cache stampede, thundering herd) - популярний ключ кешу закінчується, і сотні одночасних запитів не знаходять значення, всі разом ідуть у базу й обчислюють одне й те саме. База, що спокійно працювала завдяки кешу, раптом отримує навантаження, від якого падає. А поки вона повільна, ключ ще довше не з'являється в кеші.
Захист:
1. Блокування - обчислює лише один.
$value = Cache::get('report');
if ($value === null) {
$value = Cache::lock('report:lock', 10)->block(5, function () {
return Cache::remember('report', now()->plus(minutes: 10), fn () => buildReport());
});
}
Один процес обчислює, інші чекають і отримують готове значення.
2. Stale-while-revalidate - віддавати застаріле, оновлювати у фоні. У Laravel - Cache::flexible():
$stats = Cache::flexible('dashboard:stats', [300, 900], fn () => computeStats());
- перші 300 секунд значення свіже - віддається з кешу;
- з 300 до 900 секунд - застаріле, але прийнятне: віддається одразу, а перерахунок запускається після відправки відповіді (лише один, із блокуванням);
- після 900 секунд - обчислюється заново синхронно.
Користувачі майже ніколи не чекають на обчислення, а база не отримує лавини.
3. Ймовірнісне дострокове оновлення - кожен запит з невеликою ймовірністю, що зростає ближче до кінця TTL, оновлює значення заздалегідь. Лише один-два запити підуть у базу до закінчення терміну.
4. Розкид TTL (jitter) - ключі, створені одночасно (наприклад, після деплою чи прогріву), отримують трохи різний термін - і не закінчуються в одну секунду.
5. Прогрів кешу - фонова задача оновлює важливі ключі за розкладом, і користувацькі запити кеш ніколи не «пропускають».
Схожа проблема - холодний старт: після очищення всього кешу (деплой, перезапуск Redis) - лавина по всіх ключах одразу. Тому cache:clear на продакшені під навантаженням - ризикована операція.
У більшості вебзастосунків читань у десятки разів більше, ніж записів. Репліки дають змогу розподілити читання між кількома серверами бази, а всі записи лишити на основному (primary).
У Laravel - розділення з'єднань:
'mysql' => [
'read' => ['host' => ['10.0.0.11', '10.0.0.12']],
'write' => ['host' => ['10.0.0.10']],
'sticky' => true,
// ...
],
SELECT ідуть на репліки, усе інше - на основний сервер.
Затримка реплікації - репліка отримує зміни з запізненням: зазвичай мілісекунди, під навантаженням чи при важких запитах - секунди й більше.
Типові наслідки:
- «не бачу свого запису»: користувач зберіг профіль, сторінка після редиректу читає з репліки - і показує старі дані;
- джоба в черзі не знаходить запис: модель щойно створено на основному сервері, воркер читає з репліки, де її ще немає, -
ModelNotFoundException; - неправильні рішення: перевірка «чи вистачає залишку на складі» на застарілій репліці.
Як з цим жити:
sticky => true- якщо в поточному запиті був запис, наступні читання цього ж запиту йдуть на основний сервер;- читання з основного сервера там, де важлива свіжість: після запису, при перевірках перед зміною даних, у критичних джобах (
->useWritePdo(), окреме з'єднання); - «читати свої записи» - кілька секунд після запису користувача направляти його читання на основний сервер (позначка в сесії);
- моніторинг затримки реплікації зі сповіщеннями - і автоматичне виключення відсталої репліки з пулу.
Що репліки не вирішують:
- масштабування записів - усі записи все одно на одному сервері;
- повільні запити - запит, що займає 10 секунд, займе їх і на репліці. Спершу індекси й оптимізація.
Корисне застосування окремої репліки: важкі звіти, аналітика, бекапи - щоб вони не впливали на основний трафік.
Докладніше в документації: Laravel: з'єднання для читання й запису
Вирівнювання навантаження чергою - між тим, хто створює роботу, і тим, хто її виконує, ставиться черга. Піки запитів не б'ють напряму по повільній частині системи: вони накопичуються в черзі, а воркери розбирають її з постійною швидкістю.
Приклад: розпродаж - 5000 замовлень за хвилину. Генерація рахунків, листи, синхронізація зі складом і бухгалтерією не встигають. Якщо робити це в запиті - запити повільні, з'єднання з базою вичерпуються, сайт падає. Із чергою:
public function store(StoreOrderRequest $request)
{
$order = Order::create($request->validated());
ProcessOrder::dispatch($order); // секунди роботи - у фоні
return new OrderResource($order); // відповідь за мілісекунди
}
Користувач отримує відповідь одразу, а обробка «розтягується» на кілька хвилин після піку.
Що дає черга:
- швидкі відповіді - у запиті лише необхідне;
- стійкість до збоїв - зовнішній сервіс недоступний, задачі чекають і повторюються;
- незалежне масштабування - воркерів можна додати окремо від вебсерверів;
- захист повільних систем - сторонній API з лімітом отримує запити з контрольованою швидкістю.
Зворотний тиск (backpressure) - механізм, що сповільнює виробника, коли споживач не встигає. Без нього черга росте необмежено: пам'ять Redis закінчується, задачі виконуються з годинною затримкою, і система падає пізніше, але гірше.
Способи зворотного тиску:
- обмеження довжини черги - при переповненні відмовляти (
503) чи сповільнювати прийом; - ліміти частоти на вході - не приймати більше роботи, ніж система може переробити;
- пріоритетні черги - важливе (оплати) окремо від некритичного (аналітика), щоб масова дешева робота не блокувала важливу;
- моніторинг глибини черги й часу очікування - Laravel Horizon показує обидва й може сповіщати (
LongWaitDetected); - автомасштабування воркерів за довжиною черги.
Що варто пам'ятати: черга не прибирає роботу, лише переносить її в часі. Якщо середнє навантаження вище за пропускну здатність воркерів, черга росте вічно - потрібна більша потужність, а не довша черга.
Докладніше в документації: Azure Architecture: Queue-Based Load Leveling
Кожне з'єднання з базою коштує ресурсів на сервері бази: у PostgreSQL це окремий процес з власною пам'яттю (мегабайти), у MySQL - потік. Тому кількість з'єднань обмежена (max_connections), і її не можна просто підняти до тисяч - база витратить пам'ять і процесор на керування з'єднаннями замість запитів.
Як PHP-застосунок вичерпує з'єднання: кожен процес PHP-FPM (чи воркер черги, чи воркер Octane) тримає власне з'єднання.
4 вебсервери × 50 процесів PHP-FPM = 200 з'єднань
3 сервери воркерів × 20 процесів = 60
планувальник, Horizon, Pulse, cron ~ 20
Додали сервери під час піку - і наступний запит отримує too many connections. Причому з'єднань багато, а активних запитів у кожен момент - одиниці: більшість процесів PHP у цей час чекає на мережу, рендерить шаблон чи стоїть без діла.
Пул з'єднань - проміжний сервіс між застосунком і базою (PgBouncer для PostgreSQL, ProxySQL для MySQL, керовані пули в хмарах):
- застосунок відкриває сотні «дешевих» з'єднань до пулу;
- пул тримає невелику кількість справжніх з'єднань до бази (наприклад, 30) і видає їх на час транзакції;
- база бачить рівно стільки з'єднань, скільки може ефективно обслужити.
Режими PgBouncer:
- session - з'єднання закріплене за клієнтом на весь сеанс (мало виграшу);
- transaction - з'єднання видається лише на час транзакції - найефективніший, але ламає функції, що живуть довше транзакції:
SETна рівні сесії, advisory locks на сесію,LISTEN/NOTIFY, підготовлені запити поза протоколом; - statement - на кожен оператор, найобмеженіший.
Для Laravel з PgBouncer у режимі transaction - вимкнути емульовані/постійні підготовлені запити, якщо вони несумісні, і не покладатися на стан сесії між транзакціями.
Інші важелі:
- обмежити кількість процесів PHP-FPM і воркерів реальною потребою;
- закривати з'єднання в довгоживучих воркерах між задачами, якщо задачі рідкісні;
- скоротити тривалість транзакцій - менше часу з'єднання зайняте.
Помилка, якої варто уникати: «вирішити» проблему, піднявши max_connections до тисяч - база почне деградувати від кількості з'єднань раніше, ніж від запитів.
Оцінка «на серветці» - швидкий розрахунок порядків величин до проєктування: скільки запитів на секунду, скільки даних, скільки серверів. Мета - не точність, а розуміння, чи це задача для одного сервера чи для розподіленої системи.
Приклад: сервіс коротких посилань.
1. Припущення:
- 1 млн нових посилань на день;
- переходів у 100 разів більше - 100 млн на день;
- один запис - ~500 байтів (URL, код, метадані).
2. Запити на секунду (у добі ~86 400 с, для простоти ~100 000):
- записи: 1 млн / 100 000 ≈ 10 на секунду;
- читання: 100 млн / 100 000 ≈ 1000 на секунду;
- піки - у 3-5 разів вище середнього: до 5000 читань на секунду.
3. Сховище:
- 1 млн × 500 байтів = 500 МБ на день;
- × 365 × 5 років ≈ ~1 ТБ за п'ять років.
4. Висновки:
- 10 записів на секунду - тривіально для однієї бази;
- 5000 читань на секунду на простий пошук за ключем - під силу одній базі з індексом, але кеш (Redis) перед нею майже повністю прибере навантаження: популярні посилання - мала частка всіх;
- 1 ТБ за 5 років - одна база, без шардингу.
Корисні числа, які варто пам'ятати (порядки):
| Операція | Порядок |
|---|---|
| читання з пам'яті | наносекунди |
| запит до Redis у тій самій мережі | ~0,5-1 мс |
| простий запит до бази за індексом | ~1 мс |
| запит між дата-центрами | десятки мс |
| процес PHP на запит | десятки мс і МБ пам'яті |
Чому це цінно на співбесіді: показує, що архітектурні рішення спираються на цифри. Часто розрахунок доводить, що «складна розподілена система» не потрібна - вистачить одного сервера з кешем.
Типові помилки: не враховувати піки (середнє оманливе), забувати про репліки й бекапи в обсязі сховища, ігнорувати співвідношення читань і записів.
Шардинг - розподіл даних однієї логічної бази між кількома фізичними серверами за ключем шардингу. Кожен сервер (шард) зберігає частину рядків і обслуговує частину навантаження - зокрема записів, які реплікація не масштабує.
Стратегії вибору шарду:
- за діапазоном (користувачі 1-1 млн на шарді A) - просто, але нерівномірно: нові активні користувачі всі на останньому шарді;
- за хешем ключа - рівномірно, але діапазонні запити йдуть на всі шарди; додавання шарду переміщує багато даних (зменшує це консистентне хешування);
- за довідником - таблиця відповідності «ключ → шард»: гнучко, але довідник стає ще одним критичним компонентом;
- за орендарем (tenant) - кожен клієнт SaaS на своєму шарді: природна межа, дані клієнта разом.
Чому шардинг - останній засіб:
- запити між шардами:
JOIN, агрегати, сортування по всіх даних стають розподіленими - їх збирає застосунок; - транзакції між шардами - без звичних гарантій ACID; потрібні саги чи двофазний коміт;
- унікальність і ідентифікатори - автоінкремент на кожному шарді дасть дублікати; потрібні UUID/Snowflake;
- перебалансування - додати шард означає перенести частину даних під навантаженням;
- гарячі шарди - великий клієнт чи популярний ключ перевантажує один сервер;
- операційна складність - бекапи, міграції схеми, моніторинг на N серверах;
- вибір ключа майже незворотний - помилка дорого коштує через роки.
Що спробувати раніше:
- індекси, оптимізація запитів, усунення N+1;
- вертикальне масштабування бази (сучасні сервери - сотні ядер і терабайти пам'яті);
- кеш і репліки для читання;
- секціонування (partitioning) великих таблиць усередині однієї бази;
- винесення окремих доменів у власні бази (функціональне розділення) - лог подій, аналітика, пошук;
- архівація старих даних.
Коли шардинг виправданий: записи чи обсяг даних справді перевищують можливості найпотужнішого сервера, або вимоги до ізоляції клієнтів (SaaS з великими орендарями). Альтернатива - розподілені бази з вбудованим шардингом (Vitess, Citus, CockroachDB), що беруть частину складності на себе.
Класична задача системного дизайну. Важливо не одне «правильне» рішення, а структура міркувань: вимоги → оцінки → API → дані → вузькі місця.
1. Вимоги.
- функціональні: створити коротке посилання, перенаправити за ним, (опційно) власні аліаси, термін дії, статистика переходів;
- нефункціональні: переходи дуже швидкі й доступні; читань набагато більше, ніж записів; коди непередбачувані (не можна перебрати чужі посилання).
2. Оцінки (див. попередні розрахунки): ~10 записів і ~1000-5000 читань на секунду, ~1 ТБ за 5 років.
3. API:
POST /api/links {"url": "https://..."} → {"code": "aB3xK9q"}
GET /aB3xK9q → 301/302 Location: https://...
301 чи 302: 301 кешують браузери - менше навантаження, але зникає статистика повторних переходів. Для аналітики - 302.
4. Генерація коду - найцікавіша частина:
- хеш URL (перші символи base62 від SHA-256) - однакові URL дають однаковий код, але можливі колізії - потрібна перевірка;
- лічильник + base62 - унікально без колізій; 7 символів base62 = 62^7 ≈ 3,5 трлн кодів. Але послідовні коди передбачувані - перемішати (бієкція, шифрування лічильника);
- випадковий код + перевірка унікальності - простіше, при великій заповненості зростає ймовірність колізії.
5. Сховище: таблиця code → url з унікальним індексом на code. Пошук за ключем - ідеальний випадок для будь-якої бази; масштаб невеликий для шардингу.
6. Масштабування читань: кеш (Redis) перед базою: популярні посилання становлять малу частку, і кеш з LRU покриє більшість переходів. Плюс CDN/edge для редиректів.
7. Статистика переходів: не писати в базу синхронно на кожен перехід - події в чергу чи потік, агрегування пакетами.
8. Безпека й зловживання: перевірка URL на фішинг і шкідливі сайти, ліміти створення, заборона внутрішніх адрес.
Чого чекає інтерв'юер: уточнення вимог, обґрунтування вибору генерації кодів, розуміння, що основне навантаження - читання, і що кеш вирішує його дешевше за шардинг.
SLI (service level indicator) - вимірювана характеристика якості сервісу з погляду користувача:
- доступність: частка успішних запитів (не
5xx); - затримка: частка запитів, швидших за 300 мс;
- свіжість даних: частка оновлень, що з'явилися протягом хвилини.
SLO (service level objective) - цільове значення SLI за період:
- «99,9% запитів до API успішні за 30 днів»;
- «95% сторінок відкриваються швидше за 500 мс».
SLA (agreement) - договірне зобов'язання перед клієнтами з наслідками (компенсаціями). SLA зазвичай м'якший за внутрішній SLO, щоб мати запас.
Бюджет помилок - допустима «ненадійність», що випливає з SLO:
SLO 99,9% за 30 днів → 0,1% невдалих запитів
→ ~43 хвилини повної недоступності на місяць
Як бюджет помилок керує рішеннями:
- бюджет є - команда може ризикувати: часті релізи, експерименти, міграції;
- бюджет вичерпано - пріоритет на стабільність: заморожування ризикованих змін, виправлення причин збоїв, автоматизація відкатів.
Це прибирає вічну суперечку «розробка хоче швидше, експлуатація хоче стабільніше» - є об'єктивне число.
Чому не 100%:
- кожна наступна «дев'ятка» коштує в рази дорожче (резервування, кілька регіонів, черговість);
- користувач не помітить різниці між 99,99% і 100%, бо його мережа, провайдер і пристрій надійні менше;
- 100% означає «ніколи нічого не змінювати».
Практичні поради:
- SLI - з погляду користувача, а не серверів: «процесор на 40%» - не SLI, «90% запитів швидші за 200 мс» - SLI;
- перцентилі, а не середні: середня затримка ховає повільний «хвіст», який бачать реальні користувачі;
- кілька SLO на ключові сценарії (вхід, оформлення замовлення, пошук), а не один на весь сервіс;
- сповіщення за швидкістю витрачання бюджету (burn rate), а не за кожен окремий збій - менше хибних тривог.
Для невеликого проєкту досить почати з одного SLO доступності й одного затримки для головних сторінок - і вимірювати їх (Pulse, APM, uptime-моніторинг).
Докладніше в документації: Google SRE Book: Service Level Objectives
Каскадна відмова - збій одного компонента поширюється на інші й валить усю систему. Типовий сценарій:
- сервіс рекомендацій сповільнився (відповідає за 30 секунд замість 50 мс);
- процеси PHP, що чекають на нього, не звільняються;
- пул процесів вичерпано - уся сторінка товару, а за нею й увесь сайт перестає відповідати;
- користувачі оновлюють сторінку - навантаження зростає, повторні спроби добивають систему.
Причина - не відмова рекомендацій, а те, що від них синхронно залежала критична частина.
Плавна деградація - при збої некритичної частини система продовжує виконувати головне, з урізаною функціональністю:
- рекомендації недоступні - сторінка товару без блоку рекомендацій;
- пошук перевантажений - простий пошук за назвою замість повнотекстового;
- платіжний провайдер не відповідає - замовлення приймається зі статусом «очікує оплати».
Механізми:
1. Тайм-аути на всі зовнішні виклики - короткі, розраховані на нормальну відповідь, а не на «стандартні 30 секунд»:
Http::timeout(2)->connectTimeout(1)->get($url);
2. Запобіжник (circuit breaker) - після серії помилок перестати викликати сервіс на якийсь час і одразу повертати запасний варіант. Сервіс отримує час відновитися, а запити - швидку відповідь замість очікування.
3. Запасні відповіді (fallback) - кешоване значення, порожній блок, спрощена версія.
4. Ізоляція ресурсів (bulkheads) - окремі пули процесів, черги й з'єднання для різних залежностей: проблеми з однією не займають ресурси інших.
5. Обмеження й скидання навантаження (load shedding) - при перевантаженні відмовляти частині запитів одразу (503), щоб решта обслуговувалася нормально, - замість того щоб повільно обслуговувати всіх.
6. Повтори з експоненційною затримкою й розкидом - і з обмеженою кількістю; бездумні повтори множать навантаження на сервіс, що й так не справляється.
7. Асинхронність - некритичне (аналітика, листи, синхронізації) у черги, щоб збій отримувача не впливав на відповідь користувачу.
8. Прапорці функцій - швидко вимкнути важку функцію під час інциденту без деплою.
Перевірка: навмисно «вимикати» залежності в тестовому середовищі (chaos testing) і дивитися, що відбувається з головними сценаріями.
Докладніше в документації: Google SRE Book: каскадні відмови