The databasePart 5 of 6

Деплой — это перевод ветки в продакшн, а не миграция

Выкатка в продакшн обычно означает перенос данных: снять дамп с dev, восстановить его в отдельной прод-базе, прогнать миграции и надеяться, что окружения не разошлись. На таймлайнах Postgres с копированием при записи всё устроено иначе — продакшн это ветка, а выход в продакшн это перевод ветки в продакшн. Байты не перемещаются: продакшн форкается от head ветки dev, повторные деплои продвигают продакшн и применяют только ожидающие миграции, откат — это ветка на момент времени, и каждое окружение масштабируется до нуля независимо. Вот модель и её механика.

Деплой — это перевод ветки в продакшн, а не миграция

Обычный путь в продакшн перемещает данные. Вы снимаете снимок с dev, восстанавливаете его в отдельной прод-базе, прогоняете там миграции и надеетесь, что за это время две схемы не разошлись. В итоге вы держите две базы данных, которые должны быть одинаковыми, но постепенно перестают ими быть, а шаг копирования замедляется по мере роста данных.

Если ваши базы данных — это таймлайны с копированием при записи поверх общего хранилища страниц, деплой устроен совершенно иначе. Продакшн — это не отдельная база, в которую вы копируете данные, а ветка. Выход в продакшн — это перевод ветки в продакшн: операция с метаданными, которая сдвигает указатель, а не байты. Этот пост — про модель, вокруг которой построена архитектура таймлайнов платформы, и про механику под ней.

Он опирается на два примитива, разобранных в других постах: ветвление с копированием при записи на LSN и compute с масштабированием до нуля. Если они вам в новинку, сначала пробегитесь по ним — этот пост их объединяет.


Модель: продакшн — это устойчивая ветка, форкнутая от dev

База данных проекта — это таймлайн. Изо дня в день этот таймлайн — окружение dev, где агент собирает проект и итеративно его улучшает. Продакшн — это второй таймлайн: устойчивая ветка, на которую идёт трафик клиентов.

Первый деплой форкает текущий head ветки dev в новый, устойчивый прод-таймлайн. С этого момента продакшн — это собственный таймлайн с данными реальных пользователей, а dev продолжает работать на своём таймлайне, оставаясь нетронутым. Поскольку ветвление — это O(1)-копирование при записи, форк не копирует данные: деплой стоит одинаково, держит ли база данных килобайт или сто гигабайт.

first deploy: fork dev head → prod

shared history up to the fork

dev timeline
agent builds here · keeps running

prod timeline
durable · customer traffic

Из тезиса «продакшн и dev — это один и тот же движок, в одной ветке друг от друга» прямо следуют два вывода:

  • Никакого расхождения окружений. Продакшн — это не отдельно поднятая база, в которую вы делаете дамп и восстановление, а форк, унаследовавший журнал миграций dev в момент разделения. Деплой применяет только действительно ожидающие миграции (DDL, который dev уже выполнил, а продакшн ещё нет), поэтому не возникает зазора «работает на dev, ломается на проде» из-за двух баз, поднятых и мигрированных независимо.
  • Превью на реальных данных продакшна. Нужно отладить проблему в продакшне или проверить изменение на данных продакшн-масштаба? Форкните продакшн в одноразовый таймлайн и поковыряйтесь в нём — копирование при записи, изоляция, удаление по завершении. Реальный прод-таймлайн этого не заметит.

Механика перевода в продакшн

Первый перевод в продакшн — это единственный сдвиг метаданных: форкнуть head ветки dev в устойчивый прод-таймлайн. Решение чистое — по текущим веткам определить, что станет продакшном, — а выполнение это операция ветвления без копирования данных:

pageserverneon-conn-proxybackend (promotion)deploy triggerpageserverneon-conn-proxybackend (promotion)deploy triggerpromoteToProduction(project)planPromotion(branches) %% pick the dev-serving timelinefork dev head → prod timeline (O(1), CoW)create_timeline(ancestor=dev head)record prod branch + setServingBranch("prod")provision prod compute (env=prod) for the prod timeline

