Привет, Хабр! Меня зовут Кирилл Курдюков, я разработчик YDB. Ранее мы рассматривали интеграцию YDB c Liquibase, теперь поговорим о результатах поддержки инструмента Flyway для управления миграциями схемы данных YDB.

Введение

Flyway — это инструмент для миграций схем баз данных с открытым исходным кодом. Он имеет расширения для различных систем управления базами данных (СУБД), включая YDB. Является таким же широко используемым и популярным инструментом для управления миграциями схем базы данных, как и Liquibase.

Flyway и Liquibase определяют, какие изменения уже были применены к базе данных, а какие еще не были. Для этого они ведут журнал выполненных миграций в самой базе данных.

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

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

Как использовать Flyway вместе с YDB

Чтобы использовать Flyway вместе с YDB в Java / Kotlin приложении или в Gradle / Maven плагинах, требуется установить зависимость расширения Flyway для YDB и YDB JDBC Driver:

Maven:

<!-- Set actual versions -->
<dependency>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-core</artifactId>
    <version>${flyway.core.version}</version>
</dependency>

<dependency>
    <groupId>tech.ydb.jdbc</groupId>
    <artifactId>ydb-jdbc-driver</artifactId>
    <version>${ydb.jdbc.version}</version>
</dependency>

<dependency>
    <groupId>tech.ydb.dialects</groupId>
    <artifactId>flyway-ydb-dialect</artifactId>
    <version>${flyway.ydb.dialect.version}</version>
</dependency>

Gradle:

dependencies {
    // Set actual versions
    implementation "org.flywaydb:flyway-core:$flywayCoreVersion"
    implementation "tech.ydb.dialects:flyway-ydb-dialect:$flywayYdbDialecVersion"
    implementation "tech.ydb.jdbc:ydb-jdbc-driver:$ydbJdbcVersion"
}

Для работы с YDB через Flyway CLI требуется установить утилиту flyway любым из рекомендованных способов.

Затем Flyway нужно расширить диалектом YDB и JDBC-драйвером:

# install flyway
# cd $(which flyway) // prepare this command for your environment

cd libexec
# set actual versions .jar files

cd drivers && curl -L -o ydb-jdbc-driver-shaded-2.1.0.jar https://repo.maven.apache.org/maven2/tech/ydb/jdbc/ydb-jdbc-driver-shaded/2.1.0/ydb-jdbc-driver-shaded-2.1.0.jar

cd ../lib && curl -L -o flyway-ydb-dialect.jar https://repo.maven.apache.org/maven2/tech/ydb/dialects/flyway-ydb-dialect/1.0.0-RC0/flyway-ydb-dialect-1.0.0-RC0.jar

Управление миграциями с помощью Flyway

baseline

Команда baseline инициализирует Flyway в существующей базе данных, исключая все миграции вплоть до baselineVersion включительно.

Предположим, что мы имеем существующий проект с текущей схемой базы данных:

Запишем наши существующие миграции следующим образом (таблицы взяты из примера):

db/migration:
  V1__create_series.sql
  V2__create_seasons.sql
  V3__create_episodes.sql

Установим baselineVersion = 3, затем выполним следующую команду:

flyway -url=jdbc:ydb:grpc://localhost:2136/local -locations=db/migration -baselineVersion=3 baseline

Результатом исполнения будет созданная таблица flyway_schema_history с baseline записью:

Создана baseline запись
Создана baseline запись

migrate

Команда migrate обновляет схему базы данных до последней версии. Если таблица истории схемы не была создана, то Flyway создаст ее автоматически.

Добавим к предыдущему примеру миграцию загрузки данных:

db/migration:
  V1__create_series.sql
  V2__create_seasons.sql
  V3__create_episodes.sql
  V4__load_data.sql

Применим последнюю миграцию следующей командой:

flyway -url=jdbc:ydb:grpc://localhost:2136/local -locations=db/migration migrate

Результатом исполнения будет загрузка данных в таблицы series, seasons и episodes:

