Ne v kontakte Asocial programmer's blog

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

Feature image

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

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

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

  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