Zero-downtime миграция схемы БД: паттерн Blue-Green
Самая распространённая причина downtime при деплое — изменение схемы БД. ALTER TABLE на таблице в 200 млн строк в PostgreSQL держит AccessExclusiveLock несколько минут. Всё это время запросы на запись висят в очереди, connection pool забивается, приложение отдаёт 500. Для высоконагруженного e-commerce такой простой может стоить около $9k–13k. Мы специализируемся на zero-downtime миграциях и используем Expand-Contract — единственный надёжный паттерн, проверенный на таблицах объёмом более 1 ТБ. Он позволяет менять схему без остановки приложения. В e-commerce каждая минута простоя стоит до $5 000, поэтому критически важно избежать блокировок.
Blue-green database migration — это не две отдельные базы данных с синхронизацией. В реальности это расширение схемы так, чтобы одновременно работали и старая, и новая версия приложения, читая из одной БД. Мы предлагаем полный цикл: аудит, проектирование, реализацию и тестирование. Получите консультацию по zero-downtime миграции — свяжитесь с нами для оценки вашего проекта.
Почему Expand-Contract — единственный надёжный паттерн?
Фундаментальный принцип zero-downtime миграции схемы называется Expand-Contract (или Parallel Change). Он состоит из трёх фаз, каждая из которых деплоится отдельно.
Фаза Expand — расширяем схему, не ломая старый код:
-- Добавляем новый столбец как nullable (!)
ALTER TABLE orders ADD COLUMN payment_method VARCHAR(64);
-- Или переименование через добавление нового столбца
ALTER TABLE users ADD COLUMN first_name VARCHAR(255);
ALTER TABLE users ADD COLUMN last_name VARCHAR(255);
-- Индекс создаём CONCURRENTLY — без блокировки таблицы
CREATE INDEX CONCURRENTLY idx_orders_payment_method ON orders(payment_method);После этого деплоится версия приложения v2, которая пишет и в старые, и в новые столбцы одновременно. Старая версия v1 продолжает работать — она не знает о новых столбцах, но это не проблема, потому что они nullable.
Фаза Migration — бэкфилл данных в фоне:
-- Заполняем новые столбцы батчами, не блокируя таблицу
DO $$
DECLARE
batch_size INT := 10000;
last_id BIGINT := 0;
max_id BIGINT;
BEGIN
SELECT MAX(id) INTO max_id FROM users;
WHILE last_id < max_id LOOP
UPDATE users
SET first_name = split_part(name, ' ', 1),
last_name = trim(substr(name, strpos(name, ' ')))
WHERE id > last_id AND id <= last_id + batch_size AND first_name IS NULL;
last_id := last_id + batch_size;
PERFORM pg_sleep(0.1); -- небольшая пауза между батчами
END LOOP;
END $$;Фаза Contract — убираем старое после полного перехода:
-- Только после того, как v1 полностью выведена из эксплуатации
ALTER TABLE users DROP COLUMN name;
ALTER TABLE orders ALTER COLUMN payment_method SET NOT NULL; Где это ломается: типичные ошибки и их решения
Самая частая ошибка — добавить NOT NULL без DEFAULT в Expand фазе. PostgreSQL до версии 11 при этом переписывал всю таблицу. С PostgreSQL 11+ ADD COLUMN NOT NULL DEFAULT 'value' работает мгновенно (значение хранится в системном каталоге), но только если DEFAULT статический, а не вызов функции.
Вторая ловушка — уникальные индексы. CREATE UNIQUE INDEX CONCURRENTLY может завершиться с ошибкой duplicate key, если данные уже содержат дубли. Тогда индекс остаётся в состоянии INVALID — его надо дропнуть и разобраться с данными прежде, чем создавать снова.
Третья проблема — внешние ключи. ALTER TABLE ADD CONSTRAINT FOREIGN KEY в PostgreSQL берёт ShareRowExclusiveLock на обе таблицы и полностью сканирует дочернюю таблицу. На больших таблицах это займёт минуты. Решение — создавать FK с NOT VALID, потом валидировать отдельно:
-- Шаг 1: добавить FK без проверки существующих строк (быстро)
ALTER TABLE orders ADD CONSTRAINT fk_orders_users FOREIGN KEY (user_id) REFERENCES users(id) NOT VALID;
-- Шаг 2: валидация в фоне (не блокирует DML, только берёт ShareUpdateExclusiveLock)
ALTER TABLE orders VALIDATE CONSTRAINT fk_orders_users; Как избежать блокировок при добавлении внешних ключей?
Используйте двухшаговый подход: сначала NOT VALID, затем отдельная валидация. Это снижает время блокировки с минут до миллисекунд. Валидация сканирует таблицу, но не мешает обычным операциям — только короткая блокировка ShareUpdateExclusiveLock для обновления словаря.
Автоматизация проверок в CI/CD
В pipeline перед деплоем стоит запускать статический анализ миграций. Инструмент squawk (для PostgreSQL) проверяет SQL-файлы на опасные паттерны:
# .github/workflows/db-migration-check.yml
- name: Check migrations safety
run: |
squawk db/migrations/*.sql \
--exclude=prefer-text-field \
--pg-version=14.0
Squawk поймает ADD COLUMN NOT NULL без DEFAULT, DROP INDEX без CONCURRENTLY, ALTER TABLE RENAME и другие операции, требующие AccessExclusiveLock. По сравнению с ручным ревью, Squawk находит 90% проблем в 10 раз быстрее.
Для автоматического тестирования самой миграции — поднять копию production БД (через pg_dump/restore или логическую репликацию) и прогнать миграцию там перед применением на prod.
Чек-лист для безопасной миграции
- Проверить, что нет NOT NULL без DEFAULT в Expand
- Убедиться, что уникальные индексы создаются CONCURRENTLY и нет дублей
- Для FK использовать NOT VALID + отдельная валидация
- Запустить squawk перед деплоем
- Протестировать на копии production
- Подготовить скрипты отката
| Проверка | Чем опасна | Как детектит squawk |
|---|---|---|
| ADD COLUMN NOT NULL без DEFAULT | Блокирует таблицу на всё время добавления | ✅ |
| DROP INDEX без CONCURRENTLY | Блокирует таблицу | ✅ |
| ALTER TABLE RENAME | Требует AccessExclusiveLock | ✅ |
| ADD FOREIGN KEY без NOT VALID | Сканирует таблицу под ShareRowExclusiveLock | ⚠️ (требует дополнительного анализа) |
Что входит в нашу работу
При заказе blue-green миграции мы предоставляем:
- Аудит текущей схемы — выявление узких мест, зависимостей, размеров таблиц.
- План миграции — декомпозиция на фазы Expand-Contract с временными оценками.
- Реализация миграций — написание SQL-скриптов, бэкфилл-процедур, скриптов отката.
- Тестирование — прогон на staging с объёмом, сопоставимым с production.
- Документация — описание процесса, runbook для операторов, postmortem-шаблон.
- Поддержка после деплоя — мониторинг ошибок, наблюдение за метриками базы данных.
Процесс реализации
| Этап | Содержание | Срок |
|---|---|---|
| Аудит схемы | Анализ текущих таблиц, размеров, типов изменений | 1 день |
| Проектирование | Декомпозиция изменений на фазы Expand-Contract | 1-2 дня |
| Реализация | Написание миграций, бэкфилл-скриптов | 2-5 дней |
| Тестирование | Прогон на staging с production-объёмами данных | 1-2 дня |
| Деплой | Поэтапный rollout по фазам | По расписанию |
Для простых изменений (добавление nullable столбца, новый индекс) — 2-3 дня с тестированием. Для сложных переструктурирований таблиц с десятками миллионов строк — до 2-3 недель включая бэкфилл и мониторинг.
Оценим ваш проект за один рабочий день. Напишите нам — подберём оптимальный план миграции с гарантией zero-downtime. Наш опыт — десятки успешных миграций на PostgreSQL в highload-проектах. Свяжитесь с нами, чтобы обсудить вашу задачу.
Согласно документации PostgreSQL, ALTER TABLE с NOT VALID минимизирует блокировки (PostgreSQL Documentation).







