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

Проводил ли миграцию данных в крупных сервисах

1.8 Middle🔥 181 комментариев
#Другое

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

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

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

Проводил ли миграцию данных в крупных сервисах

Мой опыт с миграциями

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

Типичный сценарий: Миграция хранилища

Задача

Переместить 500 млн записей из MySQL на PostgreSQL с минимальным downtime, сохраняя целостность данных и обеспечивая возможность откати.

Подход: Dual-Write с верификацией

// 1. Фаза 1: Dual-Write (одновременная запись в оба хранилища)
public class DualWriteUserRepository implements UserRepository {
    private final UserRepository mysqlRepository;
    private final UserRepository postgresRepository;
    private final MigrationLogger migrationLogger;
    
    @Override
    public void saveUser(User user) {
        // Сначала сохраняем в основное хранилище
        mysqlRepository.saveUser(user);
        
        // Затем в новое (асинхронно)
        try {
            postgresRepository.saveUser(user);
            migrationLogger.logSuccess(user.getId(), "save");
        } catch (Exception e) {
            migrationLogger.logError(user.getId(), "save", e);
            // Критично — логируем ошибку для последующей обработки
            alertOpsTeam(e);
        }
    }
    
    @Override
    public User getUserById(Long id) {
        // На время миграции читаем из основного хранилища
        return mysqlRepository.getUserById(id);
    }
}

// 2. Batch-миграция исторических данных
public class DataMigrationJob {
    private final UserRepository sourceRepository;
    private final UserRepository targetRepository;
    private static final int BATCH_SIZE = 1000;
    
    public void migrateHistoricalData() {
        long totalProcessed = 0;
        long offset = 0;
        
        while (true) {
            // Читаем батч из источника
            List<User> batch = sourceRepository.findWithOffsetAndLimit(offset, BATCH_SIZE);
            
            if (batch.isEmpty()) {
                break;
            }
            
            // Трансформация при необходимости
            List<User> transformedBatch = transformBatch(batch);
            
            // Пишем в целевое хранилище
            targetRepository.saveAll(transformedBatch);
            
            // Логируем и верифицируем
            verifyBatch(batch, transformedBatch);
            
            offset += BATCH_SIZE;
            totalProcessed += batch.size();
            
            // Логируем прогресс каждый батч
            System.out.println(String.format(
                "Migrated: %d / 500000000 (%.2f%%)",
                totalProcessed,
                (totalProcessed / 500000000.0) * 100
            ));
            
            // Небольшая задержка для снижения нагрузки
            Thread.sleep(100);
        }
    }
    
    private void verifyBatch(List<User> source, List<User> target) {
        for (int i = 0; i < source.size(); i++) {
            User sourceUser = source.get(i);
            User targetUser = target.get(i);
            
            if (!verifyUserIntegrity(sourceUser, targetUser)) {
                throw new MigrationIntegrityException(
                    "Integrity check failed for user " + sourceUser.getId()
                );
            }
        }
    }
}

// 3. Фаза 2: Dual-Read (переключение на чтение из нового хранилища)
public class SwitchableUserRepository implements UserRepository {
    private final UserRepository mysqlRepository;
    private final UserRepository postgresRepository;
    private final FeatureFlagService featureFlags;
    
    @Override
    public User getUserById(Long id) {
        // Если флаг включен, читаем из новой БД
        if (featureFlags.isEnabled("read_from_postgres")) {
            User postgresUser = postgresRepository.getUserById(id);
            
            // Параллельно читаем из старой БД для верификации
            User mysqlUser = mysqlRepository.getUserById(id);
            
            // Сравниваем результаты
            if (!postgresUser.equals(mysqlUser)) {
                alertOpsTeam("Data mismatch for user " + id);
            }
            
            return postgresUser;
        }
        
        return mysqlRepository.getUserById(id);
    }
}

// 4. Откат (rollback)
public class MigrationRollback {
    public void rollback() {
        // Переключаемся обратно на чтение из MySQL
        featureFlags.disable("read_from_postgres");
        
        // Останавливаем dual-write
        featureFlags.disable("dual_write_postgres");
        
        // Ждём завершения in-flight операций
        waitForOperationsToComplete();
        
        // Логируем откат
        auditLog.info("Migration rolled back successfully");
    }
}

