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

Питання на співбесіді: Масштабування

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

6 питань

На одному сервері багато речей «просто працює», бо все зберігається локально. Коли серверів кілька за балансувальником, кожен наступний запит користувача може потрапити на інший сервер. Тому все, що зберігається на диску чи в пам'яті одного сервера, треба винести в спільне сховище.

Чек-лист:

Що На одному сервері На кількох
сесії file database чи redis - інакше користувача розлогінює
кеш file redis, memcached чи database - інакше кожен сервер має свій кеш і скидання не працює
файли користувачів диск local/public S3, R2 чи інше об'єктне сховище
черги database чи sync redis, database чи SQS - спільні для всіх воркерів
блокування (Cache::lock, withoutOverlapping) будь-який кеш спільний кеш з атомарними блокуваннями
планувальник cron на сервері на одному сервері чи onOneServer() для задач
логи storage/logs централізований збір (stderr, Papertrail, сервіс логів)

Що ще перевірити:

  • однаковий APP_KEY на всіх серверах - інакше cookie й сесії, зашифровані одним сервером, не розшифрує інший;
  • довірені проксі (trustProxies) - щоб застосунок бачив справжню IP-адресу й HTTPS за балансувальником;
  • міграції запускаються один раз за деплой, а не на кожному сервері;
  • локальні шляхи в коді (storage_path() для тимчасових файлів між запитами) - тимчасовий файл, створений одним запитом, інший сервер не побачить;
  • кеш конфігурації й маршрутів формується на кожному сервері при деплої - це нормально.

Головна ідея - сервер застосунку без стану (stateless). Його можна будь-коли вимкнути, замінити чи додати ще один, і користувачі цього не помітять. Тоді масштабування - це просто збільшення кількості однакових серверів.

Корисно перевірити локально: запустити два екземпляри застосунку (наприклад, два контейнери) за простим балансувальником - проблеми з сесіями й файлами проявляться одразу.

Докладніше в документації: Розгортання

Redis у Laravel часто виконує одразу кілька ролей:

Роль Конфігурація Що станеться, якщо дані зникнуть
кеш CACHE_STORE=redis нічого страшного - перерахується
черги QUEUE_CONNECTION=redis втрачено задачі: листи не підуть, оплати не обробляться
сесії SESSION_DRIVER=redis користувачів розлогінить
блокування Cache::lock, withoutOverlapping, ShouldBeUnique дублювання задач на короткий час
обмеження частоти RateLimiter ліміти скинуться
Horizon, Pulse, broadcasting метрики, pub/sub залежить від ролі

Проблема спільного Redis - різна цінність даних. Кеш можна втратити, черги - ні. А налаштування в Redis одне на весь екземпляр.

Витіснення при нестачі пам'яті (maxmemory-policy):

  • для кешу логічна allkeys-lru - видаляти найстаріші ключі;
  • але ця політика видаляє будь-які ключі, включно з задачами в черзі й сесіями. Під навантаженням, коли кеш заповнив пам'ять, Redis тихо видаляє задачі;
  • для черг потрібна noeviction - тоді при нестачі пам'яті Redis відмовляє в записі, і помилку видно.

cache:clear очищує всю базу Redis (FLUSHDB), яку використовує кеш. Якщо кеш і черги в одній базі, скидання кешу при деплої видаляє черги.

Тому Laravel за замовчуванням розділяє з'єднання в config/database.php:

'redis' => [
    'default' => [..., 'database' => env('REDIS_DB', '0')],        // черги, сесії
    'cache'   => [..., 'database' => env('REDIS_CACHE_DB', '1')],  // кеш
],

Окремі номери баз захищають від FLUSHDB, але не від витіснення - політика пам'яті спільна для екземпляра.

