Когда миграция данных пошла не так: стратегии отката, которые действительно работают

Вы только что развернули миграцию базы данных в продакшене. Скрипт выполнялся двенадцать минут, изменил три таблицы, переместил данные между колонками, а затем упал на последнем операторе. Половина изменений применена. Вторая половина — нет. Ваше приложение теперь отдает ошибки, потому что ожидает схему, которая существует не полностью.

Это тот самый момент, когда большинство команд обнаруживают, что откат миграции данных — это совсем не то же самое, что откат развертывания приложения. С кодом приложения вы меняете бинарник или образ контейнера, и старая версия снова работает. С данными вы не можете отменить удаление колонки, просто переразвернув старый код. Колонки больше нет. Данные, которые в ней были, тоже могут исчезнуть.

Стратегия отката для миграции данных должна существовать до начала миграции. Планировать ее после сбоя — уже слишком поздно.

Бекап до миграции, а не после

Самый надежный механизм отката — это полный снапшот базы данных, сделанный непосредственно перед началом миграции. Это не ваш ночной бекап. Это копия на определенный момент времени, которая фиксирует точное состояние данных прямо перед изменением.

Этот бекап должен быть автоматизирован как часть пайплайна. Перед запуском шага миграции пайплайн инициирует дамп базы данных, снапшот или остановку репликации, создающую точку восстановления. Если шаг бекапа завершается ошибкой, пайплайн останавливается. Миграция не запускается. Это исключает человеческую ошибку, когда кто-то забывает сделать ручной бекап перед нажатием кнопки развертывания.

Следующая блок-схема иллюстрирует путь принятия решений после сбоя миграции и помогает выбрать подходящий метод отката в зависимости от ситуации.

flowchart TD A[Pre-Migration Backup Created] --> B[Migration Runs] B --> C{Migration Succeeded?} C -->|Yes| D[Done] C -->|No| E{Choose Rollback Method} E -->|Destructive change or data loss| F[Point-in-Time Recovery] E -->|Reversible schema change| G[Version Rollback - Down Migration] E -->|Full snapshot available| H[Restore Pre-Migration Snapshot] F --> I[Investigate Root Cause] G --> I H --> I I --> J[Fix Migration Script] J --> B

Для облачных баз данных это часто означает создание снапшота тома или клона инстанса. Для самостоятельных баз данных — выполнение команды дампа или использование снапшотов файловой системы. Важно, чтобы бекап был проверяемым. Файл бекапа, который невозможно восстановить, — это не бекап.

Откат версий миграции имеет ограничения

Большинство фреймворков для миграций поддерживают прямые и обратные версии. Flyway называет их migrate и undo. Liquibase — update и rollback. Alembic — upgrade и downgrade. Эти инструменты могут откатывать изменения схемы с помощью скриптов down-миграции.

Загвоздка в том, что down-миграции безопасно работают только для обратимых изменений. Добавление nullable-колонки обратимо: down-миграция удаляет колонку, и данные не теряются. Переименование колонки обратимо, если down-миграция переименовывает ее обратно. Но деструктивные изменения — это совсем другая история. Если вы удалили колонку, down-миграция может воссоздать ее, но данные, которые были в этой колонке, потеряны. Если вы преобразовали данные из одного формата в другой, down-миграция может обратить преобразование только в том случае, если вы где-то сохранили исходные значения.

Откат версий полезен для раннего обнаружения ошибок, например, когда миграция была развернута не в том окружении или изменение схемы ломает запрос. Но это не страховка от потери данных. Полагаться исключительно на down-миграции — распространенная ошибка, которая приводит к потере данных при откате.

Point-in-Time Recovery как страховочная сеть

Самая надежная стратегия отката вообще не зависит от скриптов миграции. Point-in-time recovery (восстановление на момент времени) использует журнал транзакций базы данных для восстановления базы на любой момент до начала миграции.

Вот как это работает. База данных непрерывно пишет журналы транзакций или write-ahead логи, которые записывают каждое изменение. Если у вас есть эти журналы и базовый бекап, вы можете воспроизвести журналы до определенной временной метки. Когда миграция падает в 14:00, вы восстанавливаете базу данных на 13:59, до начала миграции. Все изменения, сделанные миграцией, исчезают, и база данных возвращается в исходное состояние независимо от того, насколько деструктивной была миграция.

Point-in-time recovery требует подготовки. База данных должна быть настроена на непрерывное архивирование журналов транзакций. У команды должны быть инструменты и права для выполнения восстановления на определенное время. И процесс должен регулярно тестироваться. Многие команды обнаруживают, что их настройка point-in-time recovery сломана, только когда она нужна во время инцидента.

Этот подход работает для любых миграций, включая деструктивные. Ему все равно, добавила ли миграция колонки, удалила таблицы или преобразовала миллионы строк. Он просто отматывает время назад.

Тестируйте откат, а не только миграцию

Миграция, которая проходит все тесты в стейджинге, все равно может упасть в продакшене из-за неожиданного объема данных, конфликтов блокировок или граничных случаев в данных. То же самое верно и для отката. Единственный способ узнать, что откат работает, — протестировать его.

В вашем стейджинг-окружении запустите миграцию. Затем попробуйте выполнить откат каждым из ваших методов: down-миграцией, бекапом до миграции и point-in-time recovery. Замерьте, сколько времени занимает каждый метод. Если point-in-time recovery занимает четыре часа, это важная информация, которую нужно знать до инцидента в продакшене.

Если откат не удается или занимает слишком много времени, исправьте процесс до того, как он понадобится. Это тестирование должно быть частью вашего пайплайна. Плановое задание может запускать цикл миграции и отката в стейджинге каждую неделю, чтобы убедиться, что механизмы восстановления все еще работают после изменений инфраструктуры.

После отката — расследование перед повторной попыткой

Когда откат прошел успешно, естественная реакция — исправить скрипт миграции и запустить его снова. Сопротивляйтесь этому импульсу. Сбой мог выявить более глубокую проблему: несоответствие данных, которое миграция не учла, состояние гонки с другим процессом или непонимание схемы.

Сначала расследуйте первопричину. Проверьте логи миграции. Посмотрите на данные, которые вызвали сбой. Проанализируйте, не предполагала ли миграция такой формы данных, которой не существует в продакшене. Только после того, как вы поймете, почему произошел сбой, модифицируйте скрипт и пробуйте снова.

Практический чеклист для отката миграций

  • Автоматизируйте шаг бекапа перед миграцией в пайплайне. Если бекап не удался, пайплайн падает.
  • Пишите down-миграции только для обратимых изменений схемы. Не полагайтесь на них для деструктивных операций.
  • Настройте point-in-time recovery для вашей базы данных и тестируйте его как минимум раз в квартал.
  • Тестируйте откат в стейджинге перед каждой продакшен-миграцией, а не только при первоначальной настройке.
  • Документируйте процедуру отката и расчетное время восстановления для каждого окружения.
  • После отката расследуйте первопричину перед повторной попыткой миграции.

Конкретный вывод

Откат миграции данных — это не скрипт, который вы запускаете. Это система, которую вы строите до начала миграции. Бекап перед миграцией — ваша первая линия обороны. Point-in-time recovery — ваше последнее средство. Down-миграции полезны только в узких случаях, когда изменения обратимы. Тестируйте все это в стейджинге, документируйте процедуру и никогда не предполагайте, что откат сработает, пока вы не докажете это.