Представьте себе Oracle DBA. Ему уже за тридцать, он слегка полноват, носит жилетку, на шее у него висит секретный токен доступа ко всем базам, а в резюме полстраницы пройденных им сертификаций. Суббота. День большого релиза. Кульминация. Время накатывать изменения на базу данных. Он набирает sqlplus, нажимает ENTER и по черному экрану куда-то вверх, в пустоту, устремляются километры SQL команд. Совсем как в звездных войнах. Спустя пять минут все готово. Через час релиз завершен. Работа сделана, день удался. Теперь можно и по паре пива.

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

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

Что такое инструмент миграции баз данных?


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

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

Простейший пример миграции:
-- 20180618152059: create sequence for some_table
CREATE SEQUENCE some_table_seq;
--//@UNDO
DROP SEQUENCE some_table_seq;

Такой подход дает множество преимуществ по сравнению с организацией изменений в общем SQL файле. Одно только отсутствие merge конфликтов чего стоит.

Тем удивительнее, что сам подход получил популярность сравнительно недавно. Кажется, что общую известность подходу принес фреймворк Ruby on Rails, в котором инструмент миграции был встроен изначально, это был конец 2005 года. Немногим ранее о подходе писал Martin Fowler, 2003. Вероятно, все дело в том, что разработка начала активно адаптировать использование системы контроля версий только в начале этого века. Еще в 2000 году первый пункт теста Joel Spolsky звучал “Do you use source control?” — это наводит на мысль о том, что системами контроля версий тогда пользовались далеко не все. Но мы отвлеклись.

Восемь лет с MyBatis Migrations


Мы в Wrike начали использовать подход изменения баз данных через миграции в 2010 году, 29 марта, в половину первого. С тех пор мы реализовали 1440 миграций, содержащий 6436 прямых изменений и 5015 обратных. В целом, у нас накопился некоторый опыт использования инструмента MyBatis Migrations в связке с PostgreSQL.

Если коротко, мы ни разу не пожалели. Случись такое, что вы еще не используете Migrations или нечто подобное, самое время начать. Да, Pentium 4 тоже устарел.

Но о достоинствах чего-либо говорить скучно, давайте сразу к трудностям.

Специфика работы с PostgreSQL


С написанием миграций для Postgres нет никаких сложностей, пожалуй, кроме двух:
  • Нельзя создавать индексы,
  • Нельзя добавлять NOT NULL колонки.

Нет, на самом деле можно, просто не совсем очевидным образом. При создании индекса нужно всегда указывать CREATE INDEX CONCURRENTLY, в противном случае вы сломаете production, поскольку Postgres заблокирует таблицу на время создания индекса, а это может быть достаточно долго. Конечно, разработчики через раз об этом забывают, приходится всегда иметь эту тонкость в виду. Здесь можно было бы написать тест. Но это лишь небольшое неудобство.

Создание NOT NULL колонки хитрее, тут нужно делать изменение в четыре приема:
  1. Создать NULL колонку (в Postgres это бесплатно).
  2. Задать колонке DEFAULT значение.
  3. В цикле, порциями обновить NULL значения в DEFAULT.
  4. Установить SET NOT NULL.

Самый большой подвох тут в третьем пункте. NULL значения нужно обновлять порциями, поскольку UPDATE some_table SET some_column=’’ WHERE some_column IS NULL; заблокирует таблицу, как и в случае с индексом, с теми же последствиями. А Migrations умеет выполнять только SQL команды, так что такие скрипты приходится накатывать в production руками. Удовольствие ниже среднего. Вот если бы в Migrations можно было написать цикл, проблемы бы не возникло. Возможно, это реализуемо через hooks.

Создание UNIQUE индекса и смена PRIMARY KEY также требуют определенной сноровки, но эти операции делаются сравнительно редко, чтобы на них останавливаться.

Специфика работы с кластером


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

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

Жонглирование ролями


Не секрет, что роли как сущности живут не на уровне отдельной базы данных, но на уровне всего сервера баз данных, по крайней мере, в Postgres. При этом в миграции может потребоваться указать REVOKE INSERT ON some_table FROM some_role; Ожидать, что роли будут заранее сконфигурированы в production еще можно, но для dev или staging это уже затруднительно. При этом в разработке, конечно, все базы существуют на одном локальном сервере, так что просто написать в миграции CREATE ROLE нельзя, а IF NOT EXISTS не поддерживается. Все решается просто:
DO $$ BEGIN
    IF NOT EXISTS (SELECT * FROM pg_roles WHERE rolname = 'some_role') THEN
        CREATE ROLE "some_role" NOLOGIN;
    END IF;
