Автоматическое тестирование миграций БД: где ломается выкатка
Сбой на выкатке редко случается в самой миграции — он зреет в расхождении схемы. Боевая база и репозиторий описывают разные состояния: изменение когда-то применили вручную, чтобы разблокировать релиз, в цепочку миграций не зафиксировали — и следующая выкатка падает на несуществующем объекте либо создаёт дубль. Проверка изменений схемы устроена как сравнение слепка боевой базы с результатом прогона миграций с нуля: без неё расхождение всплывает только в момент сбоя.
Вторая точка отказа — блокировка. Изменение типа колонки или перестройка индекса на большой таблице держит блокировку минутами: запись встаёт, очередь соединений растёт, сервис деградирует. На пустом тестовом стенде такое изменение проходит мгновенно, поэтому решение о выкатке принимают вслепую. Прогон на копии боевых данных показывает, сколько держится блокировка и переживёт ли её пиковая нагрузка.
Третья — откат изменения, который никто не проверял. Обратные скрипты пишут по остаточному принципу и не гоняют в конвейере. При откате релиза код возвращается к прежней версии, а схема остаётся новой: запросы падают на несовпадении типов, и восстановление затягивается дольше, чем длилась сама выкатка.
Закрывается это циклом: применить миграции на копии боевых данных, откатить, применить снова и сверить итоговую схему с эталонным слепком. Тогда необратимое изменение видно до прода, а не после.
Что даёт проверка изменений схемы до выкатки: время и класс отказов
Пока изменения схемы проверяются руками на копии боевой базы, каждая выкатка содержит паузу: инженер ждёт, пока накатится набор миграций, и только потом смотрит, поднялось ли приложение. На потоке из нескольких релизов в неделю это часы ожидания и отдельный класс отказов — «упало на схеме», который ловится уже после переключения трафика.
Автоматический прогон миграций в конвейере закрывает это на этапе сборки. Изолированное окружение базы разворачивается из слепка на той же версии PostgreSQL, что и боевая: накат всех миграций по порядку, откат до предыдущего состояния и повторный накат. Время выпуска перестаёт зависеть от того, кто именно сегодня дежурит.
Дальше проверяются свойства, которые руками почти не тестируют. Обратная совместимость схемы — приложение предыдущей версии продолжает работать с новой схемой, значит откат релиза не требует отката миграции. Идемпотентность миграций — повторный запуск даёт тот же результат, и упавшую на середине миграцию можно безопасно прогнать снова.
Отдельная проверка — простой из-за блокировок: тест замеряет, сколько держится исключительная блокировка при изменении таблицы, и помечает операции, останавливающие запись на живой базе. Такие шаги разбиваются на безопасные: добавить колонку, заполнить фоном, переключить чтение, удалить старое.
Итог измерим: отказы, связанные со схемой, уходят из статистики выпусков, повторные прогоны не требуют ручного вмешательства, а предсказуемость выпуска перестаёт зависеть от конкретного инженера.
Подходы к прогону изменений схемы: пустая база, копия, реплика
Достоверность прогона определяется не самим фактом запуска миграции, а тем, на какой базе он идёт. Пустая база ловит синтаксис, порядок применения и обратимость изменения, но молчит о том, что встретится на боевых данных: существующие строки в новом обязательном столбце, конфликт уникального индекса, перестроение большой таблицы. Именно на таких расхождениях чаще всего и падает выкатка.
Поэтому в CI/CD обычно держат не один вариант, а несколько — под разные стадии и разный уровень риска.
| Вариант | Когда подходит | Ограничения |
|---|---|---|
| Прогон на пустой базе | На каждый коммит: проверка синтаксиса, порядка применения и отката | Не воспроизводит конфликты с существующими данными и объёмные перестроения таблиц |
| Упрощённая база с репрезентативной выборкой | Быстрые проверки, где нужны данные, но полная копия слишком тяжёлая | Расхождение с боевой схемой накапливается: тесты проходят там, где реальная выкатка упадёт |
| Копия боевой схемы с обезличенными данными | Предрелизная проверка перед выкаткой на критичный контур | Нужны снимок, обезличивание и регулярная актуализация; подготовка занимает время |
| Теневая реплика | Оценка длительности блокировок и поведения на реальном объёме и потоке | Требует изоляции от боевого трафика и отдельных ресурсов, дороже в поддержке |
Комбинируем по возрастанию цены и достоверности: пустая база на каждый коммит, копия — на ветке перед выкаткой, теневая реплика — точечно для миграций, которые трогают большие таблицы или меняют типы столбцов. Упрощённая база полезна как быстрый компромисс, но её нужно периодически сверять с боевой схемой — иначе тесты начинают подтверждать то, чего в реальности нет.
Как выстроить проверку миграций в конвейере: этапы работ
Миграции — единственный шаг развёртывания, который меняет данные и почти всегда необратим. Незамеченная блокировка на крупной таблице останавливает запись прямо во время релиза, а рассинхрон схемы всплывает уже после. Ниже — порядок работ, который делает эту операцию предсказуемой.
-
Инвентаризация миграций. Собираем накопленные файлы, отделяем идемпотентные от одноразовых и помечаем опасные: смену типа колонки, перестройку крупной таблицы, удаление столбцов.
-
Эталонная схема и версионирование схемы. Снимаем дамп актуального состояния и храним его вместе с кодом — каждая миграция обязана приводить структуру к эталону. Появляется детерминированная проверка вместо «на тестовой базе вроде работало».
-
Чистые окружения в конвейере. Поднимаем пустую базу нужной ветки и прогоняем два сценария: накат всех миграций с нуля и обновление с состояния прошлого релиза.
-
Автотесты на схему и данные. Сравниваем структуру с эталоном, проверяем обратимость откатных миграций и целостность данных на подготовленном наборе. Красный тест блокирует сборку до исправления.
-
Проверка блокировок под нагрузкой. Прогоняем миграцию на объёме, близком к рабочему, и снимаем план выполнения запроса: сколько держится блокировка, не перестраивается ли индекс целиком, укладывается ли операция в окно обслуживания.
-
Регламент. Миграция попадает в релиз только с зелёными проверками, а накат в рабочий контур вручную — через отдельную заявку с фиксацией причины.
-
Передача. Отдаём описание конвейера, порядок дежурства и разбор типовых сбоев, чтобы команда сопровождала схему дальше сама.
Что заказчик получает на руки: конфигурации, тесты, регламенты
Работа заканчивается не отчётом, а комплектом, который ваша команда запускает и поддерживает без нас. Всё, что настроено на тестовом контуре, передаётся в том же виде, в котором работает на основном, — с описанием, почему выбраны такие пороги и как их менять.
- Настройки конвейера непрерывной поставки: шаги сборки, запуск миграций на пустой базе и на копии боевых данных, условие остановки выпуска при расхождении схемы.
- Набор автотестов схемы: применение и откат каждой миграции, сверка таблиц, индексов, ограничений, значений по умолчанию и прав после наката.
- Настройки окружений и скрипты подъёма/сброса: разворот базы за считанные минуты, предсказуемые тестовые данные, сброс без остаточных объектов между прогонами.
- Политики доступа к копиям: кому и на какой срок выдаются обезличенные копии, где они хранятся, как отзываются права и что попадает в журнал.
- Регламент выпуска: действия при конфликте миграций, правила совместимости соседних версий схемы, точка отката и ответственный за решение.
- Комплект документации: карта конвейера, описание окружений, перечень проверок с примерами падений, порядок разбора инцидентов.
Отдельно фиксируем, как добавить новую проверку или окружение своими силами: обычно это копия существующего блока с изменёнными параметрами. Такой комплект снимает зависимость от подрядчика — развивать тестирование миграций можно внутри команды.
Кейс: как схема перестала ломать выкатку после внедрения проверок
До: схема расходилась между средами
Миграции выпускались тем же конвейером, что и код, но проверялись глазами: ревьюер читал текст миграции и сверял его с текущей схемой. Тестовый стенд догонял прод вручную и не всегда. Из десяти релизов два-три падали на применении — блокировка таблицы на большом объёме данных, конфликт с уже существующим индексом, снятая колонка, которую ещё читал предыдущий релиз кода. Дрейф схемы копился месяцами: на тестовом стенде не хватало части полей, а на проде жили индексы, которых не было ни в одном репозитории.
Что изменили
Каждую миграцию прогоняем дважды: на пустой базе — проверка применимости с нуля, и на свежей копии прод-схемы — проверка совместимости с накопленными данными. Обратный сценарий обязателен: без рабочего отката миграция не проходит стадию. Порядок изменений держим в журнале Liquibase, поэтому откат шага назад — такая же автоматическая операция, как и применение. Снимки схемы из dev, тестового стенда и прода сравниваются на каждой сборке: расхождение блокирует выкатку с указанием конкретного объекта.
Что стало
Время выпуска сократилось с двух-трёх дней до нескольких часов: стадия проверки идёт параллельно сборке и не требует ручного прогона перед релизом. Отказов на выкатке — один за квартал вместо двух-трёх из десяти релизов. Расхождений между средами было больше тридцати объектов, стало ноль — проверка не пропускает их дальше сборки. Разбор инцидентов на этапе изменения схемы ушёл из практики: конфликт виден на стадии проверки, а не на боевой базе под нагрузкой.
Чем прогон на пустой базе отличается от прогона на копии боевой?
Пустая база проверяет только то, что миграция синтаксически корректна и применяется в нужном порядке: таблицы создаются, индексы встают, внешние ключи валидны. Достоверность такой проверки заканчивается там, где начинается поведение на данных. Планировщик запросов на пустых таблицах выбирает один план, а на боевых объёмах — другой: обход индекса, незаметный на десятке строк, на реальном распределении даёт блокировки и рост времени отклика.
Копия боевой закрывает этот разрыв: срез с сохранённым объёмом тестовых данных и распределением значений — редкие ветки, пустые значения, дубликаты, строки предельной длины — заставляет ограничения работать по-настоящему. Ограничение целостности срабатывает только тогда, когда в таблице есть строка, которую оно должно отклонить; на пустой базе этот сценарий не воспроизводится.
Отдельная причина расхождений — диалект базы. На другом диалекте не воспроизводится приведение типов, сравнение строк с учётом регистра, порядок сортировки и обработка пустых значений. Миграция проходит в конвейере и падает уже на боевой, когда откат стоит дороже всего.
Поэтому держим оба прогона: пустая база ловит ошибки в самой миграции и в порядке применения с нуля, копия боевой отвечает за достоверность проверки на реальных объёмах. Срез обезличиваем, чтобы персональные данные не попадали в тестовый контур, но состав столбцов, типы и распределение сохраняем — иначе проверка снова становится формальной.
Сомнения инженера: сложные доработки схемы, дрейф, откаты
Возражения против автотестов миграций обычно предметные: схема уже живёт в проде, часть данных под ограничениями, а изменение может держать таблицу часами. Ниже — как каждый из этих случаев проверяется на стенде, а не в бою.
Схема в проде уже разошлась с миграциями — тесты это покажут?
Стенд поднимается из накатанной цепочки миграций, и расхождение структуры с продом видно на первом же прогоне — до выката, а не после. Дальше расхождение либо закрывают новой миграцией, либо приводят прод к цепочке отдельным скриптом, который тоже проходит проверку.
Изменение схемы идёт долго и блокирует таблицу — что с этим делают?
Прогон на копии продового объёма показывает, сколько реально держится блокировка. Если дольше допустимого окна — изменение разбивают на шаги: добавить колонку без ограничения, заполнить порциями, переключить чтение, снять старое. Каждый шаг — отдельная миграция со своим тестом.
Как проверяется обратная совместимость и возможен ли откат?
Тесты поднимают рядом прошлую и новую версию приложения и проверяют, что обе работают на одной схеме — тогда откат выката не требует отката базы. Обратный скрипт прогоняется на данных, оставшихся после прямого; если откат без потери невозможен, это фиксируется на согласовании заранее.
Данные чувствительные — как тестировать, если их нельзя копировать?
Стенд получает синтетический набор с той же структурой и похожим распределением значений, поэтому ограничения внешних ключей и уникальности воспроизводятся полностью. Чувствительные поля заменяют генераторами — проверка идёт на реальных правилах, а не на пустых таблицах.
Обсудим вашу задачу: что уточним и с чем вернёмся
Чтобы вернуться с оценкой и планом, а не с общими соображениями, нужен разбор вашего конвейера: как правки схемы проходят путь от изменения в ветке до релиза и что происходит между этими точками. Ниже — вводные, которых нам обычно хватает, чтобы посчитать объём работ.
- СУБД и инструмент миграций: чем накатываются изменения, версионируются ли они пофайлово, есть ли транзакционная обёртка и отдельный шаг отката.
- Объём и характер правок: сколько изменений схемы в неделю, есть ли тяжёлые перестроения больших таблиц и блокирующие операции.
- Что уже проверяется: ручные прогоны на копии, ревью запросов, самописные скрипты — и на каком шаге они дают сбой.
- Окружения и данные: есть ли отдельный контур для прогонов, как готовится тестовый набор, обезличиваются ли данные.
- Ограничения: допустимое окно недоступности, требования к откату, порядок согласования релизов.
- Доступы и ответственность: кто владелец репозитория, раннеров и настроек окружений, кто примет регламенты на стороне эксплуатации.
После разбора вернёмся со схемой конвейера, оценкой и планом работ, а по итогам внедрения передадим регламенты: что запускается на каждое изменение, какие проверки блокируют слияние, как действовать при падении. Дальше остаёмся на сопровождении — донастраиваем правила под новые типы миграций и разбираем инциденты. Опишите задачу — вернёмся с оценкой и планом.







