Теоретический конспект для студентов • Тема 4 (§2.2 с.7–8) • Занятие №28

Управление жизненным циклом и эволюцией схемы баз данных: миграции, версионирование и Zero-Downtime развертывание

Фундаментальные основы версионирования реляционных структур данных, механизмы Flyway и Liquibase, структура системной таблицы метаданных, паттерн Expand/Contract и регрессионный контроль клиентских систем.

1. Введение: Почему базы данных требуют специального версионирования?

В современной промышленной разработке исходный код приложений управляется системами контроля версий (Git). Любое изменение кода фиксируется коммитом, может быть ветвлено, проверено автотестами и при необходимости откачено на любую прошлую версию.

Однако реляционная база данных обладает свойством состояния (Stateful). В отличие от кода микросервиса, базу данных нельзя просто «стереть и запустить заново из Git» в продуктивной среде — в ней содержатся реальные финансовые, операционные и персональные данные пользователей. Схема базы данных должна непрерывно эволюционировать во времени, сохраняя накопленное состояние.

Проблема ClickOps / Ad-Hoc DDL: Ручное выполнение скриптов ALTER TABLE или CREATE INDEX администратором через GUI-клиенты (pgAdmin, DBeaver) на боевом сервере приводит к фатальным последствиям:
  • Рассинхронизация схем: тестовый стенд, стейджинг и прод имеют разные наборы колонок и ограничений;
  • Отсутствие воспроизводимости: новый разработчик не может развернуть копию БД для отладки;
  • Невозможность автоматизированного тестирования и безопасного отката релиза.

2. Пошаговые блок-схемы применения миграций схемы БД

Процесс применения миграций (наката изменений) в СУБД строго детерминирован и выполняется миграционным движком (например, Flyway) по следующему алгоритму:

+-----------------------------------------------------------------------------------+ | АЛГОРИТМ НАКАТА МИГРАЦИЙ (FLYWAY MIGRATE) | +-----------------------------------------------------------------------------------+ │ ▼ [ 1. Подключение к целевой СУБД ] │ ▼ { Существует ли таблица flyway_schema_history в схеме? } │ │ НЕТ ДА │ │ ▼ ▼ [ Создать flyway_schema_history ] [ Прочитать журнал версий ] │ │ └────────────────┬─────────────────┘ │ ▼ [ 2. Сканирование папки миграций (classpath/sql) ] │ ▼ [ Сортировка скриптов по номеру версии: V1 < V2 < V3 ... ] │ ▼ [ 3. Цикл валидации уже примененных миграций ] │ { Контрольная сумма файла совпадает с checksum в flyway_schema_history? } │ │ НЕТ ДА │ │ ▼ ▼ [ АВАРИЙНЫЙ СТОП! Ошибка сборки: { Есть ли новые версии скриптов, } ValidateFailedException (файл изменен) ] { которых еще нет в таблице? } │ ┌────────┴────────┐ НЕТ ДА │ │ ▼ ▼ [ Схема актуальна! [ НАЧАЛО ТРАНЗАКЦИИ ] Завершение работы ] │ ▼ [ Выполнение DDL ] │ ▼ [ Запись в историю: version, checksum, execution_time, success = TRUE ] │ ▼ [ COMMIT ТРАНЗАКЦИИ ]

Стандарты наименования файлов миграций:

<Префикс><Версия>__<Описание>.sql

Префиксы:
  V  - Версионированная миграция (Versioned). Выполняется один раз. Версия уникальна (напр. V1, V2.1, V20261001).
  U  - Undo-миграция (Откат). Возвращает базу к состоянию, предшествующему соответствующей V-миграции.
  R  - Повторяемая миграция (Repeatable). Не имеет номера версии. Перезапускается каждый раз, когда меняется ее хэш (идеально для функций, представлений и процедур).

Примеры:
  V1__create_core_tables.sql
  V2__add_barcode_to_goods.sql
  U2__drop_barcode_from_goods.sql
  R__view_active_orders_summary.sql

3. Структура служебной таблицы flyway_schema_history

Служебная таблица версий — это «черный ящик» базы данных, хранящий полный аудит всех когда-либо примененных DDL-структур:

