Я вспоминаю свой первый день разработчиком-джуном, он свеж в моей памяти, как будто это было вчера. Я ужасно нервничал и понятия не имел, что делаю. Наверно, мой стресс был заметен, потому что одна добрая душа решила взять меня под свою защиту. В тот день я научился писать SQL в коде на PHP, чтобы делать интересные вещи с базой данных.

Однако прежде чем начать, я должен был попросить администратора базы данных (database administrator, DBA) создать несколько таблиц. Я быстро понял, что для того, чтобы сделать любое действие, нужно сразу обращаться к DBA. Нужен новый столбец? Свяжись с DBA. Необходимо отредактировать хранимую процедуру? Это работа для DBA. Я смотрел на него снизу вверх, он был такой суперзвездой, что в итоге позже я сам стал администратором.

Разумеется, я понимаю, что зависимость от кого-либо неизбежно создаёт ограничения. Это безрассудно, трудно и, что хуже всего, впустую тратит навыки DBA.

Управление данными при помощи CI/CD


Автоматизация управления данными при помощи CI/CD позволяет нам сохранять гибкость: схема баз данных обновляется в рамках процесса поставки или развёртывания. Мы можем инициализировать тестовые базы данных в различных условиях и при необходимости выполнять миграции схемы, обеспечивая возможность тестирования на нужной версии базы данных. При развёртывании наших приложений мы можем одновременно выполнять апгрейд или даунгрейд. Автоматизированное управление данными позволяет нам отслеживать все изменения в базе данных, что помогает отлаживать проблемы в продакшене.

Использование CI/CD для управления данными — это единственный способ выполнения непрерывного развёртывания (continuous deployment).


Использование CI/CD для развёртывания приложений и обновления структур баз данных.

Роль DBA


В чём заключается роль DBA, если управление данными автоматизировано? Возможно, они не нужны? Напротив: освобождённые от рутины, они могут сосредоточиться на гораздо более увлекательной и ценной работе, например:

  • Мониторинг и оптимизация производительности движка баз данных.
  • Помощь в разработке структуры схемы.
  • Планирование нормализации данных.
  • Экспертная оценка изменений в базах данных и в скриптах миграции с учётом их влияния на работу баз данных.
  • Выбор оптимального момента для выполнения миграций.
  • Обеспечение соответствия стратегии восстановления потребностям SLA.
  • Написание или улучшение скриптов миграции.


Методики управления данными при помощи CI/CD


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

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

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

Коммиты скриптов баз данных в систему контроля версий


В общем случае существует два типа скриптов баз данных: скрипты на языке описания данных (data-definition language, DDL) и на языке манипулирования данными (data-manipulation language, DML). DDL создаёт и модифицирует такие структуры баз данных, как таблицы, индексы, триггеры, хранимые процедуры, разрешения и представления. DML используется для манипулирования данными в таблицах.

Как и любой код, оба типа скриптов должны храниться в системе контроля версий. Хранение изменений в системе управления версиями позволяет нам воссоздать всю историю развития схемы баз данных. Это делает изменения видимыми для команды, чтобы их можно было контролировать. К скриптам баз данных относятся следующие виды:

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


▍ Использование инструментов для миграции баз данных


Существует множество инструментов для написания и поддержки скриптов миграции. В некоторых фреймворках наподобие Rails, Laravel и Django они являются встроенными. Но если в вашем технологическом стеке это не так, существуют и универсальные инструменты наподобие Flyway, DBDeploy и SQLCompare.

Цель всех этих инструментов — поддержка непрерывного набора дельта-скриптов, по необходимости выполняющих апгрейд и даунгрейд схемы базы данных. Эти инструменты могут определять, какие обновления необходимы при помощи исследования имеющейся схемы и выполнения скриптов обновления в нужной последовательности. Это гораздо более безопасная альтернатива, чем написание скриптов вручную.

