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