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

Как вставить Disaster Recovery в систему устойчивости базы данных

3.0 Senior🔥 71 комментариев
#Docker, Kubernetes и DevOps#Базы данных и SQL

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

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

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

# Как вставить Disaster Recovery в систему устойчивости базы данных

Этот вопрос касается архитектуры высокодоступных систем и восстановления после сбоев. Давайте разберемся в механизмах Disaster Recovery (DR).

Что такое Disaster Recovery (DR)?

Disaster Recovery — это набор процессов и технологий для восстановления системы после катастрофического сбоя:

RPO (Recovery Point Objective) = максимальный объем потерянных данных
RTO (Recovery Time Objective) = максимальное время на восстановление

Примеры:
- RPO = 1 час (потеряем максимум 1 час данных)
- RTO = 15 минут (восстановим систему за 15 минут)

Уровни Disaster Recovery

Уровень 1: Резервные копии (Backup)

// Простая схема: регулярное копирование БД

@Component
public class DatabaseBackupService {
    @Autowired
    private DataSource dataSource;
    
    @Autowired
    private StorageService storageService; // S3, GCS, etc.
    
    @Scheduled(cron = "0 0 * * *") // Каждый день в 00:00
    public void performDailyBackup() {
        try {
            // Создать dump всей БД
            String backupFile = createDatabaseDump();
            
            // Загрузить на удаленное хранилище
            storageService.upload(
                backupFile,
                "backups/daily-" + LocalDate.now() + ".sql.gz"
            );
            
            log.info("Daily backup completed successfully");
        } catch (Exception e) {
            sendAlert("Backup failed: " + e.getMessage());
        }
    }
    
    private String createDatabaseDump() throws Exception {
        // Использовать mysqldump или pg_dump
        Process process = Runtime.getRuntime().exec(
            "mysqldump --all-databases > backup.sql"
        );
        process.waitFor();
        return "backup.sql";
    }
    
    public void restoreFromBackup(String backupFile) throws Exception {
        String sql = storageService.download(backupFile);
        
        try (Connection conn = dataSource.getConnection();
             Statement stmt = conn.createStatement()) {
            // Выполнить SQL из backup'а
            stmt.executeUpdate(sql);
            log.info("Database restored from: " + backupFile);
        }
    }
}

// Недостатки:
// - RPO = 1 день (потеряем все данные за день)
// - RTO = несколько часов (ручное восстановление)

Уровень 2: Логические резервные копии + WAL Archiving

// Более частое копирование + логирование транзакций

@Configuration
public class PostgresWALArchivingConfig {
    
    // В postgresql.conf:
    // wal_level = replica
    // archive_mode = on
    // archive_command = 'test ! -f "/backup/wal/%f" && cp "/var/lib/postgresql/wal/%f" "/backup/wal/%f"'
    
    // Результат:
    // - Каждые 16MB данных логируются в WAL
    // - WAL архивируется на удаленное хранилище
    // - RPO = несколько минут
    // - RTO = 30 минут
}

// Java-side monitoring
@Component
public class WALArchiveMonitor {
    @Scheduled(fixedRate = 60000) // Каждую минуту
    public void checkWALArchiveHealth() {
        long unachedWALFiles = getUnarchivedWALFileCount();
        
        if (unachedWALFiles > 10) {
            sendAlert("WAL archive falling behind: " + unachedWALFiles);
        }
    }
}

Уровень 3: Репликация (Replication)

// Активная репликация на вторичную БД

// PostgreSQL streaming replication:
// Primary DB (Production)
//     ↓ (WAL stream)
// Secondary DB (Standby) - read-only копия
//     ↓ (failover)
// Tertiary DB (Backup)

@Configuration
public class DatabaseReplicationConfig {
    
    @Bean
    public DataSource primaryDataSource() {
        // Основная база данных для write'ов
        return DataSourceBuilder.create()
            .url("jdbc:postgresql://primary-db:5432/myapp")
            .username("postgres")
            .password("password")
            .build();
    }
    
    @Bean
    public DataSource readReplicaDataSource() {
        // Read-only копия для read-only операций
        return DataSourceBuilder.create()
            .url("jdbc:postgresql://replica-db:5432/myapp")
            .username("postgres")
            .password("password")
            .build();
    }
}

@Service
public class UserService {
    @Autowired
    private DataSource primaryDataSource;    // WRITE
    
