Релиз не должен быть событием. Если каждое обновление требует ночного окна, предупреждения клиентам и некоторой смелости, обновлять начинают реже. Редкие релизы становятся большими, а большие ломаются чаще и чинятся дольше. Выход из этого круга один: сделать выкатку настолько безопасной и незаметной, чтобы её можно было проводить днём, в обычном рабочем порядке.
Для этого не обязательны кластер и собственная платформа. Основные приёмы работают и на одном сервере.
Откуда берётся простой
Сервис падает при обновлении не потому, что обновление — опасная вещь, а по нескольким конкретным причинам.
- Файлы меняются на месте. Пока идёт копирование, часть кода уже новая, часть ещё старая. Запрос, попавший в этот момент, получает ошибку.
- Зависимости ставятся на живой системе. Пока идёт установка пакетов, приложение находится в полусобранном состоянии.
- Миграция блокирует таблицу. Изменение структуры большой таблицы может остановить запись на минуты.
- Код и база не совпадают. Новый код ждёт колонку, которой ещё нет, или старый обращается к той, которую уже удалили.
- Кэш очищается под нагрузкой. Первые запросы после релиза одновременно пересобирают всё, что было прогрето.
- Фоновые процессы продолжают работать на старом коде или обрываются посреди задачи.
На каждую из причин есть своё решение, и вместе они складываются в понятную схему.
Атомарное переключение релизов
Идея простая: новая версия не накатывается поверх старой, а собирается рядом. Каждый релиз лежит в собственном каталоге; общие данные — загрузки, журналы, файл с секретами — вынесены отдельно и подключаются ссылками. Веб-сервер смотрит не на конкретный релиз, а на символическую ссылку current.
/var/www/app/releases/2026-04-02_1015/
/var/www/app/releases/2026-04-02_1432/
/var/www/app/shared/
/var/www/app/current -> releases/2026-04-02_1432
# переключение: новая ссылка создаётся рядом и переименованием заменяет старую
ln -sfn /var/www/app/releases/2026-04-02_1432 /var/www/app/current.new
mv -T /var/www/app/current.new /var/www/app/current
В новом каталоге спокойно ставятся зависимости, собираются ресурсы и прогревается кэш — работающий сайт этого не замечает. Затем ссылка переключается одной операцией переименования: в любой момент времени она указывает либо на старый релиз, либо на новый, промежуточного состояния нет.
Для PHP есть важная деталь. Кэш байт-кода запоминает файлы по путям, и если отдавать интерпретатору путь через ссылку, он может продолжить выполнять старый код. Поэтому в конфигурации nginx передают уже разрешённый путь:
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $realpath_root;
Несколько предыдущих релизов остаются на диске. Это и есть откат: та же операция со ссылкой, только в обратную сторону.
Миграции: сначала расширить, потом сжать
Код переключается мгновенно, а база — нет, и она у старой и новой версии общая. Отсюда правило: каждая миграция должна быть совместима и с тем кодом, который работает сейчас, и с тем, который придёт ему на смену. Тогда порядок выкатки и откат перестают зависеть от состояния схемы.
Переименование колонки при таком подходе превращается из одной операции в несколько релизов.
- Миграция добавляет новую колонку. Старый код её не замечает.
- Выходит релиз, который пишет в обе колонки, а читает по-прежнему из старой.
- Фоновая задача небольшими порциями переносит накопленные значения.
- Выходит релиз, который читает из новой колонки.
- Когда всё стабильно, отдельная миграция удаляет старую.
Выглядит дольше, зато на каждом шаге можно остановиться и вернуться назад. Разрушающие изменения — удаление колонок и таблиц — всегда выкатываются отдельно и с задержкой: откатить код легко, вернуть удалённые данные можно только из резервной копии.
Вторая часть правила касается блокировок. Изменение структуры большой таблицы проверяют на копии базы сопоставимого размера: на тестовых данных оно занимает секунду, а в продакшне способно остановить запись на всё время перестроения.
После переключения: проверка и откат
Переключить версию — половина дела. Вторая половина — быстро понять, что новая версия работает, и ничего не потерять по дороге.
- Проверка после выкатки. Скрипт деплоя сам запрашивает ключевые страницы и служебный адрес проверки состояния. Если ответ не тот, ссылка возвращается на предыдущий релиз без участия человека.
- Наблюдение. Первые минуты после релиза — время смотреть на графики ошибок и времени ответа, а не закрывать ноутбук.
- Фоновые процессы. Обработчикам очередей дают команду завершить текущую задачу и перезапуститься на новом коде. Обрывать их на середине нельзя.
- Сессии и загрузки. Всё, что должно пережить релиз, хранится вне каталога релиза — иначе пользователям придётся заново входить после каждого обновления.
- Переключатели функций. Новую возможность можно выкатить выключенной и включить позже — сначала для себя, потом для всех. Так выкатка кода и запуск функции становятся двумя разными событиями.
Откат, который ни разу не проводили, — такой же миф, как бэкап, который ни разу не разворачивали.
Когда нужен второй контур
Blue-green — следующий шаг той же идеи: существуют два одинаковых окружения, новая версия разворачивается на неактивном, проверяется, и трафик переключается на балансировщике. Постепенная выкатка идёт ещё дальше: новую версию сначала получает небольшая доля запросов.
Оба подхода требуют второго комплекта ресурсов и балансировщика и оправданы там, где минута простоя стоит дорого или серверов несколько. Для проекта на одном сервере схема со ссылкой даёт большую часть пользы за малую часть сложности.
И честная оговорка: иногда простой дешевле. Если тяжёлую миграцию можно провести за пять минут ночью, предупредив пользователей, строить ради неё многошаговую схему незачем. Обновление без простоя — инструмент, а не догма. Плановые работы в согласованное окно — нормальная практика, если о них знают заранее; как это записывают в договоре — в статье о том, как читать SLA.
Чек-лист выкатки
- Релиз собирается в отдельном каталоге, переключение — одной операцией.
- Общие данные и секреты вынесены из каталога релиза.
- Миграции совместимы с предыдущей версией кода; разрушающие — отдельно и позже.
- Тяжёлые изменения схемы проверены на копии реального размера.
- Кэш прогревается до переключения, а не после.
- После переключения выполняется автоматическая проверка, при неудаче — автоматический откат.
- Фоновые процессы перезапускаются аккуратно.
- Откат отрепетирован, предыдущие релизы хранятся на сервере.
- Перед релизом есть свежая резервная копия базы.
Когда все пункты выполнены, релиз перестаёт быть событием — а значит, его можно делать часто и небольшими частями. Это и есть главный выигрыш: маленькое изменение проще проверить, проще откатить и проще объяснить. Этого порядка мы придерживаемся и в сервисах, которые разрабатываем, и в проектах, которые берём на сопровождение.