Почему база данных требует собственного CI/CD-пайплайна
Вы работаете над приложением, которым каждый день пользуются реальные люди. Когда нужно выкатить новую фичу, вы меняете код, прогоняете его через пайплайн — и новая версия уходит в продакшн. Если что-то пошло не так, вы откатываетесь на предыдущую версию, и всё возвращается в норму за считанные минуты. Процесс кажется гладким, потому что ваше приложение не хранит внутри своего кода никаких важных данных.
А теперь представьте, что нужно добавить одну колонку в таблицу пользователей. На локальной машине вы выполняете ALTER TABLE user ADD COLUMN ... — и готово. Но в продакшне в этой таблице миллионы строк с реальными данными. Приложения каждую секунду читают и пишут в неё. Некоторые запросы могут сломаться, если новая колонка изменит структуру индексов. Соединения с базой могут уйти в тайм-аут, если операция изменения затянется.
В этом и заключается фундаментальное различие, которое делает изменения в базе данных принципиально иными, чем изменения в приложении. И именно поэтому относиться к миграциям БД как к обычному развёртыванию кода — значит рано или поздно получить серьёзные проблемы.
Stateless vs Stateful: ключевое различие
Приложения stateless (не хранят состояние). Вы можете убить инстанс, заменить его новым или откатиться на старую версию — ничего не потеряется. Коду всё равно, кто использовал его раньше. Каждое развёртывание — это чистая замена.
Базы данных — stateful (хранят состояние). Они содержат данные, которые должны оставаться целостными, непротиворечивыми и доступными для активных пользователей. Схема — это контракт между вашим приложением и данными. Когда этот контракт меняется внезапно или некорректно, данные могут повредиться, приложения начнут выбрасывать ошибки, а пользователи потеряют доступ.
Это различие полностью меняет подход к управлению изменениями. Типичный пайплайн приложения проверяет, собирается ли код, проходят ли тесты, и затем развёртывает. Пайплайн базы данных должен отвечать на более сложные вопросы:
- Совместимо ли это изменение схемы с текущей версией приложения?
- Заблокирует ли эта операция таблицу слишком надолго?
- Будут ли существующие данные читаемы после изменения?
- Если что-то пойдёт не так на полпути, как вернуться в безопасное состояние без потери данных?
Проблема тайминга
Пайплайны приложений могут запускаться в любое время, хоть несколько раз в день. Вы пушите код, пайплайн отрабатывает — новая версия на боевом сервере. Весь процесс занимает минуты.
Пайплайны базы данных часто должны запускаться в определённое время. Возможно, в часы низкой нагрузки. Возможно, после подтверждения, что нет долгих транзакций. Возможно, только после ручного одобрения от того, кто понимает продакшн-нагрузку.
Если пайплайн приложения упал, вы просто переразвёртываете предыдущую версию. Просто. Если пайплайн базы данных упал на середине миграции, вы получаете частичную миграцию: половина изменений применена, половина — нет. Это одна из самых сложных проблем, и она требует тщательного планирования до того, как вы вообще запустите пайплайн.
Совместимость — не опция
Когда вы развёртываете новую версию приложения, она ожидает определённую схему базы данных. Когда вы запускаете миграцию, она эту схему меняет. Если они не синхронизированы — всё ломается.
Сложность в том, что во время развёртывания старая и новая версии приложения могут работать одновременно. Blue-green-деплой, canary-релизы и rolling-обновления создают окно, в котором активны несколько версий приложения. Схема базы данных должна быть совместима со всеми ними одновременно.
Это означает, что нельзя просто добавить колонку и сразу начать её использовать в том же развёртывании. Нужен многошаговый подход: сначала добавить колонку без её использования, развернуть приложение, а затем начать использовать колонку в следующем развёртывании. Такой паттерн обратно совместимых миграций критически важен для zero-downtime-деплоя и требует координации между пайплайном приложения и пайплайном базы данных.
Зачем нужны отдельные пайплайны
Может возникнуть соблазн включить миграции базы данных как шаг внутри пайплайна приложения. В конце концов, и то, и другое — часть доставки фичи. Но их смешивание создаёт несколько проблем:
Следующая диаграмма показывает, как два пайплайна расходятся по сложности и проверкам безопасности:
Ваш пайплайн приложения запускается при каждом изменении кода. Миграции базы данных должны запускаться только тогда, когда схема действительно меняется. Запуск ненужных миграций добавляет риск и замедляет развёртывание.
Откат приложения — прост. Откат базы данных — нет. Если ваш пайплайн объединяет и то, и другое, откат приложения может также откатить изменение в БД, что приведёт к потере данных.
Пайплайны приложений быстрые. Миграции базы данных могут быть медленными, особенно на больших таблицах. Медленная миграция может заблокировать весь пайплайн развёртывания, задерживая другие изменения, которые даже не касаются базы данных.
Пайплайны приложений предполагают stateless-окружение. Пайплайны базы данных должны понимать текущее состояние схемы, объём данных и продакшн-нагрузку. Это принципиально разные задачи.
Как выглядит хороший пайплайн базы данных
Хорошо спроектированный пайплайн базы данных не обязан быть сложным. Он просто должен уважать природу stateful-систем. Вот что он должен делать:
Запускать каждую миграцию как повторяемый скрипт, который можно ревьюить, тестировать и выполнять консистентно. Каждая миграция должна быть отдельным файлом с чётким номером версии, содержащим как прямой, так и обратный шаги.
Тестировать миграции на базе, максимально приближенной к продакшну — не только по схеме, но и по объёму и структуре данных. Миграция, которая выполняется за секунду на пустой тестовой базе, может занять двадцать минут на продакшне.
Запускать миграции в контролируемом порядке, по одной. Никогда не объединяйте несколько изменений схемы в одну операцию. Каждая миграция должна быть маленькой, сфокусированной и обратимой.
Верифицировать результат после каждой миграции. Проверять, что схема соответствует ожиданиям, индексы на месте, существующие данные целы.
Предоставлять чёткий путь вперёд или назад, если что-то пошло не так. Это означает наличие протестированных скриптов отката, знание времени их выполнения и понимание, какие данные могут быть затронуты.
Практический чек-лист для вашего пайплайна базы данных
Прежде чем настраивать пайплайн базы данных, убедитесь, что можете ответить на эти вопросы:
- Может ли каждая миграция выполняться независимо, или она зависит от других миграций?
- Обратима ли каждая миграция, и тестировали ли вы откат?
- Совместима ли миграция с текущей версией приложения и со следующей?
- Сколько времени займёт миграция на вашем продакшн-объёме данных?
- Заблокирует ли миграция таблицы, и если да, то на сколько?
- Что произойдёт, если миграция упадёт на середине?
- Кто должен утвердить запуск этой миграции в продакшн?
- В какое время суток должна выполняться эта миграция?
- Как вы проверите, что миграция прошла успешно?
Вывод
Изменения в базе данных — это не изменения кода. Они работают с живыми данными, влияют на активных пользователей и несут реальный риск. Относиться к ним как к коду приложения — значит рано или поздно получить даунтайм, проблемы с данными или и то, и другое. Отдельный пайплайн для изменений базы данных даёт вам контроль, безопасность и предсказуемость, которые необходимы. Он не обязан быть сложным. Он просто должен быть спроектирован для того, чем управляет: stateful-данными, которые живут долго и от которых зависят люди.