    @Autowired
    private DataSource readReplicaDataSource; // READ
    
    @Transactional
    public User createUser(UserDTO dto) {
        // Пишем в primary
        try (Connection conn = primaryDataSource.getConnection()) {
            insertUser(conn, dto);
        }
    }
    
    @Transactional(readOnly = true)
    public List<User> getAllUsers() {
        // Читаем из replica (может быть задержка на несколько ms)
        try (Connection conn = readReplicaDataSource.getConnection()) {
            return queryUsers(conn);
        }
    }
}

// Преимущества:
// - RPO = несколько секунд
// - RTO = 1-5 минут (failover)
// - Масштабируемость на чтение (replica может обработать 10000 read'ов)

Уровень 4: Multi-Region / Multi-DC (Master-Master Replication)

// Активная репликация между несколькими data center'ами

// DC1 (US East)
//  └─ Primary DB ←→ Primary DB (DC2, US West)
//     Quorum commit

@Configuration
public class MultiRegionReplicationConfig {
    
    @Bean
    public DataSource usEastDataSource() {
        return DataSourceBuilder.create()
            .url("jdbc:postgresql://us-east-db:5432/myapp")
            .build();
    }
    
    @Bean
    public DataSource usWestDataSource() {
        return DataSourceBuilder.create()
            .url("jdbc:postgresql://us-west-db:5432/myapp")
            .build();
    }
}

@Service
public class MultiRegionDataService {
    @Autowired
    private DataSource usEastDataSource;
    
    @Autowired
    private DataSource usWestDataSource;
    
    public void saveWithQuorum(User user) {
        // Quorum commit: требует одобрения от большинства DC'ов
        int successCount = 0;
        
        try {
            if (saveToRegion(usEastDataSource, user)) successCount++;
            if (saveToRegion(usWestDataSource, user)) successCount++;
            
            // Требуем 2 из 2 успешных (или 2 из 3 если есть третий)
            if (successCount < 2) {
                throw new QuorumCommitFailedException();
            }
        } catch (Exception e) {
            log.error("Quorum commit failed", e);
            throw new RuntimeException(e);
        }
    }
    
    private boolean saveToRegion(DataSource ds, User user) {
        try (Connection conn = ds.getConnection()) {
            // Вставить user
            return true;
        } catch (Exception e) {
            log.error("Failed to save to region", e);
            return false;
        }
    }
}

// Преимущества:
// - RPO = 0 (zero data loss)
// - RTO = 0 (instantaneous failover)
// - Может работать при потере одного DC

Failover Strategy

@Component
public class FailoverManager {
    
    @Autowired
    private PrimaryDatabaseHealthChecker healthChecker;
    
    @Autowired
    private LoadBalancer loadBalancer;
    
    // Проверяем health primary DB каждые 10 секунд
    @Scheduled(fixedRate = 10000)
    public void checkPrimaryHealth() {
        if (!healthChecker.isPrimaryHealthy()) {
            log.error("Primary DB is down! Initiating failover...");
            triggerFailover();
        }
    }
    
    private void triggerFailover() {
        try {
            // 1. Убедиться, что replica синхронизирована
            if (!isReplicaSynchronized()) {
                log.warn("Replica not fully synchronized, some data may be lost");
            }
            
            // 2. Promote replica to primary
            promoteReplicaToPrimary();
            
            // 3. Переключить traffic на новый primary
            loadBalancer.switchToPrimary("replica-server");
            
            // 4. Уведомить Ops team
            notifyOpsTeam("Failover completed. Check replication status.");
            
        } catch (Exception e) {
            log.error("Failover failed!", e);
            sendCriticalAlert("CRITICAL: Failover mechanism failed");
        }
    }
    
    private boolean isReplicaSynchronized() {
        // Проверить replication lag
        long replicationLagMs = getReplicationLag();
        return replicationLagMs < 5000; // Максимум 5 секунд задержки
    }
}

// Health check реализация
@Component
public class PrimaryDatabaseHealthChecker {
    @Autowired
    private DataSource primaryDataSource;
    
    public boolean isPrimaryHealthy() {
        try (Connection conn = primaryDataSource.getConnection()) {
            // Выполнить простой query
            try (Statement stmt = conn.createStatement()) {
                stmt.execute("SELECT 1");
                return true;
            }
        } catch (SQLException e) {
            log.error("Primary DB health check failed", e);
            return false;
        }
    }
}

