Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Junior: питання на співбесіді з теми «Масштабування й продуктивність»

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

5 питань

Вертикальне масштабування (scale up) - зробити сервер потужнішим: більше процесорів, пам'яті, швидші диски.

Горизонтальне (scale out) - додати ще сервери й розподілити навантаження між ними.

Вертикальне Горизонтальне
зміни в коді майже не потрібні застосунок має бути готовим
межа найпотужніший доступний сервер практично немає
відмовостійкість одна точка відмови відмова одного сервера не зупиняє систему
вартість дорожчає нелінійно лінійна, дешеві сервери
складність низька балансувальник, спільний стан, деплой на кілька машин

Почати варто з вертикального. Сучасний сервер з 32 ядрами й 128 ГБ пам'яті витримує дуже багато - для більшості Laravel-проєктів це роки росту без зміни архітектури. Передчасне горизонтальне масштабування додає складність без потреби.

Горизонтальне потрібне, коли:

  • вертикальне вперлося в межу чи стало непропорційно дорогим;
  • потрібна відмовостійкість - навіть невеликий сервіс з вимогою «працювати під час оновлення сервера» потребує щонайменше двох екземплярів;
  • навантаження стрибкоподібне (розпродажі, пікові години) - простіше додати й прибрати сервери.

Що треба зробити в застосунку, щоб масштабуватися горизонтально - прибрати стан з окремого сервера:

  • сесії - у Redis чи базі, а не у файлах;
  • завантажені файли - в S3/R2, а не на локальний диск;
  • кеш і блокування - у спільному Redis;
  • черги - Redis/SQS, воркери на окремих машинах;
  • планувальник - запуск на одному сервері (onOneServer()).

Різні шари масштабуються по-різному: вебсервери горизонтально прості (вони без стану), а база даних - найскладніша частина: її зазвичай спершу масштабують вертикально, потім репліками для читання, і лише в крайньому разі - шардингом.

Докладніше в документації: System Design Primer

Застосунок без стану (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) на балансувальнику - обхідний шлях: користувача завжди відправляють на той самий сервер. Працює, але сервер стає точкою відмови для своїх користувачів, а навантаження розподіляється нерівномірно.

Перевірка готовності: запустити два екземпляри за балансувальником і пройти основні сценарії - вхід, завантаження файлу, черги, кеш. Те, що зламається, і є прихованим станом.

Докладніше в документації: Laravel: налаштування сесій

Балансувальник навантаження приймає запити клієнтів і розподіляє їх між кількома серверами застосунку. Для клієнта це одна адреса, а за нею - пул серверів.

Що він дає:

  • масштабування - навантаження ділиться між серверами;
  • відмовостійкість - перевірки стану (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, а не адресу балансувальника; спільні сесії, кеш і файли - сервери мають бути без стану.

Балансувальник сам може стати точкою відмови - у продакшені їх резервують (пара з перемиканням чи керований хмарний сервіс).

Докладніше в документації: Load balancing

Cache-aside (ліниве кешування) - найпоширеніша стратегія: застосунок сам керує кешем.

  1. Шукаємо значення в кеші.
  2. Знайшли - повертаємо.
  3. Не знайшли - читаємо з бази, кладемо в кеш, повертаємо.
$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 спрощують групову інвалідацію.

Докладніше в документації: Azure Architecture: Cache-Aside

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, що посилається на нові файли, або навпаки. Старі хешовані файли варто зберігати якийсь час після деплою.

Докладніше в документації: MDN: HTTP-кешування