← Назад к вопросам

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

1.0 Junior🔥 231 комментариев
#DevOps и инфраструктура#Django

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

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Проблемы при миграции нескольких версий данных

Миграция данных — это один из самых опасных процессов в разработке. Рассмотрим основные проблемы и способы их решения.

1. Data Loss (Потеря данных)

Проблема: Самая критичная — просто потеря информации.

# НЕПРАВИЛЬНО: Удаляем старое поле без миграции
ALTER TABLE users DROP COLUMN phone;

# Если никто не скопировал phone в новое место,
# все номера телефонов потеряны навсегда

Решение:

-- Миграция 1: Добавляем новое поле
ALTER TABLE users ADD COLUMN phone_backup TEXT;

-- Копируем данные
UPDATE users SET phone_backup = phone;

-- Миграция 2 (позже, после проверки): Удаляем старое
ALTER TABLE users DROP COLUMN phone;

-- Переименовываем
ALTER TABLE users RENAME COLUMN phone_backup TO phone;

Best Practice:

# Всегда создавайте backup перед миграцией
import subprocess
from datetime import datetime

def backup_database():
    timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
    backup_file = f'backup_{timestamp}.sql'
    
    subprocess.run([
        'pg_dump',
        'production_db',
        '-f', backup_file
    ])
    
    return backup_file

print(f"Backup created: {backup_database()}")

2. Corruption (Повреждение данных)

Проблема: Данные остаются, но становятся невалидными.

# НЕПРАВИЛЬНО: Изменяем тип без преобразования
# Было: email VARCHAR(50)
# Стало: email UUID

# Старые значения 'john@example.com' не могут быть UUID

Решение:

-- Миграция 1: Добавляем новое поле
ALTER TABLE users ADD COLUMN email_uuid UUID;

-- Миграция 2: Трансформируем данные
UPDATE users 
SET email_uuid = (gen_random_uuid())
WHERE email IS NOT NULL;

-- Миграция 3: Проверяем результаты
SELECT COUNT(*) as orphaned 
FROM users 
WHERE email IS NOT NULL AND email_uuid IS NULL;

-- Если orphaned > 0, откатываем!

-- Миграция 4: Удаляем старое поле
ALTER TABLE users DROP COLUMN email;
ALTER TABLE users RENAME COLUMN email_uuid TO email;

3. Downtime (Простой)

Проблема: Большие миграции блокируют таблицы.

-- НЕПРАВИЛЬНО: Блокирует таблицу на часы
ALTER TABLE users ADD COLUMN full_name TEXT;
UPDATE users SET full_name = CONCAT(first_name, ' ', last_name);

-- На больших таблицах это может занять часы и заблокировать доступ!

Решение: Zero-Downtime Migration

-- Миграция 1: Добавляем новое поле с дефолтом
ALTER TABLE users ADD COLUMN full_name TEXT DEFAULT '';

-- Миграция 2: Батчевое обновление в background
BEGIN;

WITH batch AS (
    SELECT id 
    FROM users 
    WHERE full_name = '' 
    LIMIT 10000
)
UPDATE users 
SET full_name = CONCAT(first_name, ' ', last_name)
WHERE id IN (SELECT id FROM batch);

COMMIT;

-- Повторяем Миграцию 2 несколько раз в background job

В Python с батчами:

from django.db.models import Q

def migrate_user_data_in_batches(batch_size=10000):
    """
    Миграция данных малыми батчами
    Не блокирует таблицу
    """
    qs = User.objects.filter(full_name='')
    total = qs.count()
    
    for i in range(0, total, batch_size):
        batch = qs[i:i + batch_size]
        
        # Обновляем батч
        for user in batch:
            user.full_name = f"{user.first_name} {user.last_name}"
        
        User.objects.bulk_update(batch, ['full_name'], batch_size)
        
        print(f"Processed {i + batch_size} of {total}")
        
        # Даём БД отдохнуть
        time.sleep(0.5)

