Обновление Laravel до 11: разбор инцидента с 500 на проде
Мажорный апгрейд Laravel казался рутиной: composer update, прогон тестов, деплой. Через минуту после выкатки половина запросов начала отдавать 500. Разбираем, что сломалось и почему zero-downtime не спас.

Приложение на Laravel обслуживало около 400 запросов в секунду. Обновление с 10.x до 11.x прошло проверку на стейджинге: тесты зелёные, миграции применились, вручную покликали ключевые сценарии. Задеплоили через php artisan down — деплой, php artisan up. Через 40 секунд после снятия maintenance-режима Sentry залило алертами: примерно 55% запросов возвращали 500. Ещё через две минуты откатились на предыдущий релиз. Инцидент — 3 минуты частичной недоступности, но разбор оказался поучительнее самого сбоя.
Ниже — что именно сломалось, почему стейджинг это не поймал и как выстроить процедуру мажорного апгрейда так, чтобы откат был дешевле обновления. Речь про переход между мажорными версиями фреймворка; минорные обновления в рамках одной мажорной серии — отдельная история и здесь не рассматриваются.
Что произошло в первые 40 секунд
В Laravel 11 убрали ряд файлов и изменили структуру конфигурации: вместо десятка файлов в config/ базовая конфигурация переехала в упрощённый bootstrap. Приложение годами жило на кастомном значении в config/session.php, которого в дефолтном скелете 11 больше нет. Но сломало не это.
Сломал кеш конфигурации. Деплой-скрипт выполнял php artisan config:cache. На стейджинге кеш собирался с нуля, на проде — тоже, но с одной разницей: на проде в окружении жила переменная, которую старый код читал через env() прямо в рантайме. В Laravel это давно антипаттерн: после config:cache вызовы env() вне конфиг-файлов возвращают null. В 10.x конкретно этот участок ещё как-то отрабатывал из-за порядка загрузки, в 11.x порядок изменился — и null прилетел в конструктор сервиса, который делил на него.
Zero-downtime деплой не защищает от логической несовместимости. Он рассчитан на то, что переключение атомарно, а не что новый код совместим со старым состоянием окружения.
Стейджинг не поймал сбой, потому что там этой переменной окружения просто не было — сервис падал в другую ветку и отдавал понятную заглушку, которую в ручной проверке приняли за штатное поведение.
Механизм: почему env() и config:cache несовместимы
При старте Laravel читает .env и заполняет объект конфигурации. Команда config:cache сериализует итоговый массив конфигурации в один PHP-файл и на боевом старте загружает именно его, не читая .env вообще. Это ускоряет холодный старт: вместо разбора десятков файлов — один require.
Плата за это: env('SOMETHING'), вызванный где угодно кроме файлов в config/, после кеширования вернёт null, потому что переменные окружения в этот момент не подгружены в приложение. Правильный путь — читать значение через config('services.something'), а сам env() оставить только внутри конфиг-файла:
// config/services.php
return [
'billing' => [
// env() допустим только здесь, до кеширования
'rate_divisor' => (int) env('BILLING_RATE_DIVISOR', 100),
],
];
// В сервисе — уже через config(), а не env()
$divisor = config('services.billing.rate_divisor');
В инциденте виноватый код читал env('BILLING_RATE_DIVISOR') напрямую в сервисе. До кеша конфигурации он работал случайно, после — вернул null, дальше деление и DivisionByZeroError → 500.
Как искать такие места до деплоя
Смена мажорной версии — повод прочесать проект на прямые вызовы env() вне конфигов. Быстрый способ — grep по кодовой базе, исключая директорию конфигурации и вендор:
# ищем env( вне config/ и vendor/
grep -rn "env(" app/ routes/ database/ \
--include="*.php" | grep -v "config/"
Каждое совпадение — кандидат на замену env() на config() с переносом дефолта в конфиг-файл. После правок обязательно прогоните на локали сам сценарий с кешем:
- Соберите кеш:
php artisan config:cache. Проверка — команда завершилась без ошибок, появился файлbootstrap/cache/config.php. - Запустите приложение в этом состоянии и пройдите сценарий, который в проде читал переменную. Если раньше работал
env()— теперь получитеnullи увидите ошибку локально, а не в проде. - Сбросьте кеш перед следующей итерацией разработки:
php artisan config:clear, иначе правки в.envне подхватятся.
Процедура мажорного апгрейда с дешёвым откатом
Главная ошибка исходного деплоя — необратимость. Миграции применились до того, как новый код доказал работоспособность под трафиком. Правильная последовательность разносит несовместимые изменения во времени.
| Стратегия | Что выигрываете | Чем платите | Когда брать |
|---|---|---|---|
artisan down → деплой → up | Просто, атомарно | Есть окно недоступности; откат руками | Мелкие проекты, ночное окно |
| Blue-green (две среды, переключение балансировщика) | Мгновенный откат переключением трафика | Двойная инфраструктура; синхронизация БД | Мажорный апгрейд на нагруженном проде |
| Canary (5–10% трафика на новую версию) | Сбой видно на малой доле пользователей | Нужен роутинг по долям; обе версии совместимы с одной схемой БД | Когда цена 500 высокая |
Для мажорного апгрейда Laravel практичнее blue-green или canary. Ключевое требование обоих — совместимость схемы БД с двумя версиями кода одновременно. Это достигается разбиением необратимых миграций на шаги:
- Expand. Добавляете новые колонки/таблицы, не трогая старые. Обе версии кода работают.
- Деплой нового кода. Он пишет и в новое, и в старое (или только в новое, если старая версия колонку игнорирует).
- Contract. После того как старая версия окончательно выведена, отдельной миграцией удаляете старые колонки.
Если между версиями меняется DROP COLUMN или RENAME, откат на старый код после применённой миграции становится невозможным без восстановления из бэкапа. Именно это делает откат дорогим — а в инциденте выше миграции, к счастью, были только expand-типа, поэтому откат кода прошёл без потери данных.
Граничные случаи, где подход ломается
Blue-green и canary не панацея. Места, где они дают течь при мажорном апгрейде:
- Общий кеш и сессии. Если обе версии пишут в один Redis, а формат сериализации сессии между версиями изменился, пользователь, попавший то на старую, то на новую ноду, будет разлогинен или получит битые данные. Разносите префиксы кеша по версиям или мигрируйте формат заранее.
- Очереди. Джоб, сериализованный старой версией, может не десериализоваться новой, если изменилась структура класса. Дренируйте очередь перед переключением либо версионируйте payload.
- Плановые команды (scheduler). Если крон запускает
artisanиз того каталога, что не переключился вместе с трафиком, получите смесь версий. Убедитесь, что scheduler указывает на активный релиз. - Кеш конфигурации на новой ноде. Если забыть выполнить
config:cacheна blue-среде, она стартует медленнее и с другим поведениемenv(), чем прод. Кеш собирают в обеих средах одинаково.
Что мерить и делать дальше
После мажорного апгрейда смотрите не на «сайт открывается», а на конкретные показатели в течение первых 15–30 минут под реальным трафиком:
- Доля ответов 5xx по эндпоинтам — рост даже на 1–2% относительно базовой линии повод откатываться, а не разбираться в бою.
- Длина очередей и доля failed jobs — несовместимость сериализации проявляется здесь, а не в HTTP.
- Время холодного старта воркеров — если выросло, проверьте, собрался ли
config:cacheиroute:cache.
Перед следующим мажорным апгрейдом пройдите официальный upgrade guide построчно (он для каждой версии свой) и прогоните grep по env(). Точный список удалённых и переименованных файлов конфигурации между вашими версиями сверяйте в официальном руководстве по обновлению — он меняется от релиза к релизу, и полагаться на память здесь нельзя.
Тест и проверочная: Быстрые ответы с объяснением, почему остальные варианты неверны
Спросить за 10 ₽Источники: Laravel — Upgrade Guide (официальная документация), Laravel — Configuration (Configuration Caching)
Частые вопросы
У меня Laravel 9, а не 10 или 11 — процедура та же?
Я сделал config:cache, а изменения в .env не подхватываются. Это баг?
Что делать, если после апгрейда пошли 500, а откат уже невозможен из-за миграции?
Стейджинг зелёный, тесты проходят. Почему прод всё равно упал?
Нужен ли blue-green для небольшого проекта?
Как быстро найти все опасные вызовы env() в проекте?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.