Мысли как обеспечить обратную совместимость базы данных и zero-downtime deployment

При обновлении версии приложения, работающего 24x7, важный показатель – нулевое время простоя (zero-downtime). Чтобы его достичь, разворачивание и «прогрев» новой версии выполняется параллельно с продолжающейся работой старой версии. Этот подход называется blue green deployment. Старая blue-версия приложения продолжает работать, пока новая green-версия не будет готова и не начнет полноценно работать. Только тогда blue-версия выводится из работы. Если даже что-то случится с green-версией в процессе подготовки, прогрева и начала работы, то можно просто выключить ее. Работоспособность системы будет по-прежнему обеспечивать blue-версия. При переходе на новую версию как правило выполняются изменения в базе данных приложения. Для подхода blue green deployment важно, чтобы эти изменения были обратно совместимыми, так как в случае проблем с green-версией, blue-версия должна продолжать работать с измененной базой данных.

А как быть если все же надо внести в структуру базы данных обратно не совместимые изменения? Как в этом случае обеспечить blue green и zero-downtime deployment?

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

Какие обратно несовместимые изменения в схеме базы данных могут произойти:

  • Добавление нового not null поля таблицы
  • Удаление поля таблицы
  • Переименование типа поля таблицы
  • Изменение типа поля таблицы

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

1. Добавление нового NOT NULL поля таблицы

Если новое поле должно быть NOT NULL, то его нельзя сразу добавить в таблицу (если не задано default value), или если новое поле зависит функционально от других уже хранящихся в базе данных. Даже если таблица не содержит записей, добавление нового NOT NULL поля может привезти к отказу. Так как продолжает работать исходный вариант сервиса, который ничего не знает про новое поле и может попытаться добавить запись, не указав для него значения.

Возможный сценарий обновления в этом случае такой:

  • Шаг 0: Работает исходный вариант 0 сервиса
  • Шаг 1: Добавляем новое nullable поле. Стартуем вариант 1 сервиса, который пишет в новое поле
  • Шаг 2: Останавливаем исходный вариант 0 сервиса
  • Шаг 3: Заносим в новое поле для имеющихся записей нужное значение. Работает вариант 1 приложения
  • Шаг 4: Делаем новое поле NOT NULL. Работает вариант 1 приложения

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

2. Удаление поля таблицы

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

Возможный сценарий обновления в этом случае такой:

  • Шаг 0: Работает исходный вариант 0 сервиса
  • Шаг 1: Делаем поле nullable. Стартуем вариант 1 приложения, который не использует поле (не пишет в поле и не читает из него)
  • Шаг 2: Останавливаем исходный вариант приложения. Работает вариант 1 приложения
  • Шаг 3: Удаляем поле. Работает вариант 1 приложения

3. Переименование типа поля таблицы

Сценарий обновления:

  • Шаг 0: Работает исходный вариант 0 сервиса
  • Шаг 1: Добавляем поле nullable с новым именем. Стартуем вариант 1 приложения, который пишет и в поле со старым именем, и в поле с новым, читает из поля с новым именем, но если в нем NULL, то читаем из поля со старым именем
  • Шаг 2: Останавливаем исходный вариант приложения
  • Шаг 3: Копируем данные из поля со старым именем в поле с новым. Стартуем вариант 2 приложения, который работает только с полем с новым именем (пишет и читает)
  • Шаг 4: Останавливаем вариант 1 приложения
  • Шаг 5: Делаем поле с новым именем NOT NULL. Удаляем поле со старым именем. Работает вариант 2 приложения

4. Изменение типа поля таблицы

Последовательность действий при изменении типа поля похож на изменение имени поля, но имеет свои особенности.

Возможный сценарий обновления:

  • Шаг 0: Работает исходный вариант 0 сервиса
  • Шаг 1: Добавляем поле nullable с новым типом и именем. Стартуем вариант 1 приложения, который пишет и в поле со старым типом (именем), и в поле с новым типом (именем), читаем из поля с новым типом (именем), но если в нем NULL, то читаем из поля со старым типом (именем)
  • Шаг 2: Останавливаем исходный вариант приложения
  • Шаг 3: Копируем (конвертируем) данные из поля со старым типом (именем) в поле с новым типом (именем). Поле со старым типом (именем) делаем nullable. Стартуем вариант 2 приложения, который работает только с полем с новым типом (именем) (пишет и читает)
  • Шаг 4: Останавливаем вариант 1 приложения
  • Шаг 5: Удаляем поле со старым типом (именем). Работает вариант 2 приложения

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

Реализация описанных выше сценариев представляет определенные трудности.

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

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

55
Начать дискуссию