Практический пример: Изменение структуры данных

// Миграция: Разделение таблицы User на User и UserProfile

public class UserProfileMigrationJob {
    private final JdbcTemplate jdbc;
    
    public void migrateUserProfiles() {
        // Шаг 1: Создаём новую таблицу
        jdbc.execute(
            "CREATE TABLE user_profiles (" +
            "  id BIGINT PRIMARY KEY," +
            "  user_id BIGINT REFERENCES users(id)," +
            "  bio TEXT," +
            "  avatar_url VARCHAR(255)," +
            "  created_at TIMESTAMP" +
            ")"
        );
        
        // Шаг 2: Мигрируем данные батчами
        final int BATCH_SIZE = 5000;
        long offset = 0;
        
        while (true) {
            List<Map<String, Object>> rows = jdbc.queryForList(
                "SELECT id, bio, avatar_url FROM users LIMIT ? OFFSET ?",
                BATCH_SIZE, offset
            );
            
            if (rows.isEmpty()) break;
            
            // Вставляем в новую таблицу
            List<Object[]> batchArgs = new ArrayList<>();
            for (Map<String, Object> row : rows) {
                batchArgs.add(new Object[]{
                    row.get("id"),
                    row.get("id"),  // user_id
                    row.get("bio"),
                    row.get("avatar_url"),
                    new Timestamp(System.currentTimeMillis())
                });
            }
            
            jdbc.batchUpdate(
                "INSERT INTO user_profiles (id, user_id, bio, avatar_url, created_at) VALUES (?, ?, ?, ?, ?)",
                batchArgs
            );
            
            offset += BATCH_SIZE;
        }
        
        // Шаг 3: Добавляем внешний ключ
        jdbc.execute(
            "ALTER TABLE user_profiles ADD CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES users(id)"
        );
    }
}

Ключевые паттерны миграций

1. Feature Flags для контроля

public class MigrationFeatureFlags {
    private final FeatureFlagService flags;
    
    public boolean shouldUseMigratedData() {
        return flags.isEnabledForUser("use_new_storage", getCurrentUserId());
    }
    
    public boolean shouldPerformDualWrite() {
        return flags.isEnabled("dual_write_enabled");
    }
}

2. Verifikация данных

public class DataVerification {
    public long findDiscrepancies() {
        // SELECT COUNT(*) WHERE source_data != target_data
        return jdbc.queryForObject(
            "SELECT COUNT(*) FROM users u1 " +
            "JOIN postgres_users u2 ON u1.id = u2.id " +
            "WHERE u1.email != u2.email OR u1.name != u2.name",
            Long.class
        );
    }
}

3. Мониторинг миграции

public class MigrationMetrics {
    private final MeterRegistry meterRegistry;
    
    public void recordMigrationProgress(long processedRecords, long totalRecords) {
        meterRegistry.gauge("migration.progress.percent",
            (processedRecords * 100.0) / totalRecords);
        
        meterRegistry.counter("migration.records.processed", "count", processedRecords);
    }
}

Основные риски и как их избежать

  1. Data Loss — батчи с верификацией, транзакции
  2. Performance Impact — асинхронные операции, rate limiting, batch processing
  3. Rollback Complexity — feature flags, dual-write стратегия
  4. Стейл данные — верификация, eventual consistency подход
  5. Downtime — dual-write/dual-read вместо простого перезапуска

Типичная timeline для крупной миграции

  • День 1-2: Подготовка, создание target хранилища
  • День 3-7: Batch-миграция исторических данных
  • День 8-14: Dual-write фаза, верификация
  • День 15: Переключение на dual-read
  • День 16-30: Мониторинг и сравнение
  • День 31: Отключение старого хранилища

Ключ к успеху миграций — это планирование, механизмы отката и постоянная верификация данных на каждом этапе.

Проводил ли миграцию данных в крупных сервисах | PrepBro