Комментарии 51
Решение примера 1
Непонятно, почему генерация значения вынесена на вторую фазу миграции. Во-первых, она совершенно не помешает ничему, если будет выполнена на первой фазе, во-вторых, эта генерация может быть выполнена неявно присвоением полю свойства DEFAULT, в третьих, это позволит сразу задать NOT NULL для вставляемого поля, в четвёртых, при создании новой записи старой версией кода, не присваивающей значения полю, оно будет присвоено автоматически.
PostgreSQL: https://dbfiddle.uk/?rdbms=postgres_12&fiddle=1718110eda671e2d6ce06ba2e4604c2e
MySQL: https://dbfiddle.uk/?rdbms=mysql_8.0&fiddle=1718110eda671e2d6ce06ba2e4604c2e
SQL Server: https://dbfiddle.uk/?rdbms=sqlserver_2017&fiddle=cc70d13f81018504022fb7a90161a241
Как итог - разделение миграции на две фазы становится абсолютно бессмысленным мероприятием.
Решение примера 2
Точно так же, свойство DEFAULT поля упрощает процедуру. Достаточно на первой фазе обновить приложение и выпилить работу с полем, а на второй фазе дропнуть само поле.
По той же схеме можно упростить и решение третьего примера.
PS. Подозреваю, что неявной причиной усложнённой схемы миграции является неаккуратное программирование. Например, многие гражданы очень любят опускать список полей присвоения в запросах INSERT - типа, структура таблицы известна, значения лягут куда надо, зачем лишние буквы в запросе? Ну так за такие вещи надо просто надавать по рукам, чтобы в следующий раз неповадно было...
Вы приводите решение для простой ситуации, когда значение нового столбца можно рассчитать средствами СУБД. Такое не всегда возможно, да и если логика формирования значения столбца настолько проста и стабильна что мы её помещаем в БД, то потребность в столбце вообще отпадает.
Автор же показывает обобщённое решение, которое подходит в любой ситуации. Да, его можно оптимизировать в частных случаях, но будет неправильно говорить, что раз в частных случаях можно проще, то описанный подход вообще не нужен.
Я в общем согласен с Вашими возражениями. Однако я оперирую именно приведённым текстом. И именно для него обобщённое более сложное решение - избыточно. Потому либо сам пример должен быть усложнён, либо должно быть явно описано, почему выбран более сложный подход к решению - вот нечто вроде Вашей ремарки.
Вы приводите решение для простой ситуации, когда значение нового столбца можно рассчитать средствами СУБД. Такое не всегда возможно
Вот именно эта причина - лично для меня крайне сомнительна. Речь идёт не о кастомном значении (например, задаваемом пользователем), а о статическом значении по умолчанию, ссылке на "заглушку", которое хорошо известно и либо вообще фиксированное, либо, как максимум, задаётся в некоей служебной таблице базы данных. Но в момент миграции это значение - гарантированно статическое и неизменное.
Стоит избегать значения по умолчанию при добавлении столбца в таблицу, иначе на больших объёмах все запросы к таблице будут блокироваться, пока таблица не перепишется полностью. Это может занять часы.
Возможно, речь идет про Postgres версий 10 и раньше. Такая проблема была устранена в Postgres v11, я упоминал об этом в статье.
:-) Осталось "так же легко" решить еще несколько "проблем" обновления...
1) Штатный откат на "старую установленную версию" (app и db) без downtime
2) Установка обновления (app и db) не с предыдущей версии, а с какой-то более старой (2-3 версии "назад")
1) Ну это просто. Вставка поля откатывается удалением поля. Переименование поля откатывается переименованием поля. А то что процедуру обновления, откатывающую предыдущее обновление, надо рисовать с нуля - так это мелочи...
2) Ну собственно аналогично... хотя именно в предложенной схеме нет места для "более старой" версии.
1) Поле/поля (а впрочем и таблицы приложения) при смене версии могут быть не только добавлены или изменены, но и удалены... Т.е. "по логике" именно "новая версия" приложения просто должна "знать и уметь" работать с несколькими "версиями/структурами" БД (особенно в распределенных БД). Начиная с того что в определенный момент времени БД находится в "промежуточном" состоянии (часть структур "новая", а часть "старая"), а приложение работать должно безотказно.
2) Вариант "отката без простоя" должен быть предусмотрен "по определению" (углубляться в эту тему наверно не стоит)...
Откат на старую версию это сложный философский вопрос. Что делать с новыми данными, которые не ложатся на старую схему? Откуда вообще откат, что происходит с бизнес-процессами в физическом мире? Они тоже откатились?
Автору - было бы неплохо добавить обзор различных средств автоматизации. "Просто разбейте на три этапа" это конечно хорошо, но не очень понятно для невырожденных случаев..
Я понимаю, что статьи пишутся для читателей разного уровня подготовки, но конкретно я как читатель был разочарован, и в какой-то степени чувствую себя обманутым.
Проблема миграций без даунтайма является для меня весьма актуальной, но в итоге я прочитал 10 экранов масла масляного, описывающего очевидные вещи, а в ключевом для меня пункте — только пара абзацев с "общими рекомендациями".
В общем, получилось как в старом советском анекдоте:
В клубе объявили лекцию на тему "народ и партия едины".
никто не пришел. Через неделю была объявлена лекция "Три вида
любви". Народу набилось — тьма.
— Существуют три вида любви, — начал лектор. — Первый вид — патологическая любовь. Это — нехорошо, и на эту тему говорить не стоит. Второй вид — нормальная любовь. Это вам хорошо известно, об этом тоже говорить нет смысла. Остается третий вид любви
— советского народа к Коммунистической партии. Вот на этом мы остановимся подробнее...
Я очень рад, что для вас многое здесь является очевидным, серьезно :) Для многих разработчиков это, к сожалению, не так. У нас есть такой вопрос в техническом опроснике на собеседованиях, и после нескольких сотен проведенных собеседований могу сказать что очень редко люди на него отвечают. Что, собственно, и побудило написать эту статью.
Так что если вы хотя бы в общих чертах представляете, как мигрировать БД без даунтайма - поздравляю, вы входите в небольшой процент людей, которые хотя бы знают с чего начать. Уверен, вы сможете разобраться и дальше, и может даже напишете свой пост, покрывающий более продвинутые аспекты - будет интересно почитать.
А не будет слишком большой наглостью попросить конкретную формулировку этого вопроса? Может быть как раз на него я тоже не отвечу.
Потому что всё-таки, вопрос "что делать, если база данных уже обновлена и содержит новую колонку avatar, о которой старые версии приложения ничего не знают" выглядит высосанным из пальца (та ничего не делать, у нас половина колонок не используется, и никого это не парит, руки не дойдут их повыкидывать), а вопрос "что делать, если код пытается писать в колонку, которой нет" не выглядит настолько неберущимся. Как минимум, идея сначала проверить, есть ли эта колонка, должна прийти в голову многим.
Вопрос звучит так:
Как провести миграцию базы данных в продакшене без даунтайма, на примере добавления и удаления колонки для существующей таблицы?
Если видно, что к вопросу требуется пояснение, то добавляем: представьте - вы разрабатываете новую фичу, в рамках которой добавляется новая колонка в существующую таблицу в БД, или же наоборот, удаляется. Как выложить это в продакшен так, чтобы во время выкладки приложение всегда работало, чтобы не было никаких сбоев?
Странно, в такой формулировке я не вижу здесь проблемы. Здесь даже нет обязательного условия, что колонка должна быть не nullable (не говоря уже про чехарду с контейнерами в кубере). В итоге добавление вообще нас никак не колышит (ну добавили и добавили — коду, который про неё не знает, на неё и пофиг), а при удалении просто разносим по времени — сначала убираем функционал, который с ней работает, и таким образом сводим задачу к уже решённой, а потом просто дропаем колонку.
То есть с девелоперской точки зрения я вообще не вижу здесь проблемы, упражнение на логику, кто на ком стоял.
Для меня проблема даунтайма — это на 100% ДБА-шная проблема, и вот её решение как раз меня очень и интересовало бы.
Я кстати согласен с автором касательно того, что многие программисты не представляют, как менять схему данных на живой системе именно с точки зрения процесса выгрузки и программирования. У нас тоже есть такой вопрос в собеседованиях, правда мне больше нравится спрашивать про сплит полей.
А что касается "technicalities", из серии как дропнуть колонку в таблице с 100млн строк "на живую" и не положить мастер при этом, это несколько другая задача, к которой программисты часто не имеют никакого отношения и этот процесс, как вы правильно заметили, лежит в сфере интересов ДБА.
Кстати, вдруг вам поможет, мы для этого использовали gh-ost, очень хорошо работает.
В общем, это две разные темы, и мне кажется, их описывать в одной статье даже может и не стоит, ведь аудитории совершенно разные.
Автору на заметку: может быть имеет смысл уточнить целевую аудиторию статьи.
О, спасибо, уже смотрю! Идея интересная.
… и даже очень интересная
Если я правильно понял, они сами руками читают бинлог, и накатывают все DML запросы на таблицу-призрак. И я так глубоко ещё не прочёл, но так понимаю что накат должен идти по таким правилам
- вставки идут как есть
- апдейты и делиты проверяют наличие строки и если нету — то задерживают до появления
В общем, задачка не из простых…
Но вот это уже да — реальная рекомендация по миграции без даунтайма. Так что статья свою роль сыграла в итоге! :)
Добавьте ещё liquibase и flywaydb в раздел про то, что нельзя, чтобы на БД в один момент времени работало больше одной миграции. Эти инструменты как раз помогают этого достичь
Там же не только про то, что нельзя чтобы в один момент времени работало несколько миграций. Там еще и про ретрайи, поэтому меня настораживает фраза у flywaydb про "Auto-migration on startup". Наверняка существуют окружения в которых это допустимо, но если у нас традиционное веб-приложение на кластере - то я бы так не рисковал.
Там же не только про то, что нельзя чтобы в один момент времени работало несколько миграций.
И у вас в статье не только про это и инструменты не только такую проблему закрывают ))
Там еще и про ретрайи, поэтому меня настораживает фраза у flywaydb про "Auto-migration on startup".
Вы наверное тут имели в виду, что миграция не должна накатываться два раза по какой-нибудь роковой случайности, да? liquibase это обеспечивает, flywaydb тоже. Хотя я сам использую только liquibase.
Наверняка существуют окружения в которых это допустимо, но если у нас традиционное веб-приложение на кластере — то я бы так не рисковал.
Ваш ответ сначала привёл меня в шок, потому что как раз для традиционных веб приложений liquibase это традиционное решение ))). Но потом я посмотрел на теги и не нашёл там java. Тут то всё и встало на свои места.
liquibase это в мире java считай стандартный инструмент для миграций. Или flywaydb, они делают плюс-минус одно и то же. Их можно интегрировать в приложение или запускать как отдельную утилиту. Если в вашем мире подобных стандартных утилит нет, то рекомендую попробовать.
Правда, для запуска пока что ещё нужна java. Думаю рано или поздно это починят и сделают бинарник без дополнительных зависимостей, но пока вот так.
Каким образом flywaydb, при запуске в виде <run-db-migrations> && <launch-the-app>
, в Kubernetes или другом оркестраторе (ECS, Swarm, ...), в случае сбоя миграции на первом инстансе остановит дальнейшую раскатку приложения, которая выполняется одновременно и параллельно на нескольких инстансах?
Я не большой эксперт в kubernetes, но вроде как это можно решить с помощью настройки maxUnavailable и maxSurge.
Наверняка можно при помощи настройки закрутить конфигурацию так, чтобы по сути отказаться от параллельного запуска новых инстансов и запускать их последовательно и строго по одной попытке.
Даже если риск с повторными миграциями и уйдет, на мой взгляд это неоправданная жертва, которая серьезно затормозит процесс раскатки новой версии, и которой можно легко избежать, заменив запуск миграций на старте приложения запуском их отдельно перед началом раскатки на кластер.
Каким образом flywaydb, при запуске в виде <run-db-migrations> && <launch-the-app> в случае сбоя миграции на первом инстансе остановит дальнейшую раскатку приложения, которая выполняется одновременно и параллельно на нескольких инстансах?
Ну вы же сами написали, что не надо так делать )). Что надо раскатывать миграции через CI.
Однако, если вам интересно обсудить именно <run-db-migrations> && <launch-the-app>, то тут всё просто. flywaydb сделает так, что одновременно будет работать только одна миграция. Когда эта миграция упадёт с ошибкой, приложение, естественно, запущено не будет.
Но, как вы справедливо отметили, миграция одновременно запускается на нескольких инстансах. На одном она работает, остальные ждут. Когда миграция на одном инстансе упадёт, то сразу заработает такая же миграция на другом инстансе, которая сразу же упадёт, потому что начнётся с того места на котором закончил предыдущий инстанс. Всё попадает, деплой завершится неудачей, можно спокойно разобраться по логам, что пошло не так.
Плюс ещё есть небольшая вероятность, что первая миграция упала по таймауту. Допустим больше пяти минут работала. Вторая миграция тогда тоже скоре всего упадёт по таймауту, но впустую потратит при этом пять минут. И так далее, по пять бесполезных минут на инстанс
Для того, чтобы так не получалось обычно поступают следующим образом. Второй инстанс он же ждёт, пока первый закончит миграцию. Максимальное время ожидания — параметр, который можно задать в настройках. Если он будет равен, допустим четырём минутам, то после пятиминуной миграции на первом инстансе, остальные инстансы даже не будут пробовать, а сразу упадут по таймауту.
P. S. Да, если вас интересует подробности того, как liquibase добивается того, чтобы больше одной миграции не работало — то ответ простой. С помощью локов в специальной таблице в БД ))
"Auto-migration on startup" - это одна из стратегий. Есть еще режим "Validate", который сверяет миграции в приложении с теми, что были применены в БД. Это позволяет, например, реализовать схему, когда один экземпляр приложения отвечает за накатку миграций, а остальные делают только валидацию.
Я очень рад, что для вас многое здесь является очевидным, серьезно :) Для многих разработчиков это, к сожалению, не так.
Полностью поддерживаю! Я сам, знаете ли, своего рода разработчик и написал практически точно такую же статью, как ваша, только другую ))). Правда ещё не опубликовал. И мне тоже многие говорят, что ничего нового тут нет и так далее.
И, наверное, самым красноречивым доказательством того, что такие статьи очень нужны будет тот факт, что у вас в процедуре миграции баг ))). Возможно не фатальный, но самый настоящий. Получается что те люди, которым содержимое вашей статьи кажется очевидным, его не заметили. А значит такие статьи нужны!
P. S. Я не стал рассказывать в чём баг, потому что возможно вам хочется самому разобраться где он. Если вам интересно, я скажу в каком месте он спрятался. Или могу сразу рассказать что за баг, где он и к чему может привести, только напишите )))
спасибо! Вполне возможно что есть баг, все люди ошибаются. Если подскажете где - буду признателен
Если подскажете где баг — буду признателен
Он в сценарии переименования таблицы ))
На втором шаге из кода приложения полностью убирается работа с полем avatar. А на первом шаге чтение происходит именно из этого поля.
Если раскатать код из второго шага и дать ему совсем немного поработать, то он запишет новые значения в avatar_url. Дальше возможно придётся делать откат.
После отката старый код будет читать значения из поля avatar. А там значение устаревшее.
Чтобы такого не могло случиться, нужно делать дополнительный шаг. Вторым шагом не прекращать работу с полем avatar, а по прежнему писать и туда и туда, но читать уже из новой колонки.
Фаза 2 у вас соотвественно превращается в Фазу 3, а Фаза 3 в Фазу 4
Спасибо, понимаю ваш пойнт. Не уверен, правда, что это можно считать багом. Например, в моей любимой парадигме "Continuous Deployment" откаты в таком виде вообще не делаются. Там стратегия "всегда только вперед", то есть если нужно сделать откат, то это git revert - push в репозиторий - и опять вперед. Соответственно, в таком случае откат включал бы в себя не только возврат к прежнему коду приложения, но и скрипт для обратной миграции данных.
Разумеется, разные приложения используют разные стратегии для выкладки, да и риск связанный с миграцией данных может сильно отличаться. Для каких-то случаев предложенный вами подход наверняка был бы полезен и помог бы дополнительно подстраховаться от сбоев. Однако, я не уверен что его стоит применять строго всегда и везде.
Спасибо, понимаю ваш пойнт. Не уверен, правда, что это можно считать багом.
Всё зависит от контекста. Если в вашем случае в потере нового аватара нет ничего страшного, то это действительно не баг ))
Например, в моей любимой парадигме "Continuous Deployment" откаты в таком виде вообще не делаются.
На откатах проще объяснять, но вообще точно такая же проблема будет когда на среде одновременно работает два инстанса с первой и второй версией кода. Вторая версия кода обновит аватар в avatar_url, а первая версия этого обновления не увидит, потому что будет смотреть на поле avatar.
Для каких-то случаев предложенный вами подход наверняка был бы полезен и помог бы дополнительно подстраховаться от сбоев. Однако, я не уверен что его стоит применять строго всегда и везде.
Вы о дополнительном шаге? Думаете в в вашем случае он не актуален?
Вторая версия кода обновит аватар в avatar_url, а первая версия этого обновления не увидит, потому что будет смотреть на поле avatar.
да, действительно. Вероятность не очень большая, потому что записи, как правило, случаются сильно реже чтений, но для популярных нагруженных сервисов может проскочить. Спасибо что заметили!
поправил. К сожалению, вместе с исправлением пришлось убрать искрометную фразу про "трехфазный деплой", жаль...
Я, кстати, не говорил, что статьи по основам, "которые всем известны" не нужны. Основы тоже надо подтягивать до современного уровня. Просто замах конкретно здесь не соответствует содержанию. И проблема именно в этом несоответствии.
И всё же, второй раздел "Повышенная нагрузка на БД" в рамках названия статьи раскрыт недостаточно. Советы - это хорошо, но всё же хотелось бы посмотреть конкретные примеры, как в первом разделе. Типа "вот гигантская таблица и нам надо добавить в неё колонку, операция блокирующая - не беда, просто возьмите обычный советский..."
Добавить колонку ерунда. Вот физически удалить колонку из большой таблицы - это могут быть часы.
Полностью согласен, тема нагрузки большая и затронута лишь поверхностно. Почему - во-первых, и так уже длинная простыня получилась :) Во-вторых, в этом месте уже начинаются заметные различия между БД. Как говорится, что мускулю хорошо - то постгре смерть, или наоборот. Ну и в-третьих, статья рассчитана в качестве вводной, чтобы побудить людей этим заниматься. По моим наблюдениям, на многих проектах люди просто даже не пытаются. Если они пробуют делать первые миграции без даунтайма и достигают успехов - то дальше, как правило, все получается. И решения для больших таблиц находят самостоятельно, и все такое. Главное, начать :)
Создать копию таблицы, но уже с новой колонкой, и постепенно переключать приложение на эту копию, а потом дропнуть исходную таблицу.
Вообще тема миграции довольно обширна. Для более-менее успешного выполнения еще на этапе проектирования структуры БД необходимо предусмотреть как жизненный цикл данных, так и возможность миграции. Для миграции, например, нужно чтобы приложение обращалось к логическому слою БД. Т.е. приложение->(view, ХП, instead of триггера и т.д.)->физические таблицы, служебные представления, удаленные подключения к другим БД и т.д. Метаданные в БД обновить можно быстро(не всегда, увы), как с увеличением версии, так и с уменьшением. А обновление физической структуры можно сделать асинхронным. К сожалению, на этапе проектирования мало кто задумывается об этом. Причина, имхо, в том, что разработчиком-архитектором выступает, как правило, архитектор приклада(джавер, питонист, пхпшник etc). Он знает БД на уровне "select * from table t"(я тут утрирую, конечно). А потом начинается "веселье".
Он знает БД на уровне "select * from table t"(я тут утрирую, конечно). А потом начинается "веселье".
К сожалению, тут нет никакого утрирования - оно сплошь и рядом. И первое веселье - это когда сервер БД используется как тупая помойка для данных, а абсолютно вся обработка выносится на прикладной уровень - сперва волочём всю таблицу на клиента, там долго жуём, и затем выплёвываем обратно... даже индексов, и тех порой нету. Увы, достаточно частая штука, да к тому же интенсивно поощряемая различными фреймворками и библиотеками.
Стоило бы еще указать, что alter table add column - не совсем безопасная операция. Хотя в современных СУБД изменяются только метаданные, но она все равно требует эксклюзивную блокировку. И, например, в сценарии "длинный отчет, alter table, мелкие пишущие транзакции", все мелкие транзакции будут ждать окончания отчета (т.е. фактически таблица будет заблокирована). Перед миграциями обязательно нужно закрыть все длинные транзакции на таблице (или достоверно знать, что их не может быть).
При использовании CI/CD сервера для накатки миграций каким образом накатывать их на локальные окружения разработчиков?
Одно из удобств упомянутых ранее инструментов вроде flyway и liquibase, а если быть точнее в целом стратегии обновления схемы через приложение, в том, что процесс одинаков для всех окружений - где бы приложение не запустилось, оно либо обновит схему до нужной версии, либо упадет, сообщив, что схема несовместима.
CI/CD в статье упомянут только в контексте накатки миграций в проде. Это только способ запуска, который никак не ограничивает какой фреймворк для миграций будет использован. Я абсолютно ничего не имею против flyway, liquibase и любых других инструментов. Речь всего лишь о том, как их запускать.
В окружении разработчика, разумеется, CI/CD не нужен. Мы же не пытаемся обеспечить там zero-downtime, верно? И вообще, зачем пытаться запускать окружение разработчика на кластере и за балансировщиком нагрузки?
Мы же не пытаемся обеспечить там zero-downtime, верно? И вообще, зачем пытаться запускать окружение разработчика на кластере и за балансировщиком нагрузки?
Конечно, нет. Но мы пытаемся сделать процесс разработки удобным и безопасным не только в контексте выкатки на прод, но и для разработчиков, верно?
Поймите меня правильно, я не оцениваю предлагаемый вами подход как правильный или неправильный, а хочу понять как он в целом работает на практике.
Но мы пытаемся сделать процесс разработки удобным и безопасным не только в контексте выкатки на прод, но и для разработчиков, верно?
Да, действительно. Поэтому накат миграций через liquibase при старте приложения на проде будет выключен средствами конфигурации. А в конфигурации для локального окружения разработчика он будет включен. И будет всё очень удобно.
На практике, конечно, когда у нас есть большая многокомпонентная высоконагруженная система, то скорее всего разработчики не будут разворачивать её локально, а будут запускать локально какой-то компонент и подключать его к удалённой среде. И тут автоматический прогон ликви скриптов конечно скорее вреден. В таком воркфлоу автоматом скрипты прогоняются только для того, чтобы создать стуруктуру БД под юнит тесты ))
... накат миграций через liquibase при старте приложения на проде будет выключен средствами конфигурации. А в конфигурации для локального окружения разработчика он будет включен. И будет всё очень удобно
+1, отличный вариант.
Конечно, есть старая програмистская мудрость, что надо стремиться чтобы окружение разработки было максимально приближено к продакшену. Однако, в современных реалиях веб-разработки абсолютно точного сходства прямо во всем-всем-всем добиться очень сложно, да и не нужно. Касательно ОС, версий библиотек и фреймворков - без проблем, дело важное и нужное, тем более что системы сборки и всякие докеры значительно упростили задачу, грех не пользоваться.
Однако, когда дело касается инфраструктуры - на мой взгляд, стремиться к точному сходству не стоит. В продакшене у нас может быть Kubernetes, а локально - Docker Compose, и это нормально. В продакшене приложение запускается в нескольких экземплярах, а локально - в один, и это хорошо. В продакшене миграции запускаются одним образом, а локально - другим, и так удобнее.
промазал ответом
Зачем каждый раз перед удалением делаете колонку nullable? Бессмысленное действие, важно только использует приложение колонку или нет. Если использует удалять нельзя, то в любом случае удалять нельзя, будь она хоть nullable хоть non-nullable. А если приложение не использует колонку, то делай с ней что хочешь.
Мигрируем БД в продакшене без даунтайма