Колонка Тип данных Назначение и правила заполнения
installed_rank INT (PK) Порядковый номер применения (1, 2, 3...). Определяет физическую хронологию выполнения.
version VARCHAR(50) Номер версии из имени файла (например, 1, 2.0). Для repeatable-скриптов значение NULL.
description VARCHAR(200) Человекочитаемое описание, полученное из имени файла (символы подчеркивания заменяются пробелами).
type VARCHAR(20) Тип скрипта: SQL, UNDO, BASELINE, JDBC.
script VARCHAR(1000) Полное имя файла миграции, например: V2__add_barcode_to_goods.sql.
checksum INT Целочисленный хэш содержимого файла (CRC32 или SHA-256). Гарантирует иммутабельность миграции.
installed_by VARCHAR(100) Имя пользователя базы данных, под учетной записью которого производился накат (напр., postgres, ci_user).
installed_on TIMESTAMP Точная метка времени наката с точностью до микросекунд.
execution_time INT Время исполнения DDL в миллисекундах (критично для мониторинга длительных блокировок).
success BOOLEAN Флаг успешности: TRUE (успех), FALSE (аварийное завершение DDL).

4. Непрерывное развертывание (Zero-Downtime): Паттерн Expand/Contract

В распределенных системах высокой доступности (High Availability) остановка базы данных для проведения технического обслуживания (Maintenance Window) недопустима. При непрерывном развертывании (Rolling Update / Canary Deployments) старая и новая версии сервиса работают одновременно.

+─────────────────────────────────────────────────────────────────────────────────+ | АРХИТЕКТУРНЫЙ ПАТТЕРН РАСШИРЕНИЯ-СЖАТИЯ (EXPAND / CONTRACT) | +─────────────────────────────────────────────────────────────────────────────────+ 1. ИСХОДНОЕ СОСТОЯНИЕ: Приложение v1 читает и пишет колонку [ old_col ]. 2. ФАЗА EXPAND (Расширение): Накатывается миграция V2: добавляется [ new_col ] (NULLable, без жестких ограничений). База готова принимать данные обоих типов! 3. ФАЗА TRANSITION (Переходный период): Развертывается Приложение v2. Приложение v2: * Пишет в ОБЕ колонки: UPDATE/INSERT [ old_col, new_col ]; * Читает из [ old_col ] (или резервно из new_col); Фоновый воркер (Backfill): порциями копирует старые данные из old_col в new_col. 4. ФАЗА ПЕРЕКЛЮЧЕНИЯ ЧТЕНИЯ: Все клиенты обновлены до Приложения v2. Чтение переключается полностью на [ new_col ]. Колонка [ old_col ] больше не используется программным кодом. 5. ФАЗА CONTRACT (Сжатие): Накатывается миграция V3: удаление [ old_col ] и наложение NOT NULL на [ new_col ]. Удаление старых триггеров и представлений.
Золотое правило DDL при Zero-Downtime: Никогда не удаляйте и не переименовывайте колонку в одной миграции с изменением кода приложения! Разделяйте деструктивное действие минимум на 2–3 независимых релизных цикла.

5. Методология регрессионного тестирования клиентских приложений

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

Чек-лист регрессионной верификации:

  1. Проверка обратной совместимости вставки: Попытка выполнить INSERT INTO table (...) старым набором полей. Если вставка падает из-за нового NOT NULL ограничения — миграция признается дефектной.
  2. Анализ плана выполнения (Query Plan): Сравнение EXPLAIN (ANALYZE, BUFFERS) для ключевых запросов до и после создания/удаления индексов. Исключение появления незапланированного Seq Scan на больших таблицах.
  3. Проверка внешних ключей и триггеров: Тестирование каскадного удаления и модификации связных сущностей. Исключение ситуаций взаимных блокировок (Deadlocks).
  4. Верификация симметрии отката: Тестирование запуска Undo-скрипта. СУБД должна возвращаться в исходное состояние без остаточных мусорных объектов.

6. Вопросы для самопроверки

  1. В чем заключается принципиальное отличие версионирования исходного кода в Git от версионирования схемы реляционной базы данных?
  2. Какую информацию хранит колонка checksum в таблице flyway_schema_history и почему мигратор блокирует запуск при ее изменении?
  3. Почему добавление колонки с модификатором NOT NULL без указания DEFAULT на эксплуатируемой таблице является грубой ошибкой проектирования?
  4. Опишите все 4 фазы архитектурного паттерна Expand/Contract при разделении сущности на две независимые таблицы.
  5. В каких случаях для отката изменений предпочтительнее применить новую прямую миграцию (Forward-only fix V_next), чем выполнять Undo-скрипт?