Деплой — это перевод ветки в продакшн, а не миграция
Обычный путь в продакшн перемещает данные. Вы снимаете снимок с dev, восстанавливаете его в отдельной прод-базе, прогоняете там миграции и надеетесь, что за это время две схемы не разошлись. В итоге вы держите две базы данных, которые должны быть одинаковыми, но постепенно перестают ими быть, а шаг копирования замедляется по мере роста данных.
Если ваши базы данных — это таймлайны с копированием при записи поверх общего хранилища страниц, деплой устроен совершенно иначе. Продакшн — это не отдельная база, в которую вы копируете данные, а ветка. Выход в продакшн — это перевод ветки в продакшн: операция с метаданными, которая сдвигает указатель, а не байты. Этот пост — про модель, вокруг которой построена архитектура таймлайнов платформы, и про механику под ней.
Он опирается на два примитива, разобранных в других постах: ветвление с копированием при записи на LSN и compute с масштабированием до нуля. Если они вам в новинку, сначала пробегитесь по ним — этот пост их объединяет.
Модель: продакшн — это устойчивая ветка, форкнутая от dev
База данных проекта — это таймлайн. Изо дня в день этот таймлайн — окружение dev, где агент собирает проект и итеративно его улучшает. Продакшн — это второй таймлайн: устойчивая ветка, на которую идёт трафик клиентов.
Первый деплой форкает текущий head ветки dev в новый, устойчивый прод-таймлайн. С этого момента продакшн — это собственный таймлайн с данными реальных пользователей, а dev продолжает работать на своём таймлайне, оставаясь нетронутым. Поскольку ветвление — это O(1)-копирование при записи, форк не копирует данные: деплой стоит одинаково, держит ли база данных килобайт или сто гигабайт.
Из тезиса «продакшн и dev — это один и тот же движок, в одной ветке друг от друга» прямо следуют два вывода:
- Никакого расхождения окружений. Продакшн — это не отдельно поднятая база, в которую вы делаете дамп и восстановление, а форк, унаследовавший журнал миграций dev в момент разделения. Деплой применяет только действительно ожидающие миграции (DDL, который dev уже выполнил, а продакшн ещё нет), поэтому не возникает зазора «работает на dev, ломается на проде» из-за двух баз, поднятых и мигрированных независимо.
- Превью на реальных данных продакшна. Нужно отладить проблему в продакшне или проверить изменение на данных продакшн-масштаба? Форкните продакшн в одноразовый таймлайн и поковыряйтесь в нём — копирование при записи, изоляция, удаление по завершении. Реальный прод-таймлайн этого не заметит.
Механика перевода в продакшн
Первый перевод в продакшн — это единственный сдвиг метаданных: форкнуть head ветки dev в устойчивый прод-таймлайн. Решение чистое — по текущим веткам определить, что станет продакшном, — а выполнение это операция ветвления без копирования данных:
Логика перевода в продакшн — это маленькая чистая функция, planPromotion: она смотрит на ветки проекта и решает, какой таймлайн станет продакшном (и ничего не делает, если dev уже совпадает с продакшном). Слой выполнения форкает head ветки dev, записывает новую прод-ветку, помечает её как обслуживающую prod и поднимает прод-compute, нацеленный на неё. Откатить неудачный деплой — это тот же примитив «ветка на LSN», что и путешествие во времени, только применённый к прод-таймлайну: выбрать точку раньше, форкнуть, перенаправить — без восстановления из резервной копии.
Это первый перевод в продакшн. Повторный деплой не форкает заново: он сохраняет действующий прод-таймлайн — данные реальных пользователей переживают релизы — и продвигает его. Он закрепляет только что выпущенный коммит и применяет все ожидающие db/migrations к продакшну как DDL «только вперёд» (runPendingMigrations({ env: "prod" })), предварительно поставив контрольную точку LSN перед деплоем как якорь для отката. То есть деплой действительно прогоняет миграции схемы на продакшне; чего он не делает никогда — так это не копирует данные. Единственный путь, на котором данные продакшна перезаписываются данными dev, — это явный сброс с отдельным снимком, а не побочный эффект выкатки.
Отдельный compute на каждое окружение — и оба масштабируются до нуля
Продакшн и dev должны быть активны одновременно: клиенты идут в продакшн, пока агент продолжает итерации на dev. Один процесс Postgres привязан к одному таймлайну на момент запуска, поэтому окружения получают отдельные compute — neon-compute для dev и neon-compute-prod для продакшна — каждый нацелен на свой таймлайн.
Звучит как двойная стоимость, так бы и было, если бы оба compute не масштабировались до нуля независимо (прокси для масштабирования до нуля маршрутизирует по окружениям на основе имени базы данных и будит каждое по запросу):
- Приложение без посетителей, и никто ничего не собирает → оба на нуле.
- Заходит клиент → просыпается продакшн; dev остаётся на нуле.
- Агент итерирует → просыпается dev; продакшн остаётся на нуле.
Вы платите за используемое окружение, а не за поднятое. Манифесты генерируются для каждого окружения из единого шаблона (generateNeonComputeManifests(env)), поэтому прод-compute — это тот же набор ресурсов, что и у dev, лишь с другим именем и таймлайном.
Материализация ленивая: у проекта, который ещё не деплоился, ровно один compute (dev). Прод-таймлайн и его compute возникают при первом переводе в продакшн — так что длинный хвост никогда не деплоившихся экспериментов не несёт вообще никаких накладных расходов на продакшн.
Что это вам даёт
- Деплой мгновенный и не зависит от размера данных. Это форк плюс сдвиг указателя, а не дамп/восстановление. База на 50 ГБ переходит в продакшн так же быстро, как и пустая.
- Никакого расхождения окружений. Тот же движок, в одной ветке друг от друга — продакшн форкнут от dev и унаследовал его журнал миграций, поэтому деплой применяет только ожидающий DDL и никогда не делает слепой полный повтор на отдельно собранной базе.
- Превью на реальных данных. Форкните продакшн в одноразовый таймлайн, проверьте на данных продакшн-масштаба, удалите. Продакшн остаётся нетронутым.
- Откат — это момент времени. Продакшн — это таймлайн, поэтому возврат неудачного деплоя — это та же операция «ветка на LSN», что и путешествие во времени для кода и данных, только применённая к продакшну.
- Простой бесплатен, в каждом окружении. Масштабирование до нуля на каждом compute означает, что второе окружение — не постоянная статья расходов; оно появляется при обращении и исчезает, когда не нужно.
Суть в двух словах
Перестаньте думать о продакшне как о месте, куда вы копируете свою базу данных, и начните думать о нём как о ветке, которую вы переводите в продакшн. На Postgres с разделёнными хранилищем и compute «деплой» перестаёт быть событием миграции данных с шагом копирования и риском расхождения и становится тем, чем должен быть: сдвигом указателя, который говорит «вот эту версию получают клиенты». Данные уже были на месте — общие, с копированием при записи, в одной ветке от каждого окружения.