Результат загрузки данных
Результат загрузки данных

Обновим схему путем добавления вторичного индекса:

db/migration:
  V1__create_series.sql
  V2__create_seasons.sql
  V3__create_episodes.sql
  V4__load_data.sql
  V5__create_series_title_index.sql

Содержимое файла V5__create_series_title_index.sql:

ALTER TABLE `series` ADD INDEX `title_index` GLOBAL ON (`title`);

Применим последнюю миграцию следующей командой:

flyway -url=jdbc:ydb:grpc://localhost:2136/local -locations=db/migration migrate

Результатом будет появление вторичного индекса у таблицы series:

Cоздан индекс title_index у таблицы series
Cоздан индекс title_index у таблицы series

info

Команда info печатает подробные сведения и информацию о состоянии всех миграций.

Добавим еще одну миграцию, которая переименовывает раннее добавленный вторичный индекс:

db/migration:
  V1__create_series.sql
  V2__create_seasons.sql
  V3__create_episodes.sql
  V4__load_data.sql
  V5__create_series_title_index.sql
  V6__rename_series_title_index.sql

Содержимое файла V6__rename_series_title_index.sql:

ALTER TABLE `series` RENAME INDEX `title_index` TO `title_index_new`;

После исполнения следующей команды:

flyway -url=jdbc:ydb:grpc://localhost:2136/local -locations=db/migration info

Результатом будет подробная информация о состоянии миграций:

+-----------+---------+---------------------------+----------+---------------------+--------------------+----------+
| Category  | Version | Description               | Type     | Installed On        | State              | Undoable |
+-----------+---------+---------------------------+----------+---------------------+--------------------+----------+
| Versioned | 1       | create series             | SQL      |                     | Below Baseline     | No       |
| Versioned | 2       | create seasons            | SQL      |                     | Below Baseline     | No       |
| Versioned | 3       | create episodes           | SQL      |                     | Ignored (Baseline) | No       |
|           | 3       | << Flyway Baseline >>     | BASELINE | 2024-04-16 12:09:27 | Baseline           | No       |
| Versioned | 4       | load data                 | SQL      | 2024-04-16 12:35:12 | Success            | No       |
| Versioned | 5       | create series title index | SQL      | 2024-04-16 12:59:20 | Success            | No       |
| Versioned | 6       | rename series title index | SQL      |                     | Pending            | No       |
+-----------+---------+---------------------------+----------+---------------------+--------------------+----------+

validate

Команда validate проверяет соответствие примененных миграций к миграциям, которые находятся в файловой системе пользователя.

После применения к текущим миграциям следующей команды:

flyway -url=jdbc:ydb:grpc://localhost:2136/local -locations=db/migration validate

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

ERROR: Validate failed: Migrations have failed validation
Detected resolved migration not applied to database: 6.
To fix this error, either run migrate, or set -ignoreMigrationPatterns='*:pending'.

Давайте ее применим, выполнив flyway .. migrate. Теперь валидация проходит успешно, вторичный индекс переименован.

Далее изменим файл уже ранее примененной миграции V4__load_date.sql, удалив комментарии в SQL-скрипте.

После исполнения команды валидации, получим закономерную ошибку о том, что checksum различается в измененной миграции:

ERROR: Validate failed: Migrations have failed validation
Migration checksum mismatch for migration version 4
-> Applied to database : 591649768
-> Resolved locally    : 1923849782

repair

Команда repair пытается устранить выявленные ошибки и расхождения с таблицей историей схемы базы данных.

Устраним проблему с разными checksum, выполнив следующую команду:

flyway -url=jdbc:ydb:grpc://localhost:2136/local -locations=db/migration repair

Результатом будет обновление колонки checksum в таблице flyway_schema_history у записи, отвечающей за миграцию V4__load_data.sql:

Обновлено поле checksum
Обновлено поле checksum

После восстановления таблицы лога валидация проходит успешно.

