← Назад к вопросам
Что произойдёт, если в системе разные версии миграций?
1.0 Junior🔥 91 комментариев
#DevOps и инфраструктура#Django
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Разные версии миграций в системе: синхронизация схемы БД
Это один из самых сложных и критичных вопросов в разработке, особенно в teams с непрерывным развёртыванием. За 10+ лет работы я видел множество сценариев, когда несогласованность миграций ломала production. Давайте разберём что происходит и как избежать проблем.
Сценарий: разные версии миграций на разных серверах
Производство (3 сервера):
Сервер 1: Миграции 001-010 применены ✓
Сервер 2: Миграции 001-009 применены ✓
Сервер 3: Миграции 001-012 применены ✓
↓
КОНФЛИКТ!
✗
Проблемы:
-
Несогласованность данных
- Сервер 3 имеет новые колонки в таблице, которых нет на 1 и 2
- Код пытается писать в новую колонку на сервере 1 → ошибка
-
Неправильное поведение приложения
- На сервере 2 нет нужного индекса → запросы медленные
- На сервере 3 есть индекс → запросы быстрые
-
Откат может быть невозможен
- Миграция 012 на сервере 3 удалила колонку
- На сервере 1 это колонка всё ещё используется
Что происходит на практике
# models.py на production
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String)
email = Column(String)
phone = Column(String) # ← Добавили новую колонку в миграции 012
# Но на Сервере 2 миграция 012 ещё не применена
# Таблица users НЕ имеет колонку phone
# Код пытается создать пользователя:
user = User(name="John", email="john@example.com", phone="+1234567890")
session.add(user)
session.commit()
# Результат на Сервере 2:
# Column "users.phone" does not exist
# ❌ ОШИБКА 500
Разные типы конфликтов миграций
1. Опережающая версия (некоторые серверы впереди)
Сервер A: миграции 001-015
Сервер B: миграции 001-010
Код на обоих серверах: версия 1.5
Версия 1.5 кода ожидает наличие новой колонки (из миграции 012)
→ На сервере B: ошибка, колонки нет
2. Отставание версии (некоторые серверы отстают)
Сервер A: миграции 001-010
Сервер B: миграции 001-015
Код на сервере A: версия 1.5 (ожидает новые таблицы)
Код на сервере B: версия 1.5 (знает про новые таблицы)
→ На сервере A: ошибка, новых таблиц нет
3. Конфликтующие миграции
Ветка "feature-1" создала миграцию 010:
- Добавила колонку "old_field"
Ветка "feature-2" создала миграцию 010 (тот же номер!):
- Добавила колонку "new_field"
При merge'е: КОНФЛИКТ!
Какую миграцию применить?
Решение 1: Blue-Green Development (рекомендуется)
Идея: Миграции ВСЕГДА совместимы с предыдущей версией кода.
Этап 1: Развёртывание миграции
- Добавляем новую колонку (но код её НЕ использует)
- Развёртываем только миграцию
- Все серверы применяют миграцию
Этап 2: Развёртывание кода
- Обновляем код (теперь он использует новую колонку)
- Развёртываем код на все серверы
- Код и БД синхронизированы
Пример:
# Миграция 012 (этап 1): добавить колонку
# migrations/012_add_phone_column.sql
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
ALTER TABLE users ALTER COLUMN phone SET DEFAULT 'N/A';
# Старый код НЕ использует phone
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String)
email = Column(String)
# phone не упоминается
# После того, как миграция на всех серверах:
# Обновляем код (этап 2)
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String)
email = Column(String)
phone = Column(String, nullable=True) # Теперь используем
# Развёртываем новый код
# Всё работает, потому что колонка уже есть в БД
Решение 2: Управление миграциями (с Alembic/Goose)
# Структура миграций:
migrations/
├── 0001_initial_schema.sql
├── 0002_add_users_table.sql
├── 0003_add_posts_table.sql
├── 0004_add_phone_column.sql
├── 0005_create_index.sql
└── migrations_history.txt
# migrations_history.txt отслеживает какие миграции применены
-- 0004_add_phone_column.sql
-- +migrate Up
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
-- +migrate Down
ALTER TABLE users DROP COLUMN phone;
# Команды
goose up # Применить все невыполненные миграции
goose down # Откатить последнюю миграцию
goose status # Показать текущее состояние
Решение 3: Semantic Versioning для развёртывания
Версия: 1.5.2
↓ major (breaking changes в API)
↓ minor (новые features, backward compatible)
↓ patch (багфиксы)
Rules:
- Миграции совместимы with текущей версией БД
- Новые миграции идут с новым кодом
- Развёртывание: миграции → код → миграции отката (на случай emergency)
Реальный пример проблемы: production incident
# 14:00 - Deploy версии 2.0 на сервер A
# Включает миграцию: удаление колонки "legacy_field"
# Сервер A:
# Миграция выполнена ✓
# Колонка "legacy_field" удалена
# Версия кода: 2.0 (код не использует legacy_field)
# 14:05 - Сервер B всё ещё версия 1.9
# Старый код пытается читать legacy_field
user = session.query(User).filter(User.legacy_field == "old").first()
# ❌ Column "legacy_field" does not exist
# 14:10 - Load Balancer направляет запрос на Сервер B
# ERROR 500!
# 14:15 - Быстрый откат миграции на Сервере A
# ALTER TABLE users ADD COLUMN legacy_field VARCHAR(100);
# Заполняем значения из архива...
# Операция заняла 15 минут (на большой таблице)
# Итого downtime: 15 минут
Как избежать этой проблемы
Процесс развёртывания:
1. Создаёшь миграцию, которая ДОБАВЛЯЕТ новую колонку
2. Обновляешь код, но сначала он НЕ использует колонку
3. Развёртываешь миграцию на production (все серверы БД)
4. Проверяешь что миграция успешна на всех серверах
5. Развёртываешь новый код (все серверы приложения)
6. Проверяешь что код использует новую колонку корректно
7. Если нужно откатить колонку:
- Сначала откатываешь код (без использования колонки)
- Потом откатываешь миграцию (удаляешь колонку)
Инструменты для управления миграциями
# Python Alembic (для SQLAlchemy)
from alembic import op
import sqlalchemy as sa
def upgrade():
op.add_column('users', sa.Column('phone', sa.String(20)))
def downgrade():
op.drop_column('users', 'phone')
# Команды
alembic upgrade head # До последней миграции
alembic downgrade -1 # На одну версию назад
alembic current # Текущая версия
# Goose (general purpose)
goose create add_phone_column sql # Создаёт файл
goose up # Применяет
goose down # Откатывает
Проверка сонхронизации миграций
# health_check.py
import os
import subprocess
from sqlalchemy import text
def check_migrations():
# Получаем текущую версию миграций из БД
current_version = get_db_version()
# Получаем ожидаемую версию из кода
expected_version = get_code_version()
if current_version != expected_version:
return False, f"Migration mismatch: {current_version} vs {expected_version}"
return True, "OK"
# API endpoint для мониторинга
from fastapi import FastAPI
app = FastAPI()
@app.get("/health")
def health():
ok, msg = check_migrations()
if not ok:
return {"status": "unhealthy", "error": msg}, 503
return {"status": "healthy"}, 200
Лучшие практики
✅ ДА:
- Каждая миграция имеет уникальный номер/timestamp
- Миграции идемпотентны (можно применять несколько раз)
- Откаты миграций работают корректно
- Код совместим с предыдущей версией БД на время развёртывания
- Миграции проверяются в CI/CD перед продакшеном
- Есть мониторинг версии миграций в production
❌ НЕ:
- Ручное изменение схемы БД без миграций
- Удаление старых миграций из истории
- Создание миграций с одинаковыми номерами
- Непроверенные откаты миграций
- Развёртывание кода и миграций в разное время
Заключение
Разные версии миграций на разных серверах это критичная проблема, которая может привести к:
- ❌ Ошибкам 500 в production
- ❌ Потере данных
- ❌ Долгому downtime'у
- ❌ Проблемам при откате
Решение:
- Blue-Green Development: Миграции → Код → Проверка
- Версионирование: Каждой миграции уникальный номер
- Мониторинг: Проверять версию миграций при health check
- CI/CD: Автоматически проверять и применять миграции
- Откаты: Всегда иметь возможность откатить миграцию
Опытные разработчики знают, что миграции БД — это критичный компонент системы, требующий серьёзного подхода и планомерного развёртывания.