Например, для перехода с версии 66 на 70 инструмент миграции выполнит скрипты 66, 67, 68, 69 и 70. Тот же процесс можно выполнить в обратном порядке, чтобы откатить базу данных назад.

Версия Скрипт апгрейда Скрипт отката DDL схемы
...
66 delta-66.sql undo-66.sql schema-66.sql
67 delta-67.sql undo-67.sql schema-67.sql
68 delta-68.sql undo-68.sql schema-68.sql
69 delta-69.sql undo-69.sql schema-69.sql
70 delta-70.sql undo-70.sql schema-70.sql
...
Автоматизированные миграции покрывают 99% ваших потребностей в управлении данными. Существуют ли ситуации, в которых управление должно выполняться вне рамок CI/CD? Да, но такие случаи обычно единичны или относятся к конкретной ситуации, когда огромные объёмы данных нужно переместить в рамках масштабного инженерного проекта. Замечательным примером этого является миграция гигантского количества записей Stripe.

▍ Изменения должны быть небольшими


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

▍ Отделение развёртывания от миграций данных


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

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


Отделение развёртывания приложения от миграций БД. Каждый релиз имеет диапазон совместимых версий БД.

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

▍ Подготовка конвейеров непрерывного развёртывания и миграции


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


Непрерывное развёртывание для баз данных с использованием стратегии разделения.

▍ Миграции нужно сделать аддитивными


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

Например, в нашей базе данных продакшена есть следующая таблица.

CREATE TABLE pokedex (
id BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY,
name VARCHAR(255)
category VARCHAR(255)
);

Добавление нового столбца будет аддитивным изменением:

ALTER TABLE pokedex ADD COLUMN height float;

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

ALTER TABLE pokedex DROP COLUMN height;

Однако вносить аддитивные изменения не всегда возможно. Когда нам нужно изменить или удалить данные, можно обеспечить целостность данных, временно сохранив исходные данные. Например, изменение типа столбца может усечь исходные данные. Мы можем сделать изменение более безопасным, сохранив старые данные во временный столбец.

ALTER TABLE pokedex RENAME COLUMN description to description_legacy;
ALTER TABLE pokedex ADD COLUMN description JSON;
UPDATE pokedex SET description = CAST(description_legacy AS JSON);

Приняв эти меры предосторожности, можно выполнять откат без риска:

ALTER TABLE pokedex DROP COLUMN description;
ALTER TABLE pokedex RENAME COLUMN description_legacy to description;


▍ Откат при помощи CI/CD


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

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

▍ Выполняйте полный бэкап только тогда, когда это делается быстро


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

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

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

▍ Рассмотрите возможность использования сине-зелёного развёртывания


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

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

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


После апгрейда и тестирования неактивной системы пользователи переключаются на неё.


Пользователи переключаются на следующую версию, запущенную на зелёной.

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


При откате мы должны воспроизвести транзакции зелёной на синей, чтобы избежать утерю данных.

Методики тестирования



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

▍ Юнит-тестирование и интеграционное тестирование


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

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


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

▍ Приёмочные и сквозные тесты


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


Чтобы гарантировать совместимость приложения с текущей версией БД, мы загружаем тестовый массив данных в промежуточную (staging) БД и выполняем приёмочные тесты. Если они выполняются успешно, можно развёртывать приложение.

▍ Тесты совместимости и миграционные тесты


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

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

  1. Загружаем в тестовую базу данных текущую схему продакшена.
  2. Выполняем миграцию.
  3. Проводим приёмочные тесты.

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


Проведение приёмочных тестов на миграционной схеме БД позволяет нам выявлять регрессии и находить возможные конфликты миграции.

В заключение


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

Усилия, приложенные к созданию автоматизированного управления данными при помощи CI/CD, многократно окупятся с точки зрения скорости, стабильности и продуктивности. Разработчики смогут свободно работать, пока DBA делают всё необходимое для обеспечения чистоты и бесперебойной работы базы данных.
RUVDS | Community в telegram и уютный чат

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