Мониторинг и Alerting

@Component
public class DisasterRecoveryMonitoring {
    
    @Autowired
    private MetricsRegistry metricsRegistry;
    
    @Scheduled(fixedRate = 60000) // Каждую минуту
    public void monitorDisasterRecoveryHealth() {
        // Метрика 1: Replication lag
        long replicationLagMs = getReplicationLag();
        metricsRegistry.gauge("database.replication.lag.ms", replicationLagMs);
        
        if (replicationLagMs > 30000) {
            sendAlert("WARNING: Replication lag > 30 seconds");
        }
        
        // Метрика 2: Backup freshness
        long timeSinceLastBackup = getTimeSinceLastBackup();
        metricsRegistry.gauge("database.backup.age.ms", timeSinceLastBackup);
        
        if (timeSinceLastBackup > 86400000) { // > 1 дня
            sendAlert("WARNING: Backup is > 1 day old");
        }
        
        // Метрика 3: WAL archive lag
        long walArchiveLagFiles = getUnarchivedWALFiles();
        metricsRegistry.gauge("database.wal.archive.lag.files", walArchiveLagFiles);
        
        if (walArchiveLagFiles > 100) {
            sendAlert("WARNING: " + walArchiveLagFiles + " WAL files not archived");
        }
        
        // Метрика 4: RTO и RPO
        metricsRegistry.gauge("disaster.recovery.rto.minutes", calculateRTO());
        metricsRegistry.gauge("disaster.recovery.rpo.minutes", calculateRPO());
    }
}

Практический Disaster Recovery Plan

1. TIER 1 - LOCAL BACKUP (RPO: 24h, RTO: 4h)
   - mysqldump / pg_dump раз в день на S3
   - Хранить 30 дней
   - Тестировать восстановление раз в месяц

2. TIER 2 - STREAMING REPLICATION (RPO: 5min, RTO: 5min)
   - Replica на другой машине в том же DC
   - Automated failover
   - Непрерывное копирование WAL

3. TIER 3 - GEO-REPLICATION (RPO: 1min, RTO: 1min)
   - Async replication в другой регион
   - Manual failover для критичных сбоев

4. TIER 4 - MULTI-MASTER (RPO: 0, RTO: 0)
   - Активная репликация в 2-3 регионах
   - Quorum commit для consistency
   - Автоматический failover

Тестирование DR

@SpringBootTest
public class DisasterRecoveryTest {
    
    @Test
    public void testBackupRestoration() throws Exception {
        // 1. Создать backup
        String backupFile = backupService.createBackup();
        
        // 2. Удалить тестовые данные
        testDataService.deleteAllTestData();
        
        // 3. Восстановить из backup'а
        backupService.restoreFromBackup(backupFile);
        
        // 4. Проверить что данные восстановлены
        List<User> users = userRepository.findAll();
        assertThat(users).isNotEmpty();
    }
    
    @Test
    public void testReplicationLag() throws Exception {
        User user = new User("John Doe");
        userRepository.save(user);
        
        // Подождать пока replica синхронизируется
        Thread.sleep(2000);
        
        // Проверить что replica получила данные
        User replicatedUser = replicaUserRepository.findById(user.getId());
        assertThat(replicatedUser).isNotNull();
    }
    
    @Test
    public void testFailover() throws Exception {
        // 1. Отключить primary
        primaryDatabase.shutdown();
        
        // 2. Инициировать failover
        failoverManager.triggerFailover();
        
        // 3. Проверить что система работает
        User user = new User("Test User");
        User saved = userService.createUser(user);
        assertThat(saved.getId()).isNotNull();
    }
}

Заключение

Для полной disaster recovery системы нужно:

Tier 1: Резервные копии (backup/restore) ✅ Tier 2: Репликация (RPO/RTO в минутах) ✅ Tier 3: Geo-репликация (RPO/RTO в секундах) ✅ Tier 4: Multi-master (RPO = 0, RTO = 0)

✅ Мониторинг replication lag и backup freshness ✅ Автоматические failover'ы ✅ Регулярное тестирование восстановления ✅ Документирование RTO/RPO для каждого tier'а

Выбор tier'а зависит от критичности данных и бюджета на инфраструктуру!

Как вставить Disaster Recovery в систему устойчивости базы данных | PrepBro