Почему развертывание баз данных нельзя рассматривать как развертывание приложений
Представьте: вы управляете интернет-магазином в разгар рабочего дня. Пользователи просматривают товары, добавляют их в корзину и оформляют заказы. А в это время ваша команда DBA запускает миграцию для добавления колонки discount_price в таблицу products. Внезапно сайт начинает тормозить. Поиск товаров зависает. Оформление заказов срывается. Пользователи начинают жаловаться в соцсетях.
Что произошло? База данных заблокировала таблицу products во время изменения её структуры, и каждый запрос, которому нужно было читать или писать данные о товарах, вставал в очередь ожидания. Само приложение было в порядке. Серверы работали нормально. Но база данных защищала себя от повреждения данных во время изменения схемы.
Этот сценарий знаком командам, которые относятся к развертыванию баз данных так же, как к развертыванию приложений. Разница фундаментальна: приложение можно остановить, заменить новой версией и перезапустить за секунды. База данных должна продолжать обслуживать пользователей во время изменений.
Как работают блокировки и почему они вредят
Когда вы выполняете команду для изменения структуры таблицы, база данных должна гарантировать, что никакая другая операция не модифицирует те же данные во время изменения. Так базы данных обеспечивают согласованность. Для этого база данных захватывает блокировку на таблицу или определенные строки. Пока блокировка активна, любой другой запрос, пытающийся читать или писать данные в той же таблице, должен ждать.
Некоторые изменения схемы выполняются быстро. Например, добавление колонки со значением по умолчанию NULL в PostgreSQL может завершиться за миллисекунды, не блокируя чтение. Но другие операции не так дружелюбны. Создание индекса на большой таблице, изменение типа данных колонки или удаление колонки может заблокировать таблицу на минуты или даже часы.
Рассмотрим разницу между этими двумя SQL-запросами:
-- Безопасно: добавляет nullable колонку, выполняется за миллисекунды, без блокировки
ALTER TABLE products ADD COLUMN discount_price DECIMAL(10,2);
-- Опасно: перезаписывает всю таблицу, блокирует на минуты на больших таблицах
ALTER TABLE products ALTER COLUMN price TYPE DECIMAL(12,2);
Первый оператор добавляет колонку, которая может быть NULL, поэтому база данных обновляет только метаданные. Второй оператор изменяет тип данных существующей колонки, вынуждая базу данных перезаписывать каждую строку в таблице. Пока выполняется перезапись, таблица заблокирована, и все запросы к products должны ждать.
Реальная опасность — каскадный эффект. Запрос, ожидающий блокировку, замедляет не только одну функцию. Он может заблокировать другие запросы, которые зависят от той же таблицы. В крайних случаях приложение перестает отвечать полностью, потому что все потоки базы данных заняты запросами, ожидающими блокировки. Пользователи видят бесконечные индикаторы загрузки или ошибки тайм-аута. С точки зрения пользователя, приложение не работает. База данных просто занята самозащитой.
Не все изменения схемы одинаковы
Разные базы данных обрабатывают блокировки по-разному, и не все операции со схемой несут одинаковый риск. Понимание того, какие операции безопасны, а какие опасны, необходимо для планирования развертывания баз данных.
PostgreSQL может добавить колонку со значением по умолчанию NULL без блокировки чтения. Операции ONLINE DDL в MySQL могут выполняться без блокировки таблицы для конкурентных DML, но все равно требуют кратковременной блокировки метаданных в начале и конце. Даже операции, которые заявляют о "бесперебойности" или "нулевом простое", необходимо тестировать в среде, зеркально отражающей продакшн.
Операции, которые обычно вызывают больше всего проблем:
- Создание индексов на больших таблицах
- Изменение типов данных колонок
- Удаление колонок
- Добавление колонок с ненулевым значением по умолчанию (в некоторых базах данных)
- Переименование колонок или таблиц
- Выполнение
ALTER TABLE, которые перезаписывают всю таблицу
Операции, которые обычно безопаснее:
- Добавление колонок со значением по умолчанию
NULL(в PostgreSQL) - Добавление индексов с
CONCURRENTLY(в PostgreSQL) - Создание новых таблиц
- Добавление новых колонок с
ONLINEDDL (в MySQL для поддерживаемых операций)
Ключ в том, чтобы знать, к какой категории относится каждая операция для вашей конкретной СУБД, и тестировать фактическое время выполнения в стейджинге перед запуском в продакшн.
Почему откат сложнее, чем вы думаете
Откат приложений прост. Вы развертываете предыдущую версию кода, и приложение начинает обрабатывать запросы со старой логикой. Откат базы данных — не то же самое.
Если вы добавили колонку и нужно откатиться, вы не можете просто "удалить" колонку. Вам нужно запустить другую миграцию для её удаления. Сама операция удаления может заблокировать таблицу. Если миграция изменила типы данных или перестроила таблицы, откат может потребовать преобразования данных обратно в старый формат, что может быть медленным и рискованным.
Эта асимметрия меняет подход к оценке рисков. С приложениями вы можете развернуть быстро и откатиться, если что-то пошло не так. С базами данных нужно предотвращать проблемы заранее, потому что путь восстановления болезнен.
Практические стратегии для безопасного развертывания баз данных
Команды, которые хорошо справляются с развертыванием баз данных, не полагаются на удачу. Они строят процессы, которые снижают вероятность инцидентов, связанных с блокировками, и делают восстановление управляемым, если что-то пошло не так.
Планируйте миграции на периоды низкой нагрузки. Запускать изменение схемы во вторник в 14:00 — значит напрашиваться на неприятности. Планируйте его на воскресенье в 2:00 ночи или в любое другое окно низкой нагрузки вашего приложения. Если ваше приложение обслуживает пользователей по всему миру, возможно, потребуется разбить миграции на более мелкие шаги, которые можно выполнять в несколько окон низкой нагрузки.
Разбивайте большие изменения на маленькие шаги. Вместо одной миграции, которая добавляет три колонки, создает два индекса и изменяет тип данных, разделите её на отдельные миграции. Каждая миграция должна быть достаточно маленькой, чтобы завершиться быстро и быть откатанной без каскадных эффектов.
Измеряйте время выполнения в стейджинге. Перед запуском любой миграции в продакшн запустите её в стейджинге с аналогичным объемом данных и паттернами трафика. Если миграция занимает 30 секунд в стейджинге, в продакшне с реальными данными она может занять 30 минут. Измеряйте и планируйте соответственно.
Мониторьте время ожидания блокировок во время миграции. Настройте оповещения, которые срабатывают, когда запросы начинают ждать блокировки дольше нескольких секунд. Если вы видите, что время ожидания блокировок растет, вам нужна процедура прерывания миграции до того, как она вызовет полный сбой.
Имейте четкую процедуру прерывания. Определите, что именно делать, если миграция выполняется слишком долго или вызывает конкуренцию за блокировки. Это может означать убийство процесса миграции, откат к предыдущей версии схемы или переключение на реплику чтения, пока миграция завершается.
Практический чек-лист для развертывания баз данных
Перед запуском любого изменения схемы в продакшн пройдитесь по этому чек-листу:
- Безопасна ли эта операция для конкурентных чтений и записей в вашей СУБД?
- Тестировали ли вы миграцию в стейджинге с аналогичным объемом данных?
- Каково предполагаемое время выполнения на основе тестов в стейджинге?
- Запланирована ли миграция на окно низкой нагрузки?
- Есть ли у вас план отката, который не требует другой рискованной миграции?
- Настроены ли оповещения мониторинга для времени ожидания блокировок?
- Знает ли команда процедуру прерывания, если что-то пойдет не так?
Ключевое различие
Развертывание приложений — это замена кода. Развертывание баз данных — это трансформация живых данных во время обслуживания пользователей. Это принципиально разные операции, требующие разных стратегий, разных оценок рисков и разных планов отката.
Команды, которые успешно справляются с развертыванием баз данных, уважают это различие. Они не просто добавляют шаги миграции в тот же пайплайн, который развертывает код приложения. Они проектируют отдельные рабочие процессы с соответствующими защитными механизмами, процедурами тестирования и мониторингом.
В следующий раз, когда вы будете планировать изменение базы данных, начните с вопроса: "Что произойдет с пользователями, пока выполняется эта миграция?" Ответ на этот вопрос покажет, готовы ли вы к развертыванию.