4. Inconsistent State (Несогласованное состояние)

Проблема: Приложение работает с неполными данными.

# НЕПРАВИЛЬНО: Код ожидает новое поле, а оно ещё не заполнено

class UserSerializer:
    def to_representation(self, instance):
        return {
            'id': instance.id,
            'name': instance.full_name  # А что если NULL?
        }

# После миграции, если full_name = NULL для некоторых,
# сериализатор выломается

Решение: Dual-Write Pattern

# Этап 1: Пишем в оба поля
class User(models.Model):
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100)
    full_name = models.CharField(max_length=200, null=True, default='')
    
    def save(self, *args, **kwargs):
        # Заполняем новое поле автоматически
        if not self.full_name:
            self.full_name = f"{self.first_name} {self.last_name}"
        super().save(*args, **kwargs)

# Этап 2: Миграция исторических данных
for user in User.objects.filter(full_name=''):
    user.full_name = f"{user.first_name} {user.last_name}"
    user.save()

# Этап 3: Перестраиваем код, чтобы использовать только full_name
class UserSerializer:
    def to_representation(self, instance):
        return {'id': instance.id, 'name': instance.full_name}

# Этап 4: Удаляем dual-write
class User(models.Model):
    # Удалили first_name и last_name
    full_name = models.CharField(max_length=200)

5. Rollback Complexity (Сложность отката)

Проблема: Откатывать миграции сложнее, чем их применять.

-- Если миграция добавила и удалила данные,
-- откат может быть невозможен

ALTER TABLE orders DROP COLUMN customer_phone;  -- Данные потеряны!
-- Нет способа их вернуть

Решение: Идемпотентные миграции

-- Миграция вперёд
CREATE TABLE IF NOT EXISTS user_backups AS
SELECT * FROM users;

ALTER TABLE users DROP COLUMN customer_phone;

-- Миграция назад (откат)
CREATE TABLE IF NOT EXISTS users AS
SELECT * FROM user_backups;

DROP TABLE user_backups;

В Goose (используем в проекте):

-- migrations/0001_add_full_name.up.sql
ALTER TABLE users ADD COLUMN full_name TEXT;
UPDATE users SET full_name = CONCAT(first_name, ' ', last_name);

-- migrations/0001_add_full_name.down.sql
ALTER TABLE users DROP COLUMN full_name;

6. Race Conditions (Гонки данных)

Проблема: Приложение пишет данные во время миграции.

# Сценарий:
# 1. Миграция добавляет новое поле
# 2. В это же время приложение пытается прочитать старое поле
# 3. Результат: непредсказуемое поведение

Решение: Graceful Shutdown

import signal
import time
from django.db import connection

class MigrationManager:
    def __init__(self):
        self.is_running = True
    
    def signal_handler(self, sig, frame):
        print("Received shutdown signal")
        self.is_running = False
    
    def run_migration(self):
        signal.signal(signal.SIGTERM, self.signal_handler)
        
        # Даём приложению время завершить текущие запросы
        print("Waiting 30s for active connections...")
        time.sleep(30)
        
        # Закрываем новые подключения
        connection.close()
        
        # Выполняем миграцию
        print("Running migration...")
        
        # Миграция
        from django.core.management import call_command
        call_command('migrate')

7. Large Table Migrations (Миграции больших таблиц)

Проблема: LOCK TIMEOUT, запросы висят.

# НЕПРАВИЛЬНО: Индекс на большую таблицу
ALTER TABLE orders ADD INDEX idx_customer_id (customer_id);
# На 100М записей это может занять часы и заблокировать таблицу!

Решение: CONCURRENTLY в PostgreSQL

-- Без блокировки таблицы
CREATE INDEX CONCURRENTLY idx_customer_id ON orders(customer_id);

-- Можно удалять без блокировки
DROP INDEX CONCURRENTLY idx_customer_id;