Логика перевода в продакшн — это маленькая чистая функция, planPromotion: она смотрит на ветки проекта и решает, какой таймлайн станет продакшном (и ничего не делает, если dev уже совпадает с продакшном). Слой выполнения форкает head ветки dev, записывает новую прод-ветку, помечает её как обслуживающую prod и поднимает прод-compute, нацеленный на неё. Откатить неудачный деплой — это тот же примитив «ветка на LSN», что и путешествие во времени, только применённый к прод-таймлайну: выбрать точку раньше, форкнуть, перенаправить — без восстановления из резервной копии.

Это первый перевод в продакшн. Повторный деплой не форкает заново: он сохраняет действующий прод-таймлайн — данные реальных пользователей переживают релизы — и продвигает его. Он закрепляет только что выпущенный коммит и применяет все ожидающие db/migrations к продакшну как DDL «только вперёд» (runPendingMigrations({ env: "prod" })), предварительно поставив контрольную точку LSN перед деплоем как якорь для отката. То есть деплой действительно прогоняет миграции схемы на продакшне; чего он не делает никогда — так это не копирует данные. Единственный путь, на котором данные продакшна перезаписываются данными dev, — это явный сброс с отдельным снимком, а не побочный эффект выкатки.


Отдельный compute на каждое окружение — и оба масштабируются до нуля

Продакшн и dev должны быть активны одновременно: клиенты идут в продакшн, пока агент продолжает итерации на dev. Один процесс Postgres привязан к одному таймлайну на момент запуска, поэтому окружения получают отдельные computeneon-compute для dev и neon-compute-prod для продакшна — каждый нацелен на свой таймлайн.

Звучит как двойная стоимость, так бы и было, если бы оба compute не масштабировались до нуля независимо (прокси для масштабирования до нуля маршрутизирует по окружениям на основе имени базы данных и будит каждое по запросу):

  • Приложение без посетителей, и никто ничего не собирает → оба на нуле.
  • Заходит клиент → просыпается продакшн; dev остаётся на нуле.
  • Агент итерирует → просыпается dev; продакшн остаётся на нуле.

Вы платите за используемое окружение, а не за поднятое. Манифесты генерируются для каждого окружения из единого шаблона (generateNeonComputeManifests(env)), поэтому прод-compute — это тот же набор ресурсов, что и у dev, лишь с другим именем и таймлайном.

Материализация ленивая: у проекта, который ещё не деплоился, ровно один compute (dev). Прод-таймлайн и его compute возникают при первом переводе в продакшн — так что длинный хвост никогда не деплоившихся экспериментов не несёт вообще никаких накладных расходов на продакшн.


Что это вам даёт

  • Деплой мгновенный и не зависит от размера данных. Это форк плюс сдвиг указателя, а не дамп/восстановление. База на 50 ГБ переходит в продакшн так же быстро, как и пустая.
  • Никакого расхождения окружений. Тот же движок, в одной ветке друг от друга — продакшн форкнут от dev и унаследовал его журнал миграций, поэтому деплой применяет только ожидающий DDL и никогда не делает слепой полный повтор на отдельно собранной базе.
  • Превью на реальных данных. Форкните продакшн в одноразовый таймлайн, проверьте на данных продакшн-масштаба, удалите. Продакшн остаётся нетронутым.
  • Откат — это момент времени. Продакшн — это таймлайн, поэтому возврат неудачного деплоя — это та же операция «ветка на LSN», что и путешествие во времени для кода и данных, только применённая к продакшну.
  • Простой бесплатен, в каждом окружении. Масштабирование до нуля на каждом compute означает, что второе окружение — не постоянная статья расходов; оно появляется при обращении и исчезает, когда не нужно.

Суть в двух словах

Перестаньте думать о продакшне как о месте, куда вы копируете свою базу данных, и начните думать о нём как о ветке, которую вы переводите в продакшн. На Postgres с разделёнными хранилищем и compute «деплой» перестаёт быть событием миграции данных с шагом копирования и риском расхождения и становится тем, чем должен быть: сдвигом указателя, который говорит «вот эту версию получают клиенты». Данные уже были на месте — общие, с копированием при записи, в одной ветке от каждого окружения.

Создавай на той самой платформе, о которой эти посты.

Опиши своё приложение простыми словами — Adorable напишет код, настроит базу данных и выкатит его в онлайн.

Начать бесплатно