Когда миграции базы данных идут не так: откат (Rollback) против движения вперед (Roll-Forward)
Ваша команда только что выполнила миграцию базы данных в продакшене. Пять минут спустя панель мониторинга краснеет. Частота ошибок взлетает. Пользователи начинают сообщать о проблемах. Теперь нужно быстро принять решение: отменить изменение или написать новый фикс и двигаться дальше?
Этот момент отделяет команды, у которых есть план, от команд, которые начинают паниковать. И ответ никогда не бывает простым, вроде «просто сделай откат». Тип изменения, затронутые данные и состояние вашей системы — всё это определяет, какой путь безопаснее.
Два пути восстановления
Существует два принципиально разных способа восстановиться после неудачной миграции базы данных. Они работают по-разному, несут разные риски и применимы в разных ситуациях.
Откат (Rollback) означает отмену только что выполненной миграции. Вы запускаете down-миграцию, которая является точной противоположностью тому, что вы сделали. Если вы добавили колонку, down-миграция её удаляет. Если вы изменили тип данных, down-миграция меняет его обратно.
Движение вперед (Roll-Forward) означает, что вы оставляете проблемную миграцию на месте и пишете новую миграцию, которая исправляет проблему. Вы не идете назад. Вы движетесь вперед с исправлением.
Обе стратегии имеют право на существование. Хитрость в том, чтобы знать, какая из них подходит для вашей ситуации, до того, как она понадобится.
Когда делать откат
Откат лучше всего работает для изменений, которые безопасно отменить. Обычно это недеструктивные операции, при которых отмена изменения не приводит к потере или повреждению данных.
Хорошие кандидаты для отката:
- Добавление колонки, допускающей NULL
- Создание нового индекса
- Добавление новой таблицы
- Создание представления (view) или функции
Эти изменения являются аддитивными. Когда вы их отменяете, вы удаляете то, что было добавлено. Никакие существующие данные не теряются и не повреждаются в процессе.
Рассмотрим сценарий, где вы добавили колонку last_login_at в таблицу users. Колонка допускает NULL, поэтому с существующими строками всё в порядке. После развертывания вы обнаруживаете, что в коде приложения есть ошибка, из-за которой записываются неверные временные метки. Откат путем удаления колонки безопасен. Данные не пострадают, так как колонка была пуста или содержала данные, которые вам не нужно сохранять.
Когда двигаться вперед (Roll-Forward)
Движение вперед становится лучшим выбором, когда миграция является деструктивной или когда её откат причинит больше вреда, чем исходная проблема.
Ситуации, в которых roll-forward безопаснее:
- Удаление колонки или таблицы
- Изменение типа данных таким образом, что теряется точность
- Массовое изменение существующих значений данных
- Объединение таблиц
- Удаление ограничения NOT NULL, от которого зависят другие системы
Представьте, что вы выполнили миграцию, которая удалила колонку legacy_status. Данные в этой колонке потеряны. Написание down-миграции, которая добавит колонку обратно, не восстановит данные. Пользователи, которые зависели от этого поля статуса, теперь видят NULL. Ваш лучший ход — написать новую миграцию, которая пересоздаст колонку и заполнит её из резервной копии или из логов приложения.
Другой распространенный случай: вы изменили колонку с VARCHAR на INTEGER, преобразовав строковые значения в числа. Откат путем обратного изменения типа на VARCHAR рискован, потому что целочисленные значения могут не конвертироваться обратно в строки без потерь. Значение 42 станет "42", но что насчет значений, которые были усечены или округлены во время конвертации? Вы потеряли информацию. Roll-forward позволяет вам написать аккуратную миграцию, которая явно обрабатывает эти граничные случаи.
Написание рабочих down-миграций
Если вы решите поддерживать откат, ваши down-миграции требуют особого внимания. Они не могут быть механическим обращением up-миграции. Каждая down-миграция должна учитывать данные, которые существуют на момент отката.
Вот что делает down-миграцию опасной:
Рассмотрим конкретный пример. Вы добавили nullable колонку last_login_at в таблицу users, но в коде приложения есть ошибка. Безопасная down-миграция и roll-forward фикс будут выглядеть так:
-- Безопасная down-миграция: удаляем колонку, но только после проверки безопасности
BEGIN;
-- Шаг 1: Убедиться, что ни код приложения, ни представления не зависят от этой колонки
-- (Эта проверка выполняется в пайплайне развертывания, а не в SQL)
-- Шаг 2: Удаляем колонку
ALTER TABLE users DROP COLUMN IF EXISTS last_login_at;
COMMIT;
-- Roll-forward миграция: добавляем колонку с правильным именем
BEGIN;
-- Добавляем колонку с предполагаемым именем и типом
ALTER TABLE users ADD COLUMN last_login_at TIMESTAMP;
-- Опционально: заполняем из логов приложения или резервной копии
-- UPDATE users SET last_login_at = ... WHERE id IN (...);
COMMIT;
Down-миграция безопасна, потому что колонка nullable и аддитивна. Roll-forward миграция исправляет проблему без отмены изменения схемы.
- Она предполагает, что данные находятся в том же состоянии, что и во время выполнения up-миграции
- Она игнорирует строки, которые были добавлены или изменены после up-миграции
- Она слепо отменяет изменения схемы без проверки целостности данных
Безопасная down-миграция для добавления NOT NULL колонки со значением по умолчанию должна:
- Проверить, что удаление колонки не сломает запросы приложения
- Обработать любые строки, которые были вставлены после добавления колонки
- Убедиться, что ни одна связь по внешнему ключу не зависит от этой колонки
Для изменения типа данных с VARCHAR на INTEGER down-миграция должна обрабатывать значения, у которых нет чистого строкового представления. Возможно, вам придется привести целые числа обратно к строкам, но также обработать NULL и граничные случаи, которых не было у исходных строковых значений.
Реальные риски, которые нельзя игнорировать
Откат звучит просто, но он несет серьезные риски, которые команды обнаруживают только после того, как что-то пошло не так.
Потеря данных — самый большой риск. Когда миграция удаляет колонку, данные теряются. Никакая down-миграция не вернет их, если у вас нет резервной копии. Если вы не сделали резервную копию перед миграцией, откат означает принятие необратимой потери данных.
Зависимости миграций создают скрытые ловушки. Если миграция два зависит от колонки, добавленной миграцией один, откат к состоянию до миграции один сломает всё. Ваше приложение может упасть, потому что ожидает колонки, которых больше нет. Ваши данные могут стать несогласованными, потому что строки ссылаются на удаленные значения.
У roll-forward есть свои риски. Самый большой — время. Вам нужно написать новую миграцию, прогнать её через пайплайн и развернуть. В течение этого времени ваше приложение работает в сломанном состоянии. Пользователи видят ошибки. Ваша команда находится под давлением, что увеличивает шанс совершить еще одну ошибку.
Roll-forward также требует точного знания текущего состояния базы данных. Вы не можете написать исправление, основываясь на предположениях. Вам нужно точно знать, как выглядят данные прямо сейчас, а не то, как они выглядели, когда миграция проектировалась.
Принятие решения до того, как оно понадобится
Худшее время для выбора между откатом и движением вперед — когда продакшен горит. К этому моменту вы находитесь в стрессе, часы тикают, и ваша способность принимать решения нарушена.
Лучший подход — классифицировать каждую миграцию до её выполнения. Назначьте каждой миграции категорию восстановления:
- Безопасно для отката: Аддитивные изменения, такие как новые колонки, таблицы или индексы
- Требуется резервная копия перед откатом: Изменения, которые модифицируют существующие данные или удаляют nullable колонки
- Только roll-forward: Деструктивные изменения, такие как удаление колонок, изменение типов данных или объединение таблиц
Задокументируйте эту классификацию в файлах миграций или в вашем runbook по развертыванию. Когда что-то пойдет не так, ваша команда прочитает классификацию и выполнит предопределенную стратегию. Никаких споров. Никаких сомнений.
Краткий чек-лист для принятия решения
Прежде чем запускать любую миграцию в продакшене, задайте эти вопросы:
Следующее дерево решений поможет вам применить чек-лист под давлением:
- Является ли изменение аддитивным или деструктивным?
- Может ли down-миграция восстановить точное предыдущее состояние, включая данные?
- Есть ли у вас проверенная резервная копия, сделанная до миграции?
- Была ли down-миграция протестирована в стейджинге?
- Зависит ли эта миграция от других миграций, которые выполнялись ранее?
- Какова стоимость простоя, пока вы пишете roll-forward фикс?
Если вы не можете ответить на все эти вопросы, пока не запускайте миграцию в продакшене.
Конкретный вывод
Откат и движение вперед — это не взаимозаменяемые стратегии. Они применимы к разным типам изменений и несут разные риски. Команды, которые хорошо справляются с инцидентами в базах данных, — это не те, кто быстрее всех печатает SQL. Это те, кто подумал о восстановлении до того, как миграция была запущена. Они классифицировали свои изменения, протестировали down-миграции и подготовили резервную копию. Когда панель мониторинга покраснела, они не запаниковали. Они выполнили план, который уже составили.