Надійне розділення на продакшені:

  • окремі екземпляри Redis для кешу (з allkeys-lru і лімітом пам'яті) і для черг та сесій (noeviction, збереження на диск через AOF);
  • з'єднання для черги: REDIS_QUEUE_CONNECTION, для сесій - SESSION_CONNECTION, для блокувань кешу - REDIS_CACHE_LOCK_CONNECTION;
  • моніторинг пам'яті (INFO memory, used_memory, evicted_keys) - зростання evicted_keys сигналізує, що кеш тисне на ліміт.

Ще одна пастка: один «сусідній» застосунок на тому самому Redis може заповнити пам'ять і зламати всіх. Спільний Redis для кількох проєктів без лімітів і окремих екземплярів - поширена причина аварій.

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

Ознаки, що воркери не встигають: задачі чекають у черзі хвилинами, листи підтвердження приходять із запізненням, Horizon повідомляє про довге очікування (LongWaitDetected).

1. Більше воркерів. Найпростіший крок - запустити кілька процесів queue:work (через Supervisor numprocs, кілька контейнерів чи Horizon maxProcesses). Кожен воркер обробляє одну задачу за раз, тож N воркерів - до N задач паралельно.

Межа - ресурси: кожен воркер - окремий PHP-процес з власною пам'яттю, і кожен тримає з'єднання з базою.

2. Розділити черги за пріоритетом:

SendPasswordReset::dispatch($user)->onQueue('high');
GenerateReport::dispatch($report)->onQueue('low');
php artisan queue:work --queue=high,default,low

Воркер спершу бере задачі з high, і масовий імпорт у low не затримує скидання пароля.

3. Окремі воркери для окремих черг. Пріоритети не рятують, якщо повільні задачі займають усіх воркерів. Тоді виділяють окремі групи:

  • 2 воркери лише для high;
  • 6 воркерів для default;
  • 2 воркери для low з більшим --timeout і лімітом пам'яті.

4. Horizon - балансування автоматично:

'supervisor-1' => [
    'queue' => ['high', 'default', 'low'],
    'balance' => 'auto',
    'autoScalingStrategy' => 'time',
    'minProcesses' => 1,
    'maxProcesses' => 20,
],

Horizon перерозподіляє процеси між чергами залежно від навантаження і показує час очікування й пропускну здатність кожної черги.

5. Окремі сервери для воркерів. Воркери не мають бути на вебсерверах: важкі задачі (обробка відео, PDF) не повинні впливати на швидкість сторінок. Окремі сервери можна масштабувати незалежно.

Перш ніж додавати воркерів, перевірте задачі:

  • чому задача повільна - N+1 у задачі, синхронні виклики зовнішніх API, обробка файлів у пам'яті;
  • чи можна розбити - одна задача «надіслати розсилку 50 000 листів» гірша за 50 000 маленьких (чи пакет Bus::batch);
  • вузьке місце може бути не у воркерах: якщо всі задачі ходять у базу чи сторонній API з лімітом, більше воркерів лише збільшить конкуренцію. Тоді потрібне обмеження частоти (RateLimited middleware) і пул з'єднань з базою.

Докладніше в документації: Пріоритети черг

Sharding - горизонтальне розбиття даних між кількома незалежними БД (шардами) за ключем (tenant_id, user_id, geo). Дозволяє вийти за межі одного сервера.

shard 1: користувачі 1–1M
shard 2: користувачі 1M–2M
  • Shard key обирають так, щоб дані рівномірно розподілялись і більшість запитів влучали в один шард.
  • Складнощі: запити між шардами, унікальність ID (часто UUID/Snowflake), ребалансування при додаванні шарда, відсутність крос-шардових операцій JOIN і транзакцій.

У Laravel реалізують через множинні з'єднання й маршрутизацію за shard key. Sharding - крайній захід, коли вертикальне масштабування та репліки вже не справляються.

Serverless виконує код без керування серверами: провайдер (AWS Lambda) сам масштабує під навантаження й тарифікує за фактичні виклики.

Laravel Vapor - платформа для деплою Laravel на AWS Lambda + API Gateway, з керованими БД, чергами (SQS), кешем і CDN.

Особливості й обмеження:

  • Авто-масштабування до нуля й під пік; платиш лише за використання.
  • Cold start - затримка першого запиту після простою.
  • Файлова система ефемерна → файли лише в S3.
  • Обмеження часу виконання Lambda → довгі завданьі в черги.
  • Stateless за дизайном (сесії/кеш у Redis/DynamoDB).

Альтернатива - контейнери (ECS/Kubernetes), коли потрібен повний контроль або стабільні довготривалі процеси.

WebSocket - постійне двостороннє з'єднання поверх одного TCP, що дає реальний час без поллінгу.

У Laravel сервер WebSockets - Reverb (офіційний), Soketi або Pusher; події транслюються через Broadcasting, клієнт слухає через Echo.

Масштабування: коли інстансів WebSocket-сервера кілька, клієнти на різних інстансах не «бачать» одне одного. Рішення - Redis Pub/Sub як спільна шина: інстанс публікує повідомлення в Redis, усі інстанси отримують і розсилають своїм підключеним клієнтам.

client A ─ inst 1 ┐
                  ├─ Redis Pub/Sub ─┤
client B ─ inst 2 ┘

Окрема увага: авторизація private/presence-каналів, ліміти відкритих з'єднань, sticky sessions на балансувальнику.

Докладніше в документації: Broadcasting / WebSockets