АрхитектураПроектирование системАрхитектор распределённых систем

Во время поэтапного релиза нужно переименовать поле в большой таблице без остановки сервиса. Как провести и...

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

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Используйте стратегию expand-and-contract: сначала расширьте схему, затем временно обеспечьте совместимость старой и новой версий приложения, перенесите данные, переключите чтение и только после завершения перехода удалите старое поле. Нельзя сначала переименовывать или удалять используемую колонку: старая версия приложения немедленно начнёт получать ошибки.

Исторический контекст

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

Стратегия expand-and-contract появилась как практический способ поддерживать rolling deployment, когда разные версии приложения некоторое время работают одновременно. Она разделяет несовместимое изменение на несколько совместимых этапов.

Постановка проблемы

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

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

Подробное решение

Типичная последовательность выглядит так:

  1. Расширение схемы. Добавьте новую колонку, индекс или таблицу, не ломая старый контракт. Обычно новая колонка временно допускает NULL.
  2. Совместимый релиз приложения. Новая версия продолжает читать старое поле, но при записи обновляет оба поля. Старый код по-прежнему работает только со старым полем.
  3. Перенос существующих данных. Выполните backfill небольшими порциями, ограничивая размер транзакций и контролируя нагрузку. Операция должна быть возобновляемой.
  4. Проверка согласованности. Сравните значения старого и нового полей, найдите пропуски и расхождения. Важно учитывать записи, изменённые во время backfill.
  5. Переключение чтения. После проверки новая версия начинает читать новое поле. На этом этапе старый код всё ещё должен оставаться работоспособным.
  6. Завершение миграции. После истечения периода, когда старая версия гарантированно выведена из эксплуатации, прекратите запись в старое поле, удалите временную совместимость и только затем удалите старую колонку.

Минимальная часть миграции схемы может выглядеть так:

ALTER TABLE users ADD COLUMN display_name_new VARCHAR(200); -- Backfill выполняется порциями вне одной большой транзакции. UPDATE users SET display_name_new = display_name WHERE display_name_new IS NULL; -- После переключения приложения и проверки: ALTER TABLE users DROP COLUMN display_name;

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

Backfill нельзя безоговорочно выполнять одним большим запросом. Он может занять длительную блокировку, создать всплеск нагрузки на журнал транзакций и конкурировать с рабочими запросами. Безопаснее использовать порции, ограничивать скорость, отслеживать ошибки и повторять обработку идемпотентно.

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

Ситуация из практики

Допустим, в таблице пользователей поле name нужно разделить на first_name и last_name. Вариант с немедленным удалением name прост, но несовместим со старой версией приложения и требует остановки или синхронного обновления всех потребителей.

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

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

Что кандидаты часто упускают

  1. Как обеспечить корректность данных, если запись произошла во время backfill?

    Backfill и текущая запись конкурируют за одни и те же строки. Если сначала скопировать старое значение, а затем сразу изменить его только в старом поле, новое поле станет устаревшим.

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

  2. Почему новую колонку не всегда следует сразу сделать обязательной?

    Старые строки ещё могут не содержать значения, а старая версия приложения не умеет его записывать. Немедленное ограничение NOT NULL может сделать backfill невозможным или привести к ошибкам текущих операций.

    Обычно сначала добавляют колонку с допускаемым отсутствием значения, заполняют существующие записи и переводят все записи на новый путь записи. Ограничение добавляют отдельным этапом после проверки полноты данных. При этом нужно учитывать способ добавления ограничения конкретной СУБД: оно может потребовать проверки всей таблицы или блокировки.

  3. Когда стратегия expand-and-contract недостаточна?

    Она не устраняет физическую стоимость тяжёлой миграции. Если изменение требует длительной перестройки таблицы, большого дополнительного объёма диска или несовместимо с возможностями конкретной СУБД, даже поэтапный процесс может создать неприемлемую нагрузку.

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