Junior: питання на співбесіді з теми «Масштабування й продуктивність»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
5 питань
Вертикальне масштабування (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, що посилається на нові файли, або навпаки. Старі хешовані файли варто зберігати якийсь час після деплою.