Обновления для разработчиков и людей — несерьезно о серьезном.

Пост вышел многабукаф, поэтому ленивые могут пропускать части, помеченные как иллюстративные.

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

Типичный подход к решению этой проблемы выглядит так:

  1. Делается публичный релиз продукта.
  2. Начинается работа над следующим релизом, в ходе которой пишется и обновляется конвертор с предыдущего релиза в будущий. Таким образом, текущий релиз всегда можно обновить до dev-ветки.
  3. Публикуется новый релиз, который способен спокойно обновиться с предыдущего, пользователи счастливы.

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

Чтобы осчастливить разработчиков нужно изобрести механизм обновления с любого билда на любой более новый. Такой механизм, если я не ошибаюсь, впервые получил широкое распространение во фреймворке Ruby on Rails под названием "Database Migrations", а теперь пробрался и во многие другие среды, например в мой любимый PHP-фреймворк Yii. И так, что же нам предлагают миграции?

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

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

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

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

Чтобы немного облегчить понимание изложенного, сделаю лирическое отступление о том, как это работает. Пусть над проектом трудятся три программиста — Алексей, Борис и Вадим, и пусть они пишут CMS, ядро которой уже написано и опубликовано как релиз 1.0 перед новым годом, а теперь они заняты добавлением разных фитч.

  1. В понедельник, с похмелья, Алексей решил написать систему оценки комментариев. Для этого он добавил в таблицу cms_comments поле comment_ratinnnng и был очень доволен, оформив эту операцию в виде миграции, получившей номер 10-01_10:23:14 (10 января, 10 часов, 23 минуты, 14 секунд). Вместе с логикой голосования он ее закоммитил и пошёл домой.
  2. В тот же понедельник непьющий Борис успел написать модуль обратной связи, добавив таблицу cms_feedback (в виде миграции с номером 10-01_09:43:55), а так же усовершенствовал профиль пользователя, привнеся в него возможность указать свой логин в skype. Для этого он создал миграцию за номером 10-01_15:20:31.
  3. Вадим же в понедельник вообще не пришёл, и это полностью на его совести.
  4. Во вторник утром Вадим в порыве отваги (или угрызений совести за прогул) решил протестировать то, что они в суматохе писали перед новым годом за час до релиза, и, поставив свежую копию из репозитория, принялся заполнять CMS огромным количеством тестовых данных.
  5. На этот раз Алексей был бодр и свеж, и первым делом обновил свою рабочую копию, в которой ему приехали труды Бориса. Залив эту копию на свою тестовую машину, он запустил менеджер миграций, который подхватил обе миграции Бориса, предотвратив ошибки в духе "unknown kolumn user_skype in table cms_users". Заметим, что миграция Алексея повторно запущена не была — менеджер миграций исполнил её ещё вчера, как только Алексей её написал, и больше её запускать не будет. Вторым делом Андрей достал запасённую банку пива и больше ничем серьёзным не занимался.
  6. Борис же заметил, что Алексей вчера опечатался в названии столбца comment_ratinnnng и по доброте душевной поправил ошибку в коде, заодно создав миграцию 11_01-11:05:27, которая исправляла ошибку в базе. А ещё Борис решил, что тексты страниц лучше вынести в отдельную таблицу для улучшения производительности. Сделав это, он написал ещё и миграцию 11_01-21:05:01, которая переносит тексты в новую таблицу и удаляет из старой.
  7. А вот в среду миграции спасли Андрея и Бориса от насильственной смерти, поскольку Вадим закончил наполнять тестовыми данными свою копию CMS и перед тем, как начать её всерьёз тестировать, решил подтянуть из репозитория свежие правки, чтобы заодно протестировать и их.

А вот здесь следует проявить серьёзность и разобраться, какая ситуация сложилась у Вадима. У него установлена версия CMS, отличающаяся от релиза 1.0 наличием рейтинга у комментариев, лишним полем в профиле и возможностью обратной связи. К сожалению, в его версии все ещё присутствует опечатка в имени столбца. В среду, когда Вадим извлек свежую версию cms, благодаря механизму миграций, у него были запущены миграции 11_01-11:05:27 и 11_01-21:05:01, и не запущены те, что были добавлены 10 января, так как соответствующие изменения уже были учтены в момент установки копии Вадима. А вот если бы наши друзья использовали скрипт, обновляющий с релиза на релиз, Вадим бы оказался в очень неприятной ситуации: с одной стороны, у него уже готова куча отличных тестовых данных, с другой — чтобы их использовать для тестирования новой версии их нужно либо заново вводить, либо самому писать обновлятор, попутно разбираясь, что и как поменялось.

Резюмируя, у миграций есть следующие плюсы:

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

Однако, у миграций есть и некоторые недостатки, которые иногда играют важную роль:

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

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

P.S. Neon Synthesis — Eden



Trackback URL for this post:

/trackback/425
Vladislav Bauer пн, 19/03/2022 - 00:58

Думаю database migration появился задолго до появления java / ruby / etc.. :) Разработчики сложнх систем всегда их использовали.

Unix timestamp использовать конечно можно, но практика показывает, что не удобно. Достаточно обычной нумерации 00-*.sql, 01-*.sql и так далее. Хотя конечно можно приписывать его в конец, но на самом деле он не решает ничего. Два программиста могут модифицировать одни и те же таблицы и тут уже.. кто успел тот и съел. :)

Пару слов о типичном случае миграции базы данных с которой работает одно или несколько приложений:
Сам процесс миграции можно разделить на 3 основных части: before (до отсановки сервера приложений / приложения), critical (во время остановки сервера) и post (после запуска сервера, новая версия приложения). Таким образом можно минимизировать процесс простоя сервера в неработоспособном состоянии (минимизировав количество патчей которые содержатся в секции critical). В before выносим созидающие патчи, в post - подчищающие. Лучше выполнять стадию post предыдущей версии приложения перед before следующей (вдруг понадобятся данные которые мы хотим удалить)

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

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

Alek$ пн, 19/03/2022 - 09:53

Спасибо за ценное дополнение :)

UNIX timestamp предпочтителен для того, чтобы минимизировать шанс того, что два программиста попытаются создать одновременно миграции с одинаковыми номерами — шанс. Ну а конфликты в логике таких миграций как ни крути придётся решать вручную, как и с любым кодом, тут ничего не сделаешь, да.

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