END; $$;

Смотрите! Я их ловлю и подбрасываю, ловлю и подбрасываю, это же так просто.

Немного реальности разработки


Разработчики делают ошибки, и даже в SQL миграциях, такое случается. Обычно ошибки удается заметить на ревью, но бывает и необычно. Если говорить о прямых изменениях, то там косяки до production все-таки не доезжают – слишком много этапов проверки. А вот с обратными изменениями могут возникнуть казусы. Чтобы избежать ошибок в UNDO миграции, при тестировании миграции нужно выполнять не просто ./migrate up, но ./migrate up, затем ./migrate down, затем опять ./migrate up. В этом нет ничего сложного, нужно только добиться, чтобы сорок разработчиков всегда так делали. По-хорошему, утилита могла бы выполнять такое комбо для окружения разработчика автоматически.

Тестовые окружения


Если тестовое окружение короткоживущее: скажем, вы создаете контейнер, инициализируете базу данных и запускаете интеграцонные тесты, проблем вроде быть не должно. Вызываем ./migrate bootstrap, затем ./migrate up, и все готово. Вот только, когда количество миграций переваливает за тысячу, этот процесс может затянуться. Обидно, когда база данных инициализируется дольше, чем выполняются тесты. Приходится изворачиваться.

С долгоживущими окружениями все еще сложнее. QA, знаете ли, не любят, приходя на работу, видеть безукоризненно чистую базу данных. Я не знаю, почему так, но факт есть факт. Так что состояние баз, используемых в ручном тестировании, приходится поддерживать в целостности. А это не всегда просто.

Тонкость в том, что если миграция применена к базе данных, в нее записывается идентификатор миграции. А если код миграции позже был изменен, базы это уже не коснется. Если изменения некритичные, код может успешно доехать до production. Рассинхрон. Безусловно, это безобразие. Первый принцип работы с миграциями — никогда не менять написанные миграции, а всегда создавать новые. Но иногда так хочется схалтурить — я вот тут чуть-чуть поменяю, ничего же не сломается, ведь правда. Конечно! Валяйте!

Если бы миграции подписывались после ревью, можно было бы запретить применять черновики к staging. А еще можно было бы сохранять в changelog не только идентификатор миграции, но и checksum — тоже полезно.

Верните как было


Особенно коварный поворот случается, когда задачу отменяют: делали-делали и передумали. Вполне нормальная ситуация. Раз код больше не нужен, ветку следует удалить. А там же была миграция… а она же уже в staging… а, … опа. Хороший повод проверить, умеете ли вы восстановить бекап репозитория. Хотя вспомнить, что там было, пожалуй, проще.

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

Сделайте UNDO еще раз


Секция UNDO, безусловно, нужна. Но зачем ее писать? Конечно, бывают и заковыристые случаи, но большинство изменений — это CREATE TABLE или ADD COLUMN или CREATE INDEX. Для них утилита могла бы генерировать обратные операции автоматически, прямо по SQL коду. Конечно, тут есть специфика. CREATE TABLE ${name} — это же такая особенная команда, вдруг нестандартная. Да и чтобы сгенерировать DROP TABLE ${name}, нужно уметь распарсить выражение аж до третьего слова. Хотя, в целом, это вполне реализуемая техническая задача. Могло бы быть из коробки.

Заключение


Конечно, я придираюсь. MyBatis Migrations задумывалась как простая и универсальная утилита, минимально завязанная на специфику баз данных. И она себя более чем оправдывает. Но кажется, что несколько небольших улучшений сделали бы ее гораздо лучше, особенно при использовании на длинной дистанции.

