Когда down-миграции базы данных безопасны, а когда становятся опасными
Вы только что развернули миграцию базы данных, которая добавила столбец phone_number в таблицу users. Через несколько часов кто-то замечает, что столбец должен называться phone, чтобы соответствовать остальной кодовой базе. Первая мысль — запустить down-миграцию, удалить столбец и переразвернуть приложение с правильным именем. Просто, правда?
На ранних этапах разработки это работает отлично. В продакшене то же самое действие может стоить вам пользовательских данных, вызвать ошибки в приложении и создать хаос, на распутывание которого уйдут дни.
Down-миграции — это аналог отмены (undo) для базы данных. Если ваша миграция добавила столбец, down-миграция его удаляет. Если миграция создала таблицу, down-миграция её дропает. Концепция звучит прямолинейно, но последствия становятся далеко не простыми, когда в игру вступают реальные пользователи и реальные данные.
Down-миграции безопасны на ранних этапах разработки
Когда ваша команда разрабатывает новую функцию в ветке, схема данных может меняться несколько раз за день. Вы пишете миграцию, тестируете её, понимаете, что подход неверен, и запускаете down-миграцию. Никто не страдает, потому что нет пользователей. Никакой другой код не зависит от этой схемы, так как вы работаете изолированно.
Вот где down-миграции проявляют себя наилучшим образом. Они позволяют быстро экспериментировать, не беспокоясь об очистке. Вы можете пробовать разные типы столбцов, тестировать структуры таблиц и быстро итерировать. Цена ошибки равна нулю, потому что ничего постоянного ещё не создано.
Стейджинг вводит первые реальные риски
Среды стейджинга находятся в серой зоне. Down-миграции всё ещё работают, но уже начинают проявлять свои опасные грани.
Проблема в данных. Стейдж часто содержит данные, похожие на продакшен — либо из анонимизированных бэкапов, либо из реального использования во время тестирования. Если ваша down-миграция дропает столбец, вы теряете все данные, которые были в этом столбце. В стейджинге вы обычно можете перезагрузить данные, но этот процесс занимает время. Таблица с миллионами строк может перестраиваться часами.
Что ещё важнее, стейдж формирует привычки. Если ваша команда привыкла ежедневно запускать down-миграции в стейджинге, эта мышечная память переносится и на продакшен. То же самое действие, которое было безвредным в стейджинге, становится разрушительным в продакшене, и никто не останавливается, чтобы задуматься, потому что «мы всегда так делаем».
Продакшен: где down-миграции становятся опасными
В продакшене простая концепция «отмены» перестаёт работать. Три конкретные проблемы делают down-миграции рискованными в производственных средах.
Следующая диаграмма состояний иллюстрирует, как безопасность down-миграций меняется в зависимости от среды:
Рассмотрим эту миграцию, которая добавила столбец phone_number в таблицу users:
-- Up migration
ALTER TABLE users ADD COLUMN phone_number varchar(20);
-- Down migration
ALTER TABLE users DROP COLUMN phone_number;
Если пользователи уже ввели свои номера телефонов, запуск down-миграции мгновенно уничтожит эти данные. Никакого предупреждения, никакого подтверждения, никакой отмены. Столбец и все его значения исчезнут.
Потеря данных необратима
Когда вы запускаете down-миграцию, которая удаляет столбец, каждое значение в этом столбце исчезает. Для столбцов базы данных нет корзины. Если ваша миграция добавила столбец phone_number и пользователи уже ввели свои номера, эти номера пропадут при удалении столбца.
Вы можете подумать: «Я восстановлюсь из бэкапа». Но бэкапы, сделанные после запуска миграции, уже содержат новый столбец с новыми данными. Восстановление из бэкапа, сделанного до миграции, означает потерю всех изменений, произошедших после запуска миграции. В любом случае данные будут потеряны.
Единственный безопасный подход — восстановиться из бэкапа, сделанного до миграции, а затем воспроизвести все изменения, произошедшие после миграции, исключив саму проблемную миграцию. Этот процесс сложен, трудоёмок и чреват ошибками. У большинства команд нет ни инструментов, ни операционной дисциплины, чтобы выполнить его надёжно.
Код и схема рассинхронизируются
Это самый распространённый сценарий отказа в продакшене при использовании down-миграций. Представьте, что ваша миграция добавила столбец status в таблицу orders со значением по умолчанию pending. Ваш новый код приложения читает этот столбец. Когда вы запускаете down-миграцию, столбец исчезает.
Но ваши экземпляры приложения всё ещё выполняют новый код. Они немедленно начинают выдавать ошибки, потому что столбец, который они ожидают, больше не существует. Даже если вы начнёте откатывать код приложения, откат не происходит мгновенно. У вас несколько экземпляров, каждый со своим циклом развёртывания. Некоторые экземпляры всё ещё могут выполнять новый код, в то время как другие уже откатились. В этом окне ошибки каскадно распространяются по вашей системе.
Фундаментальная проблема в том, что откаты приложения и откаты базы данных невозможно идеально синхронизировать. Всегда будет период, когда код ожидает схему, которая больше не существует, или схема имеет столбец, который старый код не умеет обрабатывать.
Некоторые изменения невозможно отменить
Определённые миграции по своей природе являются деструктивными. Рассмотрим миграцию, которая объединяет first_name и last_name в один столбец full_name. Исходные данные были преобразованы. Запуск down-миграции может воссоздать столбцы first_name и last_name, но данные в них не будут соответствовать тому, что было до миграции. Первоначальное разделение утеряно.
Другой пример: миграция, которая удаляет столбец, всё ещё используемый устаревшими запросами. Как только столбец удалён, данные исчезают. Никакое количество магии down-миграций не вернёт их. Единственный путь восстановления — восстановление из бэкапа, что возвращает все проблемы, упомянутые ранее.
Когда down-миграции допустимы в продакшене
Down-миграции не являются абсолютно запрещёнными в продакшене. Существуют определённые условия, при которых их можно использовать безопасно:
- Миграция только добавляет новые таблицы или столбцы, которые никогда не были заполнены данными.
- Не было развёрнуто кода приложения, который зависит от новой схемы.
- Вы можете проверить, что ни один запущенный процесс, запланированная задача или фоновый рабочий процесс не ссылается на изменённую схему.
Даже в этих случаях самый безопасный подход — рассматривать down-миграцию как новую прямую (forward) миграцию. Напишите миграцию, которая явно отменяет изменение, разверните её и дайте ей пройти через ваш обычный конвейер. Это даст тот же результат, что и down-миграция, но с полной видимостью, тестированием и возможностью отката.
Практический чек-лист перед запуском down-миграции в продакшене
Прежде чем запустить эту down-миграцию, задайте себе эти вопросы:
- Есть ли пользовательские данные в столбцах или таблицах, которые удаляются?
- Есть ли экземпляры приложения, всё ещё выполняющие код, который зависит от этой схемы?
- Есть ли фоновые задачи, запланированные задания или конвейеры данных, которые ссылаются на изменённые объекты?
- Можно ли отменить изменение без потери информации, которая была введена после миграции?
- Есть ли у вас проверенный бэкап, сделанный до запуска миграции?
- Можете ли вы позволить себе время простоя, пока down-миграция выполняется на больших таблицах?
Если вы ответили «да» на любой из первых трёх вопросов, не запускайте down-миграцию. Вместо этого напишите прямую (forward) миграцию.
Более безопасная альтернатива: двигайтесь вперёд, а не назад
Самая надёжная стратегия исправления неудачной миграции базы данных в продакшене — не отменять её, а исправить, двигаясь вперёд. Напишите новую миграцию, которая исправляет проблему. Если имя столбца неверно, добавьте правильный столбец, скопируйте данные и объявите старый устаревшим. Если изменение схемы привело к ошибке, добавьте миграцию, которая приводит схему к правильному состоянию.
Прямые (forward) миграции безопаснее, потому что они сохраняют существующие данные, поддерживают совместимость с работающим кодом и следуют тому же процессу развёртывания, что и любые другие изменения. Они не требуют идеальной синхронизации между откатами приложения и базы данных. Они не создают окон несоответствия, через которые ошибки распространяются по вашей системе.
Down-миграции — это инструмент разработки. В продакшене движение вперёд всегда безопаснее, чем назад.