Масштабирование Laravel: подводные камни горизонтального роста
Что ломается первым, когда одно приложение Laravel превращается в несколько инстансов за балансировщиком: сессии, кеш, файлы, очереди и планировщик. Разбор с указанием, где именно спрятана мина.

Приложение на Laravel держало нагрузку на одном сервере, но с ростом трафика вы поднимаете второй инстанс за балансировщиком — и внезапно пользователей выбрасывает из аккаунта при каждом втором запросе, загруженные файлы видны через раз, а некоторые задачи из очереди выполняются дважды. Ни одна из этих проблем не проявляется на одном сервере: они рождаются ровно в момент, когда запросы одного пользователя начинают попадать на разные машины. Разберём, что и в каком порядке ломается при горизонтальном масштабировании Laravel 11.x и где именно закопаны грабли.
Что ломается первым: состояние, привязанное к процессу
Корень почти всех проблем один: часть состояния Laravel по умолчанию хранит на локальной файловой системе того сервера, где обрабатывался запрос. На одной машине это невидимо. На двух и более всё, что лежит в storage/ одного инстанса, недоступно другому.
Конкретные точки, которые упадут первыми, в порядке частоты:
- Сессии — драйвер
fileпишет сессию вstorage/framework/sessionsлокальной машины. Запрос попал на второй инстанс — сессии там нет, пользователь разлогинен. - Кеш — драйвер
fileимеет ту же проблему: прогретый на одном сервере кеш пуст на другом. - Загруженные файлы — локальный диск
local/publicживёт на одной машине. Аватар, загруженный через инстанс A, отдаётся 404 при запросе через инстанс B. - Очереди — драйвер
syncисполняет задачу прямо в запросе, что убивает смысл масштабирования, аdatabaseбез правильной блокировки допускает двойную обработку. - Планировщик — cron с вызовом
schedule:runна каждой машине запустит одну и ту же задачу столько раз, сколько у вас инстансов.
Сессии: перенос состояния во внешнее хранилище
Первое, что нужно поменять, — драйвер сессий. В файле config/session.php и через переменную SESSION_DRIVER значение file заменяется на общее хранилище: Redis, Memcached или таблицу в базе. Redis здесь предпочтительнее — сессии короткоживущие и их много.
# .env для Laravel 11.x
SESSION_DRIVER=redis
SESSION_CONNECTION=default
CACHE_STORE=redis
REDIS_HOST=10.0.0.5
REDIS_PORT=6379
Подводный камень: если вы используете один и тот же инстанс Redis и под кеш, и под сессии, вызов php artisan cache:clear при драйвере Redis по умолчанию затрагивает базу целиком и может вынести и сессии тоже. Разносите их по разным логическим базам Redis (параметр database в блоках redis.default и redis.cache в config/database.php) или по разным инстансам.
Горизонтальное масштабирование Laravel — это не про добавление серверов. Это про вынос всего локального состояния наружу. Пока хоть что-то пишется в storage конкретной машины, второй инстанс сломает это состояние.
Sticky sessions — соблазн и ловушка
Балансировщик умеет привязывать пользователя к одному инстансу по cookie (sticky sessions). Заманчиво: включил — и проблема сессий как будто решена без Redis. На деле это мина замедленного действия:
- инстанс упал или выведен из ротации на деплое — все привязанные к нему пользователи теряют сессию разом;
- нагрузка распределяется неравномерно: долгоживущие сессии копятся на части машин;
- вы всё равно не решили проблему кеша и файлов — только сессий.
Sticky sessions уместны как временный костыль на время миграции, но не как архитектурное решение.
Файлы: локальный диск против объектного хранилища
Загрузки нужно вынести в S3-совместимое хранилище. Laravel через flysystem поддерживает это из коробки — драйвер s3 в config/filesystems.php. Меняется только конфигурация диска, код вида Storage::disk('s3')->put(...) остаётся тем же.
Грабли при переезде:
- Публичные URL. Метод
Storage::url()для локального диска отдаёт путь черезpublic/storage, а для S3 — прямую ссылку на бакет. Если в шаблонах захардкожены пути/storage/..., после переезда картинки отвалятся. Ищите прямые обращения к путям и заменяйте наStorage::url(). - Приватные файлы. Для непубличных файлов нужны временные подписанные ссылки через
Storage::temporaryUrl()— прямой публичный URL здесь работать не должен. - Latency. Каждая операция с файлом теперь сетевой вызов. Код, который в цикле дёргает
Storage::exists()по сотне файлов, на локальном диске работал мгновенно, а на S3 превратится в сотню HTTP-запросов.
Очереди и планировщик: двойное выполнение
Когда воркеров несколько, а очередь общая, две проблемы становятся реальными.
Задачи, которые нельзя выполнять дважды
При масштабировании воркеров задача может быть подхвачена, а воркер — упасть до подтверждения. Задача вернётся в очередь и выполнится повторно. Если внутри списание денег или отправка письма — это боль. Решения:
- сделать задачу идемпотентной: перед действием проверять, не выполнено ли оно уже (по флагу в базе, по ключу в Redis);
- для задач, которые должны выполняться строго один раз в момент времени, использовать
ShouldBeUnique— Laravel возьмёт блокировку на время жизни задачи через драйвер кеша, поддерживающий атомарные локи (Redis, Memcached, а неfile).
Планировщик, запущенный на всех машинах
Если cron с php artisan schedule:run прописан на каждом инстансе, задача из routes/console.php выполнится на каждом. Для отчёта, который рассылается раз в сутки, это N рассылок вместо одной. Метод onOneServer() в определении расписания заставляет Laravel взять блокировку через общий кеш и выполнить задачу только на одном инстансе. Он требует общего драйвера кеша с атомарными локами — снова Redis или Memcached, не file.
// routes/console.php, Laravel 11.x
use Illuminate\Support\Facades\Schedule;
Schedule::command('reports:daily')
->dailyAt('06:00')
->onOneServer(); // выполнится только на одном инстансе
Альтернатива — вообще не ставить cron на веб-инстансы, а выделить одну машину под расписание. Тогда onOneServer() не нужен, но появляется единая точка отказа для фоновых задач.
Сравнение драйверов: что подходит для нескольких инстансов
| Компонент | Значение по умолчанию | Работает на N инстансах | Что ставить |
|---|---|---|---|
| Сессии | file | Нет | redis / database |
| Кеш | file (в новых проектах database) | file — нет, database — да | redis |
| Файлы | local | Нет | s3-совместимое |
| Очередь | sync / database | sync — нет, database — да с оговорками | redis / SQS |
| Атомарные локи | file | Нет | redis / memcached |
База данных: следующий потолок
Вынеся состояние в Redis и S3, вы упрётесь в базу — теперь все инстансы бьют в один PostgreSQL или MySQL. Первое, что кончается, — коннекты. Каждый PHP-FPM воркер держит соединение; при десятках воркеров на нескольких машинах вы легко упираетесь в лимит соединений СУБД. Здесь нужен пул соединений на стороне СУБД (например, PgBouncer для PostgreSQL), а не бесконечное увеличение max_connections.
Отдельная тема — репликация чтения через настройку read/write в конфиге подключения. Она снимает нагрузку с мастера, но приносит проблему согласованности: запись ушла в мастер, а следующее чтение с реплики отстаёт и не видит только что записанное. Это отдельный разбор, здесь только отмечаем как следующий потолок.
Что делать дальше
Порядок миграции на несколько инстансов: сначала сессии и кеш в Redis, затем файлы в S3, затем разбор очередей на идемпотентность и onOneServer() для планировщика, и только потом — база и пул соединений. Проверяйте каждый шаг на двух инстансах за балансировщиком: если пользователя выбрасывает из аккаунта или файл отдаёт 404 через раз — состояние всё ещё привязано к машине.
В проде смотрите на три метрики: число активных коннектов к базе (упрётесь первым делом сюда), процент попаданий в кеш Redis и число повторно выполненных задач в очереди. Резкий рост последнего — сигнал, что где-то нарушена идемпотентность.
Аналитик звонков: Разбор записи звонка по этапам сделки: где сорвалось, что упущено, три исправляемые рекомендации с цитатами.
Спросить за 90 ₽Источники: Laravel Documentation — Session, Laravel Documentation — Queues
Частые вопросы
У меня Laravel 10, а не 11 — инструкция подходит?
Обязательно ли Redis или можно обойтись драйвером database для сессий и кеша?
А если после переноса сессий в Redis пользователей всё равно выбрасывает?
Что делать со старыми файлами при переезде на S3?
Sticky sessions на балансировщике — это плохо всегда?
Как понять, что задача из очереди выполнилась дважды?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.