В MySQL: использовать gh-ost (GitHub's Online Schema Change)

# Миграция схемы без блокировки
gh-ost \
  --user=root \
  --password=password \
  --host=localhost \
  --database=mydb \
  --table=orders \
  --alter="ADD COLUMN new_field INT" \
  --execute

8. Version Mismatch (Несоответствие версий)

Проблема: Старый код работает с новой схемой БД (или наоборот).

# Сценарий:
# 1. Развернули миграцию БД (версия 2)
# 2. Забыли развернуть новый код приложения
# 3. Приложение v1 читает данные, структура которых изменилась
# 4. KeyError, AttributeError, крах!

Решение: Backward-Compatible Migrations

# Миграция 1: Добавляем новое поле с дефолтом
ALTER TABLE users ADD COLUMN email_verified BOOLEAN DEFAULT FALSE;

# Код приложения остаётся работать (использует дефолт)
user = User.objects.get(id=1)
if hasattr(user, 'email_verified'):  # Может быть не заполнено
    is_verified = user.email_verified
else:
    is_verified = False

# Миграция 2: Заполняем данные
UPDATE users SET email_verified = true WHERE validated_at IS NOT NULL;

# Миграция 3: Делаем поле NOT NULL
ALTER TABLE users ALTER COLUMN email_verified SET NOT NULL;

9. Encoding Issues (Проблемы кодировки)

Проблема: UTF-8 символы повреждаются.

-- НЕПРАВИЛЬНО: Изменяем кодировку БД
ALTER DATABASE mydb CHARACTER SET latin1;
-- Все русские символы станут "????"

Решение:

-- Сначала экспортируем с правильной кодировкой
mysqldump --default-character-set=utf8mb4 -u root -p mydb > backup.sql

-- Пересоздаём БД с UTF-8
ALTER DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- Импортируем
mysql --default-character-set=utf8mb4 -u root -p mydb < backup.sql

10. Transaction Isolation Issues (Проблемы изоляции)

Проблема: Dirty read во время миграции.

# Миграция идет: обновляет данные
UPDATE users SET status = 'active' WHERE created_at > '2024-01-01';

# В это же время приложение читает частичные данные
for user in User.objects.all():  # Некоторые уже обновлены, некоторые нет
    print(user.status)  # Inconsistent data!

Решение:

# Используй транзакции
from django.db import transaction

with transaction.atomic():
    # Все операции либо выполнятся, либо откатятся
    users = User.objects.select_for_update().filter(created_at > '2024-01-01')
    for user in users:
        user.status = 'active'
    User.objects.bulk_update(users, ['status'])

Чеклист перед миграцией

# ✅ Перед запуском миграции:
print("""
1. Создан backup? - Проверить!
2. Миграция протестирована на копии БД? - Проверить!
3. Откат написан и протестирован? - Проверить!
4. Приложение совместимо со схемой БД? - Проверить!
5. Уведомлены пользователи о возможном downtime? - Проверить!
6. Есть план если что-то пойдёт не так? - Проверить!
7. Есть мониторинг для обнаружения проблем? - Проверить!
8. Есть быстрый способ откатить? - Проверить!
""")

Best Practices Summary

  1. Always backup - сначала backup, потом everything
  2. Test migrations on clone - на копии БД, не на боевой
  3. Use transaction wrappers - транзакции для безопасности
  4. Batch updates - не обновляй миллионы строк одним запросом
  5. Monitor performance - следи за временем и блокировками
  6. Backward compatible - старый код должен работать с новой схемой
  7. Document everything - причины, сроки, откат
  8. Have rollback plan - знай как откатить за 5 минут
  9. Gradual rollout - развёртывай на 10% -> 50% -> 100%
  10. Version your schema - храни версию в коде и БД

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

С какими проблемами можно столкнуться при миграции нескольких версий данных | PrepBro