Также с помощью команды repair можно удалить неудавшийся DDL-скрипт.

clean

Команда clean удаляет все таблицы в схеме базы данных.

Так как в отличие от других СУБД YDB не имеет такой сущности, как schema, команда clean удалит все таблицы в вашей базе данных.

Удалим все таблицы в нашей базе данных следующей командой:

flyway -url=jdbc:ydb:grpc://localhost:2136/local -locations=db/migration -cleanDisabled=false clean

Результатом будет пустая база данных:

Пустая база данных
Пустая база данных

Распределенные блокировка в Flyway

При определении новых миграций, которые еще не были применены в базе данных, Flyway берет распределенную блокировку перед их применением.

Здесь нет общепринятого решения, у каждой СУБД свое решение.

В Oracle и Derby:

LOCK TABLE flyway_history_schema IN EXCLUSIVE MODE

В PostgreSQL:

SELECT pg_try_advisory_xact_lock("flyway1")

Для YDB взятие блокировки похоже на подход CockroachDB, Google Spanner или Apache Ignite.

В таблицу истории миграций flyway_schema_history клиент пытается вставить фиктивную запись c ID = -100:

INSERT INTO flyway_schema_history(installed_rank, version, description) VALUES (-100, $tableLockId, 'flyway-lock')

И кто успел вставить такую запись, тот и лидер.

Отпускается такая блокировка следующей SQL-командой:

DELETE FROM flyway_schema_history FROM installed_rank = -100

Важным отличием от CockroachDB и Google Spanner является то, что мы не проставляем тайм-аут истечения блокировки, а приостанавливаем процесс для расследования DBA, как сделано в Apache Ignite. Так как мы не знаем, в какой момент отказал лидер и в каком состоянии наша база данных.

А еще ScheduledThreadPool не закрывается, он нужен для процесса продления такой блокировки.

Поддержка и контакты

Если вы столкнулись с какой-то проблемой или у вас есть идея по улучшению диалекта YDB, то можно открыть issue в репозитории ydb-java-dialects с тегом flyway.

Либо приходите обсудить её в публичный Telegram чат YDB.

Документация по Flyway находится по ссылке.

Комментарии (1)


  1. KirillKurdyukov Автор
    17.05.2024 10:43
    +2

    Распределенные блокировки в Liquibase

    Liquibase создает таблицу DATABASECHANGELOGLOCK, в которой содержится одна запись.

    | ID | LOCKED | LOCKEDBY | LOCKGRANTED |
    |:---|:-------|:---------|:------------|
    | 1  | false  | null     | null        |

    Блокировка для большинства СУБД происходит следующим образом:

    -- acquire lock
    SELECT LOCKED FROM DATABASECHANGELOGLOCK WHERE ID = 1;
    
    -- Если false, делаем запрос на взятие блокировки
    
    UPDATE DATABASECHANGELOGLOCK SET LOCKED = true .. WHERE ID = 1;
    
    -- Если UPDATE вернул 0, то блокировку взять не удалось, повторим взятие блокировки через 1 секунду.
    
    -- run migrations..
    
    -- release lock
    UPDATE DATABASECHANGELOGLOCK SET LOCKED = false WHERE ID = 1;

    В текущем виде этот подход не позволит получить корректно распределенную блокировку для YDB, так как на данный момент UPDATE не возвращает количество обновленных строк. Этот способ становится рабочим, если взятие блокировки делать в транзакции уровня SERIALIZABLE

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

    -- acquire lock
    SELECT LOCKED FROM DATABASECHANGELOGLOCK WHERE ID = 1;
    
    -- Если false, делаем запрос на взятие блокировки
    
    UPDATE DATABASECHANGELOGLOCK SET LOCKED = true .. WHERE ID = 1;
    COMMIT;
    
    -- Transaction lock invalidated - означет, что блокировку не удалось взять, кто - то нас обогнал

    Клиенты, которые не получили блокировку пытаются получить ее снова с интервалом 1 секунда.