Дмитрий Мамонов / Wrike

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


  1. win32nipuh
    19.06.2018 15:00
    +1

    В разделе
    «Специфика работы с PostgreSQL»
    Неплохо бы указать, что в PostgreSQL 11 проблема добавления поля с неким дефолт значением и создания индексов решаются хорошо.


  1. win32nipuh
    19.06.2018 15:02
    +1

    По поводу «Не секрет, что роли как сущности живут не на уровне отдельной базы данных, но на уровне всего сервера баз данных, по крайней мере, в Postgres.» — в PostgreSQL да, но в общем так не стоит заявлять. Это не секрет, что в SQL Server роли живут в базе (Database roles)


  1. PavelGhost
    19.06.2018 20:49

    Я так полагаю, что «миграцией» Вы назвали database deployment patch.

    Тогда фраза

    Случись такое, что вы еще не используете Migrations или нечто подобное, самое время начать. Да, Pentium 4 тоже устарел.
    выглядит немного напыщенной, т.к. подобный решений существует не один десяток (DbMaestro, FlyWay) + несметное количество самописных тулов.


    1. dm_wrike Автор
      19.06.2018 20:58

      Спасибо за комментарий :)
      Да, есть некоторое разнообразие в терминологии, вероятно database deployment patch это один из вариантов. M.Fowler использует термины «database changes are migrations», тут единого
      стандарта нет.

      Migrations или нечто подобное… выглядит немного напыщенной, т.к. подобный решений существует не один десяток… вы правы, но я и не утверждаю что Migrations это
      единственное решение в своем классе, как видите.


      1. PavelGhost
        19.06.2018 21:15

        Я просто не совсем точно выразился. Я ничего не имею против каких-то самописных решений, но, согласитесь, «изобретение велосипеда» не всегда что-то полезное.
        Вот если в Вашем решении есть какие-то, действительно, интересные решения — например, как Вы обеспечиваете миграцию данных при применении патчей и, особенно, при их откате, то вот это было бы очень интересно почитать и сравнить со свои опытом.
        И, к сожалению, как раз об этом ничего нет. Т.е. как бы сказано «А», но не сказано «Б».


        1. dm_wrike Автор
          19.06.2018 23:29

          Вообще то я не говорил что у нас есть собственное решение, Migrations штука вполне стандартная и общедоступная. Тем не менее, вы угадали. Мы отказались от Migrations в сторону своего решения.

          Я планирую написать вторую статью в продолжение, если к этой будет интерес.
          Опять же, в целом вы правильно мыслите, кроме стандартных команд Migrations (init, up, down, status) мы добавили: sign, snapshot, copy и diff. Кроме того, мы частично автоматизировали UNDO, так что проблем с несимметричностью изменений должно стать меньше. На систематизацию простых миграций данных тоже есть планы.


  1. PavelGhost
    19.06.2018 20:59

    У всех подобных решений (change-driven deployment), на мой взгляд, есть досточно много недостатков, чтобы их считать чем-то современным и прогрессивным.
    Просто для контраста представьте, что Вы заставляете разработчиков приложений не комитить конечный код java-классов, javascript и прочего, а писать патчи (вставки/изменения/удаления строк) к тому, чтобы из какого-то умозрительного состояния файлов на сервере приложений получить тот самый конечный вид файлов приложения, который нужен. Вас бы посчитали сумашедшим или мазохистом, ведь так?
    Но почему-то такой подход для изменения объектов в базе данных считается вполне приемлемым.

    У нас вот тоже «исторически» внедрён подобный подход к deployment изменений в базах данных (а это десятки схем и несколько разных по сути видов баз данных — editorial, staging, customer-facing, ...) — простенькие Python скрипты — фактически просто подключение к базе и выполнение скриптов + обвязка. Этот тул и для Oracle и для PostgreSQL/Greenplum работает. Патчи в Git организованы по определённым правилам — сгруппированы по компонентам и по типам объектов, есть нумерация и шаблоны имён. Есть и checkpoint таблицы для учёта того, какие патчи установлены, какие нет и т.п.
    Но! Попробуйте с таким подходом создать схему с нуля, склонировать её куда-то или попытаться сравнить реальное состояние схемы с тем, что у вас вроде как есть в Git с историей (но в виде патчей, а не конечных состояний). Попробуйте сделать тот же rollback в любой предыдущий коммит или diff с любым предыдущим состоянием объектов в базе.
    Кроме того, к сожалению, наивно надеяться на то, что патчи разработчики будут делать правильные — idempotent — и повторное выполнение не приведёт к ошибке (простейший CREATE TABLE без IF EXISTS, например). И т.д. и т.п.

    И теперь сравните это со state-driven deployment подходом — вы коммитите ровно то состояние объектов, которое нужно иметь в базе, а как его достигнуть (CREATE TABLE или ALTER TABLE) — дело движка deployment + hooks. Это же почти как и в application code deployment.
    Например, так реализован db deployment в RedGate (Source Control for Oracle) и DaticalDB (основанный на open-source liquibase).
    По мне — это и есть то, к чему стоит стремиться в database deployment.
    Конечно, не все виды баз, не все типы объектов поддерживаются тем же RedGate и DaticalDB, но принцип очень правильный, с моей точки зрения. Они стоят денег, причём, честно не всегда адекватно (DaticalDB).

    И конечно же основная проблема в любом подходе к database deployment — это сохранение и миграция данных. Тут одними техническими решениями не обойтись — нужна чёткая и иногда даже жёсткая дисциплина в подготовке db deployment.


    1. dm_wrike Автор
      19.06.2018 23:04

      У меня статья короче чем ваш комментарий, ну, поехали.

      У всех подобных решений (change-driven deployment)...

      Люди приходят к change-driven deployment не на пустом месте, а потому что есть
      состояние которое нельзя потерять. В случае с кодом, который вы конечно привели
      для контраста, старое состояние кода не имеет ценности и всегда его можно заменить
      полным новым состоянием кода. Исключения: erlang и хранимые процедуры.

      У нас вот тоже «исторически» внедрён подобный подход

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

      Но! Попробуйте с таким подходом создать схему с нуля

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

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

      Увы, вы правы. Средненькое решение тут — тестировать, хорошее — генерировать UNDO автоматически.

      (Вторая половина ответа в следующем комментарии).


      1. PavelGhost
        20.06.2018 02:46

        Люди приходят к change-driven deployment не на пустом месте, а потому что есть
        состояние которое нельзя потерять. В случае с кодом, который вы конечно привели
        для контраста, старое состояние кода не имеет ценности и всегда его можно заменить
        полным новым состоянием кода. Исключения: erlang и хранимые процедуры.


        Конечно — часто делается это, чтобы хоть что-то было для deployment.
        Не всегда хранимые процедуры (и даже представления) можно заменить — в PostgreSQL это как раз сложнее, чем в Oracle и всегда есть ньюансы.


      1. PavelGhost
        20.06.2018 03:22

        У меня статья короче чем ваш комментарий

        Каюсь — грешен этим.

        Я планирую написать вторую статью в продолжение, если к этой будет интерес.
        Опять же, в целом вы правильно мыслите, кроме стандартных команд Migrations (init, up, down, status) мы добавили: sign, snapshot, copy и diff. Кроме того, мы частично автоматизировали UNDO, так что проблем с несимметричностью изменений должно стать меньше. На систематизацию простых миграций данных тоже есть планы.

        Будет любопытно почитать.
        Тема для меня интересная и отчасти болезненная — через много пришлось пройти на собственном опыте.
        Зато, в результате, сейчас есть опыт автоматизации db deployment в Oracle/Greenplum пары десятков компонентов (~50 схем) с помощью скриптов на Python + GitHub + Jenkins + Docker — всё внутри AWS. DBA нынче не просто DBA ;-)


        1. MikeSam
          22.06.2018 06:36

          Зато, в результате, сейчас есть опыт автоматизации db deployment в Oracle/Greenplum пары десятков компонентов (~50 схем) с помощью скриптов на Python + GitHub + Jenkins + Docker — всё внутри AWS. DBA нынче не просто DBA ;-)

          Вы можете показать свое решение? Было бы интересно посмотреть, если конечно это не закрытая информация.


          1. PavelGhost
            22.06.2018 13:31

            Вы можете показать свое решение? Было бы интересно посмотреть, если конечно это не закрытая информация.


            Весь код я показать не могу — это всё лежит в private GitHub repo, но схему взаимодействия и идею попробую обрисовать (это может занять какое-то время)


    1. dm_wrike Автор
      19.06.2018 23:20

      И теперь сравните это со state-driven deployment подходом

      Подход интересный, но организационно сложнее.

      (ссылка на Red Gate)
      1. А как будет собираться состояние к релизу — из тех патчей/ченжей/миграций?
      2. Тогда получается что либо перед либо после релиза нужно собрать финальное состояние и положить его в код? Вроде как лишний шаг.

      Мне кажется что такой вариант идеально подошел ты тому, мифическому, Oracle DBA из начала статьи.

      И конечно же основная проблема в любом подходе к database deployment — это сохранение и миграция данных.

      Миграция данных куда хуже, полностью с вами согласен.
      Даже просто откатить DROP TABLE уже непростая задача.


      1. PavelGhost
        20.06.2018 02:54

        Подход интересный, но организационно сложнее.


        Это скорее не сильно сложнее технически (как бы такое утверждение не казалось бы слишком оптимистичным), а сложнее для изменения культуры разработки, т.к. database developer-ы меньше готовы к восприятию версионирования объектов базы данных, технологии релизов, насколько это всё давно естественно и как бы само собой подразумевается в разработке кода приложений. Культура совсем другая нужна.

        Если оценить, то ведь и типов объектов в базе данных, которые применяются в приложениях сейчас обычно не так много, да и видов операций тоже. Т.е. написать генератор перехода состояний не так уж и сложно. Я это делал (несмотря на то, что я — Oracle DBA) на perl даже для версионирования контента таблиц метаданных (конечно, без LOB и очень простых и небольших).


      1. PavelGhost
        20.06.2018 03:09

        Вам надо было посмотреть на RedGate Source Control в связке с Schema Comparison.

        1. А как будет собираться состояние к релизу — из тех патчей/ченжей/миграций?

        Абсолютно также как и application deployment — при таком подходе применимы любые современные методики и разработки и релизов (CI/CD).
        В том и суть, что Source Control не хранит никаких патчей, а хранит стандартизированные DDL объектов, что мне не нравится, кстати. Я в своей давнишней поделке для описания аттрибутов объектов использовал XML, а сейчас бы выбрал JSON. Патчи генерятся на лету уже Schema Comparison, где происходит сравнение двух «endpoint», каждый из которых может быть или коннектом к базе, или snapshot (снимок структуры объектов) или как раз путь к рабочему каталогу в Git с теми DDL, которые сгенерированы Source Control. Snapshot — вообще очень удобная штука — нет прямого доступа к Prod — попросил сделать snapshot и дальше можно его использовать для сравнения, генерации патчей — создания пустых схем и т.п.
        (ссылка на Red Gate)
        2. Тогда получается что либо перед либо после релиза нужно собрать финальное состояние и положить его в код? Вроде как лишний шаг.


        Конечные состояния объектов всегда в Git — как вы их собираете в релизы — это обычная тема application deployment методик.

        Если бы RedGate Deployment Pack не был бы только под Windows, с фокусом сейчас только на SQL Server, без поддержки PostgreSQL, и трудным расширением проприетарного функционала, то мог бы получиться очень продвинутый инструмент. Но и даже в этом виде + VM или docker и какие-никакие command line возможности вполне прилично работают с Oracle, как минимум.
        Я с ними общался на Pipeline конференции пару лет назад — планов по поддержке PostgreSQL у них не было и до сих пор нет.


      1. PavelGhost
        20.06.2018 03:15

        Миграция данных куда хуже, полностью с вами согласен.
        Даже просто откатить DROP TABLE уже непростая задача.


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


  1. BalinTomsk
    19.06.2018 23:10

    А что там с миграцией из ORM баз?


    1. mgramin
      20.06.2018 12:40

      Они щас в соседнем топике обсуждаются — habr.com/post/413597


    1. roller
      20.06.2018 15:17

      А ничего. Модели ORM использовать внутри миграций запрещено. Точка.
      Потому что может случится казус что модели не существует / она изменена, и миграция уже не соотвествует модели ORM.

      PS Вся статья выглядит как попытка откровения, но в 2k18 после десятка лет существования рельсов это скорее капитанство


  1. Stas911
    20.06.2018 13:51

    Однажды довелось собирать обновление базы для боевой системы из 100+ скриптов изменений, разбросанных по почте. Отдельный вопрос, как такое произошло, но самое интересное, что базу тогда собрали и систему запустили (седых волос только добавилось). Тогда я сказал себе «никогда больше»


    1. dm_wrike Автор
      20.06.2018 15:11

      По этому у вас ник 911? :)