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

С чего начинается инцидент
Типичная картина: команда обновляет Laravel с 10.x на 11.x, прогоняет тесты локально, деплоит через blue-green — и получает всплеск 500-х на переключении трафика. В логах — unserialize(): Error at offset и падения воркеров очередей. Причина не в самом фреймворке, а в том, что во время переключения в одной шине данных встречаются процессы двух версий: старые PHP-FPM ещё держат соединения, новые уже пишут в тот же Redis и ту же таблицу jobs. Формат сериализации моделей и структура полезной нагрузки задач между мажорными версиями меняются, и десериализация ломается.
Разбираем, где именно рвётся связность при мажорном апгрейде и как поймать реальный простой в цифрах, а не на глаз. Речь про Laravel 10.x → 11.x на PHP 8.2+, но механизм тот же для любого мажорного перехода. Инвалидация конфигурационного кеша и миграции схемы БД — отдельные темы, здесь только совместимость двух версий под живым трафиком.
Механизм: почему рвётся именно на переключении
При blue-green или rolling-деплое в течение нескольких секунд (а при медленном сливе соединений — минут) сосуществуют два набора кода. Три места, где они делят состояние:
- Очереди. Задача, попавшая в
jobsиз кода 10.x, содержит сериализованный объект. Воркер 11.x читает её и падает на десериализации, если изменились свойства класса задачи, трейты или сигнатура. Задача уходит вfailed_jobsили бесконечно ретраится. - Кеш и сессии. Если в кеше или сессии лежат сериализованные модели Eloquent, а между версиями поменялись casts или структура атрибутов, чтение из кода другой версии даёт мусор или исключение.
- Схема БД. Миграция, добавляющая колонку, безопасна для старого кода. Миграция, удаляющая или переименовывающая колонку, ломает старые процессы, которые ещё обслуживают запросы.
Простой возникает не потому, что «Laravel обновился», а потому что два контракта данных активны одновременно. Цель — свести окно сосуществования к нулю там, где контракт несовместим.
Мажорный апгрейд без простоя — это не про скорость деплоя. Это про то, чтобы старая и новая версии никогда не читали одну и ту же несовместимую сериализованную структуру.
Что мерить: до и после
Прежде чем оптимизировать, зафиксируйте базовую линию. «Простоя стало меньше» — не результат. Результат — конкретные метрики на конкретной нагрузке.
Условия замера
Фиксируйте окружение, иначе цифры несравнимы: количество экземпляров приложения, RPS в момент деплоя, число воркеров очередей и их конкурентность, драйвер очереди (Redis, database, SQS), тип деплоя. Замер на 5 RPS и на 500 RPS покажет разные проблемы.
Метрики, которые есть смысл снимать
| Метрика | Чем снимать | Что показывает |
|---|---|---|
| Число 5xx за окно деплоя | Логи балансировщика / APM | Прямой простой для пользователя |
Рост failed_jobs | SELECT count(*) FROM failed_jobs до и после | Задачи, погибшие на десериализации |
| p95/p99 времени ответа | APM (Telescope в dev, внешний APM в проде) | Деградация без явных ошибок |
| Длительность окна двух версий | Метки времени в деплой-пайплайне | Сколько секунд активны оба контракта |
Если внешнего APM нет, минимум — считать 5xx по логам Nginx за минуту вокруг переключения и сравнивать failed_jobs до и после. Придумывать красивые цифры «было 8 секунд простоя, стало 0» без замера бессмысленно: снимайте на своём стенде под своей нагрузкой.
Подходы к устранению окна несовместимости
Очереди: остановить старые задачи до деплоя
Самый частый источник падений. Порядок, который убирает окно:
- Перед деплоем перестать класть новые задачи в очередь (feature-флаг или пауза постановки на уровне приложения).
- Дождаться, пока текущая очередь опустеет:
php artisan queue:sizeдолжен вернуть 0 по нужным очередям. - Только после этого выкатывать новый код и рестартовать воркеров через
php artisan queue:restart— эта команда даёт воркерам корректно завершить текущую задачу и подняться уже на новом коде. - Снять паузу постановки.
Проверка: после рестарта failed_jobs не растёт, а обработанные задачи в логах воркера идут без исключений десериализации.
Кеш и сессии: не хранить объекты, переживающие деплой
Если в сессии лежит сериализованная модель, апгрейд её сломает. Приём — хранить в сессии и долгоживущем кеше примитивы (id, скаляры), а модель поднимать из БД по id. Для кеша с моделями — либо инвалидировать соответствующие ключи в момент деплоя, либо версионировать префикс ключей, чтобы новый код читал только новые записи.
Схема БД: expand / contract
Разносите изменения схемы на два релиза. Сначала добавляете новое (колонку, таблицу) — старый код её не замечает, новый уже пишет. После того как весь трафик на новой версии, отдельным релизом удаляете старое. Одновременное удаление колонки и выкатка кода, который её больше не читает, — гарантированный источник 500-х на переходе.
Сравнение стратегий деплоя под этот сценарий
| Стратегия | Окно двух версий | Цена |
|---|---|---|
| Rolling (по одному инстансу) | Долгое: минуты, версии сосуществуют весь раскат | Максимальный риск для очередей и кеша, но нет простоя по инфраструктуре |
| Blue-green с мгновенным переключением | Секунды на сливе соединений | Нужен двойной ресурс, но окно минимально |
| Пауза постановки задач + опустошение очереди | Только по HTTP, очереди — ноль | Требует управляемой паузы, но снимает главный источник падений |
Когда что выбирать
- Мало фоновых задач, изменения в моделях минимальны — достаточно blue-green и короткого окна, отдельная пауза очереди избыточна.
- Тяжёлые долгие задачи в очереди — обязательна пауза постановки и опустошение, иначе воркеры двух версий гарантированно столкнутся.
- Ломающая миграция схемы — expand/contract на два релиза, без вариантов; попытка сделать в один заход даёт простой.
Что ломается на практике
queue:restartдействует только на воркеров, которые проверяют сигнал рестарта между задачами. Зависшая на минутах задача не перезапустится, пока не завершится или не упрётся в таймаут — закладывайте это в окно.- Если очередь и кеш живут в одном инстансе Redis и вы флашите БД, снесёте вместе с кешем и невыполненные задачи. Разделяйте базы Redis или префиксы.
- OPcache на PHP-FPM может держать старый байткод после деплоя. Без сброса кеша (перезапуск FPM или очистка OPcache) часть воркеров и запросов останется на старом коде дольше, чем вы думаете.
Что делать дальше
Соберите базовую линию на стейджинге под нагрузкой, близкой к продовой: прогоните деплой с включённым замером 5xx и failed_jobs, зафиксируйте окно двух версий в секундах. Затем внедрите паузу постановки задач и expand/contract для схемы и повторите замер на том же стенде — сравнивайте только сопоставимые прогоны. В проде после апгрейда неделю держите под наблюдением рост failed_jobs и p99: отложенные задачи и редкие эндпоинты вылезают не сразу. Точный порядок обновления и список breaking changes для вашего перехода сверяйте в официальном upgrade guide — между мажорными версиями меняются и дефолтные драйверы, и структура каркаса приложения.
Логотип и фирменный стиль: Три концепции разного характера, палитра с обоснованием, проверка на читаемость в малом размере и в один цвет.
Спросить за 60 ₽Источники: Laravel — Upgrade Guide, Laravel — Queues
Частые вопросы
А если у меня Laravel 9 → 10, а не 10 → 11?
Не сработало: воркеры всё равно падают на десериализации после рестарта
Обязательно ли ставить паузу на очередь, если задач немного?
Грозит ли простой, если я делаю rolling-деплой без blue-green?
Как понять, что в моём кеше или сессии лежат сериализованные модели?
Достаточно ли прогнать тесты, чтобы не бояться простоя?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.