С какими проблемами можно столкнуться при миграции нескольких версий данных
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Проблемы при миграции нескольких версий данных
Миграция данных — это один из самых опасных процессов в разработке. Рассмотрим основные проблемы и способы их решения.
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
- Always backup - сначала backup, потом everything
- Test migrations on clone - на копии БД, не на боевой
- Use transaction wrappers - транзакции для безопасности
- Batch updates - не обновляй миллионы строк одним запросом
- Monitor performance - следи за временем и блокировками
- Backward compatible - старый код должен работать с новой схемой
- Document everything - причины, сроки, откат
- Have rollback plan - знай как откатить за 5 минут
- Gradual rollout - развёртывай на 10% -> 50% -> 100%
- Version your schema - храни версию в коде и БД
Миграция данных — это не что-то, что делается на скорую руку. Каждый шаг должен быть тщательно спланирован и протестирован.