Каждый раз, когда мы вносим изменение в схему нашей базы данных, мы также должны вносить изменения в код, который использует эту схему базы данных.
Когда мы добавляем новый столбец в базу данных, нам нужно изменить код, чтобы использовать этот новый столбец.
Когда мы удаляем столбец из базы данных, нам нужно изменить код, чтобы этот столбец больше не использовался.
В этом руководстве мы обсудим, как мы можем координировать изменения кода с изменениями базы данных и развертывать их в нашей производственной среде без простоев. Мы шаг за шагом рассмотрим пример сценария использования и воспользуемся флагами функций, помогающими нам выполнить изменения.
Пример кода
Эта статья сопровождается примером рабочего кода на GitHub.
Проблема: согласование изменений в базе данных с изменениями кода
Если в релизе одновременно изменяются база данных и код, мы удваиваем риск того, что что-то пойдет не так. Мы объединили риск изменения базы данных с риском изменения кода.
Обычно наше приложение запускается на нескольких узлах, и во время нового релиза новый код развертывается на одном узле за раз. Это часто называют «скользящим развертыванием» или «циклическим релизом» с целью обеспечения нулевого времени простоя. Во время развертывания будут узлы, работающие со старым кодом, несовместимым с новой схемой базы данных! Как мы можем с этим справиться?
Что нам делать, если развертывание изменения кода не удалось из-за ошибки? Приходится откатиться к старой версии кода. Но старая версия кода может больше не быть совместима с базой данных, потому что мы уже применили изменение базы данных! Так что нам тоже нужно откатить изменение базы данных! Откат сам по себе несет в себе определенный риск неудачи, потому что откат часто не является хорошо спланированным и хорошо отрепетированным действием. Как мы можем исправить эту ситуацию?
Ответ на эти вопросы - отделить изменения базы данных от изменений кода с помощью флагов функций.
С помощью флагов функций мы можем развертывать изменения базы данных и код в любое время, когда захотим, и активировать их в любое время после развертывания.
В этом руководстве содержится пошаговое руководство по безопасному релизу изменений базы данных и соответствующих изменений кода без простоев с использованием Spring Boot, Flyway и флагов функций, реализованных с помощью платформы маркировки функций, такой как LaunchDarkly.
Пример сценария использования: разделение одного столбца базы данных на два
В качестве примера сценария использования мы собираемся разделить столбец базы данных на два.
Изначально наше приложение выглядит так:
У нас есть CustomerController
REST API для наших клиентов. Он использует CustomerRepository
, который является Spring Data репозиторием, который сопоставляет записи в CUSTOMER
таблице базы данных с объектами типа Customer
. В
таблице CUSTOMER
есть столбцы id
и address
для нашего примера.
Столбец address
содержит как название улицы и номер улицы в одном поле. Представьте, что из-за каких-то новых требований мы должны разделить столбец address
на два столбца: streetNumber
иstreet
.
В итоге мы хотим, чтобы приложение выглядело так:
В этом руководстве мы рассмотрим все изменения, которые нам необходимо внести в базу данных и код, а также способы их максимально безопасного релиза с использованием флагов функций и нескольких развертываний.
Шаг 1. Отделите изменения базы данных от изменений кода
Прежде чем мы даже начнем с изменения кода или схемы базы данных, мы хотим отделить выполнение изменений базы данных от развертывания приложения Spring Boot.
По умолчанию Flyway выполняет миграцию базы данных при запуске приложения. Это очень удобно, но дает мало контроля. Что делать, если изменение базы данных несовместимо со старым кодом? Во время последовательного развертывания могут быть узлы со старыми кодами, которые все еще используют базу данных!
Нам нужен полный контроль над тем, когда мы выполняем изменения схемы нашей базы данных! С небольшой настройкой нашего приложения Spring Boot мы можем добиться этого.
Во-первых, мы отключаем Flyway по умолчанию для выполнения миграции базы данных при запуске:
@Configuration
class FlywayConfiguration {
private final static Logger logger = LoggerFactory.getLogger(FlywayConfiguration.class);
@Bean
FlywayMigrationStrategy flywayStrategy() {
return flyway -> logger.info("Flyway migration on startup is disabled! Call the endpoint /flywayMigrate instead.");
}
}
Вместо того, чтобы выполнять все миграции базы данных, которые еще не были выполнены, теперь он просто напечатает строку в журнале, в которой говорится, что вместо этого мы должны вызвать конечную точку HTTP.
Но мы также должны реализовать эту конечную точку HTTP:
@RestController
class FlywayController {
private final Flyway flyway;
public FlywayController(Flyway flyway) {
this.flyway = flyway;
}
@PostMapping("/flywayMigrate")
String flywayMigrate() {
flyway.migrate();
return "success";
}
}
Всякий раз, когда мы сейчас вызываем /flywayMigrate
через HTTP POST, Flyway запускает все сценарии миграции, которые еще не были выполнены. Обратите внимание, что вы должны защищать эту конечную точку в реальном приложении, чтобы не все могли ее вызвать.
С этим изменением мы можем развернуть новую версию кода без одновременного изменения схемы базы данных. Мы воспользуемся этим на следующем шаге.
Шаг 2. Разверните новый код под флагом функции
Далее мы пишем код, который нам нужен для работы с новой схемой базы данных:
Поскольку мы собираемся изменить структуру таблицы базы данных CUSTOMER
, мы создаем класс, NewCustomer
, который сопоставляется с новыми столбцами таблицы (то есть, streetNumber
и street,
а не просто address
). Мы также создаем новый Spring Data репозиторий NewCustomerRepository
, который привязан к той же таблице, что и класс CustomerRepository
, но использует класс NewCustomer
для сопоставления строк базы данных с Java объектом.
Обратите внимание, что мы развернули новый код, но еще не активировали его. Он пока не может работать, потому что база данных все еще находится в старом состоянии.
Вместо этого мы спрятали его за флажками функций. В классе CustomerController
у нас теперь есть код, который выглядит примерно так:
@PostMapping("/customers/create")
String createCustomer() {
if (featureFlagService.writeToNewCustomerSchema()) {
NewCustomer customer = new NewCustomer("Bob", "Builder", "Build Street", "21");
newCustomerRepository.save(customer);
} else {
OldCustomer customer = new OldCustomer("Bob", "Builder", "21 Build Street");
oldCustomerRepository.save(customer);
}
return "customer created";
}
@GetMapping("/customers/{id}}")
String getCustomer(@PathVariable("id") Long id) {
if (featureFlagService.readFromNewCustomerSchema()) {
Optional<NewCustomer> customer = newCustomerRepository.findById(id);
return customer.get().toString();
} else {
Optional<OldCustomer> customer = oldCustomerRepository.findById(id);
return customer.get().toString();
}
}
С помощью инструмента пометки функций, такого как LaunchDarkly, мы создали два флага функций:
Логический флаг featureFlagService.writeToNewCustomerSchema()
определяет активен ли путь записи в новую схему базы данных. Этот флаг функции в настоящее время все еще отключен, потому что мы еще не обновили схему базы данных.
Логический флаг featureFlagService.readFromNewCustomerSchema()
определяет, активен ли путь чтения из новой схемы базы данных. Этот флаг функции на данный момент также отключен.
С помощью флагов функций мы развернули новый код, даже не касаясь базы данных, что мы и сделаем на следующем шаге.
Шаг 3. Добавьте новые столбцы базы данных
С развертыванием нового кода на предыдущем шаге мы также развернули новый SQL для выполнения Flyway. После успешного развертывания, теперь мы можем вызвать конечную точку /flywayMigrate
, подготовленную в шаге 1. Он будет выполнить SQL скрипт и обновит схему базы данных с новымми полями streetNumber
и street
:
Эти новые столбцы пока будут пустыми. Обратите внимание, что мы пока оставили существующий столбец address
нетронутым. В конечном состоянии мы хотим удалить этот столбец, но сначала нам нужно перенести данные в новые столбцы.
Флаги функций пока отключены, так что и чтение, и запись идут в старый столбец базы данных address
.
Шаг 4: активируйте запись в новые столбцы базы данных
Затем мы активируем флаг функции writeToNewCustomerSchema
, чтобы приложение теперь записывало в новые столбцы базы данных, но по-прежнему читало из старого:
Каждый раз, когда приложение теперь записывает нового клиента в базу данных, оно использует новый код. Обратите внимание, что новый код по-прежнему будет заполнять старый столбец address
в дополнение к новым столбцам streetNumber
и street
для обратной совместимости, поскольку старый код по-прежнему отвечает за чтение из базы данных.
Мы пока не можем переключить новый код на чтение данных из базы данных, потому что для большинства клиентов новые столбцы будут пустыми. Новые столбцы будут медленно заполняться с течением времени, поскольку новый код используется для записи данных в базу данных.
Чтобы заполнить новые столбцы для всех клиентов, нам нужно запустить миграцию данных.
Шаг 5. Перенесите данные в новые столбцы базы данных
Далее мы собираемся запустить миграцию, читает данные всех клиентов в базе данных, у которых поля streetNumber
и street
все еще пустые, читает поле address
, и мигрирует его данные в новые поля:
Эта миграция может быть SQL скриптом, некоторым пользовательским кодом или реальными людьми, которые по очереди просматривают данные клиентов и выполняют миграцию вручную. Выбор наилучшего способа зависит от сценария использования, качества данных и сложности задачи миграции.
Миграция данных с помощью Flyway?
Обратите внимание, что тип миграции, о котором мы говорим в этом разделе, обычно не является задачей Flyway. Flyway предназначен для выполнения сценариев, которые переводят схему базы данных из одного состояния в другое. Миграция данных - совсем другая задача.
Да, Flyway можно использовать для миграции данных. В конце концов, миграция данных вполне может быть просто скриптом SQL. Однако миграция данных может вызвать такие проблемы, как длительные запросы и блокировки таблиц, чего не должно происходить в контексте миграции Flyway, потому что у нас мало контроля над этим.
Шаг 6: активируйте чтение из новых столбцов базы данных
Теперь, когда все данные о клиентах перенесены в новую структуру данных, мы можем активировать флаг функции, чтобы использовать новый код для чтения из базы данных:
Новый код теперь используется для записи и чтения из базы данных. И старый код, и старый столбец базы данных address
больше не используются.
Шаг 7. Удалите старый код и столбец базы данных
Последний шаг - очистка:
Мы можем удалить старый код, который больше не используется. И мы можем запустить еще одну миграцию Flyway, которая удалит старый address
столбец из базы данных.
Мы также должны удалить флаги функций из кода сейчас, потому что мы больше не используем старый код. Если мы не удалим старый код, у нас возникнет технический долг, который затруднит понимание кода следующему разработчику. При масштабном использовании флагов функций в масштабах всей организации платформа маркировки функций, такая как LaunchDarkly, может помочь в этом, поскольку она отслеживает использование флагов функций во всей кодовой базе.
Теперь мы можем также переименовать NewCustomerRepository
в CustomerRepository
и NewCustomer
в Customer
чтобы сделать код чистым и понятным.
Развертывайте с уверенностью
Вышеуказанные 7 шагов будут распределены между несколькими развертываниями приложения. Некоторые из них можно объединить в одно развертывание, но будет как минимум два развертывания: одно для развертывания нового кода и флагов функций, а второе для удаления старого кода и флагов функций.
Флаги функций дают нам большую гибкость и уверенность в изменениях базы данных, как в сценарии использования, который мы обсуждали выше. Флаги функций позволяют нам отделить изменения кода от изменений базы данных. Без флагов функций мы можем активировать новый код только путем развертывания новой версии приложения, что значительно усложняет управление сценариями, требующими обратной совместимости со старой схемой базы данных (и более рискованными!).
Если вы хотите узнать больше о пометке функций, обязательно прочтите обзор LaunchDarkly и Togglz, двух самых популярных инструментах пометки функций в мире JVM.
Примечание переводчика.
Togglz - это расширяемая библиотека Java (лицензия Apache License Version 2.0), а LaunchDarkly - это облачная платформа для управления функциями. В указанном выше обзоре рассмотрены реализации некоторых общих сценариев использования пометки функций для каждого из них, а также плюсы и минусы каждого инструмента.
Вероятно стоит реализовать описанный в статье сценаий с помощью Togglz.