Veritabanı Felaket Kurtarma: MySQL ve PostgreSQL için Kapsamlı Rehber
Yıllar önce bir müşterimin üretim veritabanı sunucusunun disklerinden ikisi aynı anda bozuldu. RAID 5 konfigürasyonu vardı ve tam o sırada yedekler de birkaç haftadır sessizce başarısız oluyordu. Kimse kontrol etmemişti. O geceyi hatırladığımda hala içim sıkışıyor. İşte bu yüzden felaket kurtarma planı yazmak değil, test edilmiş bir felaket kurtarma planına sahip olmak ile kurtarma planı yazmış olmak arasındaki fark, tam anlamıyla işini kaybetmek ile kaybetmemek arasındaki farktır.
Bu yazıda MySQL ve PostgreSQL için gerçekçi, test edilebilir ve otomasyon odaklı bir felaket kurtarma yaklaşımı anlatacağım.
Felaket Kurtarma Planının Temelleri
Felaket kurtarma (Disaster Recovery, DR) planı hazırlamadan önce iki temel metriği netleştirmeniz gerekiyor:
- RPO (Recovery Point Objective): Kaç saatlik/dakikalık veri kaybını tolere edebilirsiniz?
- RTO (Recovery Time Objective): Sistemin ne kadar sürede ayağa kalkması gerekiyor?
Bu iki değer, kullanacağınız yedekleme stratejisini doğrudan belirliyor. RPO = 0 istiyorsanız senkron replikasyon zorunlu. RPO = 1 saat ise saatlik snapshot yeterli olabilir. Bütçe ile risk iştahı arasındaki bu dengeyi kurmadan teknik detaylara girmek anlamsız.
Tipik bir üretim ortamında şu felaket senaryolarına hazırlıklı olmanız gerekiyor:
- Disk arızası veya tam sunucu kaybı
- Yanlışlıkla silinen tablo veya veritabanı (DROP TABLE felaketi)
- Bozuk veri yazılması (uygulama bug’ı veya migration hatası)
- Fidye yazılımı saldırısı
- Veri merkezi düzeyinde erişim kaybı
Her senaryo için kurtarma prosedürü farklı olduğundan, bunları ayrı ayrı ele alacağız.
MySQL İçin Yedekleme Stratejisi
Fiziksel Yedekleme: Percona XtraBackup
Mantıksal yedekler (mysqldump) küçük veritabanları için iş görür ama 100GB+ veritabanlarında restore süresi saatler alabilir. Percona XtraBackup, InnoDB tablolarının hot backup’ını alarak bu sorunu çözer.
#!/bin/bash
# mysql_backup.sh - Tam ve artımlı yedekleme scripti
BACKUP_DIR="/backup/mysql"
DATE=$(date +%Y%m%d_%H%M%S)
FULL_BACKUP_DIR="$BACKUP_DIR/full_$DATE"
INCREMENTAL_DIR="$BACKUP_DIR/inc_$DATE"
LAST_FULL=$(ls -td $BACKUP_DIR/full_* 2>/dev/null | head -1)
# Haftalık tam yedek, günlük artımlı
DAY_OF_WEEK=$(date +%u)
if [ "$DAY_OF_WEEK" -eq 7 ] || [ -z "$LAST_FULL" ]; then
echo "[$(date)] Tam yedekleme basliyor..."
xtrabackup --backup
--target-dir="$FULL_BACKUP_DIR"
--user=backup_user
--password="$MYSQL_BACKUP_PASSWORD"
--parallel=4
--compress
--compress-threads=4
# Yedek dogrulama
if [ $? -eq 0 ]; then
xtrabackup --prepare --target-dir="$FULL_BACKUP_DIR"
echo "[$(date)] Tam yedekleme tamamlandi: $FULL_BACKUP_DIR"
else
echo "[$(date)] HATA: Tam yedekleme basarisiz!" >&2
exit 1
fi
else
echo "[$(date)] Artimli yedekleme basliyor..."
xtrabackup --backup
--target-dir="$INCREMENTAL_DIR"
--incremental-basedir="$LAST_FULL"
--user=backup_user
--password="$MYSQL_BACKUP_PASSWORD"
--compress
--compress-threads=4
if [ $? -eq 0 ]; then
echo "[$(date)] Artimli yedekleme tamamlandi: $INCREMENTAL_DIR"
else
echo "[$(date)] HATA: Artimli yedekleme basarisiz!" >&2
exit 1
fi
fi
# 30 gunden eski yedekleri temizle
find "$BACKUP_DIR" -maxdepth 1 -type d -mtime +30 -exec rm -rf {} ;
Binary Log ile Point-in-Time Recovery
Disk arızası değil de yanlışlıkla silinen veri sorunuyla karşılaştığınızda, full backup yeterli olmaz. MySQL binary log’ları etkinleştirerek belirli bir anın verilerine geri dönebilirsiniz.
# my.cnf yapılandırması
# [mysqld] altına ekleyin
server_id = 1
log_bin = /var/log/mysql/mysql-bin
binlog_format = ROW
binlog_row_image = FULL
expire_logs_days = 14
max_binlog_size = 500M
sync_binlog = 1
innodb_flush_log_at_trx_commit = 1
Diyelim ki sabah 09:15’te birileri yanlışlıkla DROP TABLE orders komutunu çalıştırdı ve siz bunu 10:30’da fark ettiniz. Gece 02:00’da alınmış bir full backup varsa şu adımları izlersiniz:
#!/bin/bash
# point_in_time_recovery.sh
# Kullanim: ./point_in_time_recovery.sh "2024-01-15 09:14:59"
TARGET_TIME="$1"
FULL_BACKUP="/backup/mysql/full_20240115_020000"
BINLOG_DIR="/var/log/mysql"
RESTORE_DIR="/tmp/mysql_restore"
if [ -z "$TARGET_TIME" ]; then
echo "Kullanim: $0 'YYYY-MM-DD HH:MM:SS'"
exit 1
fi
echo "=== Point-in-Time Recovery Basliyor ==="
echo "Hedef zaman: $TARGET_TIME"
# 1. Full backup'i hazirla
mkdir -p "$RESTORE_DIR"
xtrabackup --prepare --target-dir="$FULL_BACKUP"
# 2. Yedegi gecici konuma geri yukle (test ortami)
xtrabackup --copy-back
--target-dir="$FULL_BACKUP"
--datadir="$RESTORE_DIR/data"
# 3. Binary log'lardan olayları calistir (DROP oncesine kadar)
echo "Binary log'lar uygulanıyor..."
mysqlbinlog
--stop-datetime="$TARGET_TIME"
--database=production_db
"$BINLOG_DIR"/mysql-bin.0* |
mysql -u root -p --one-database production_db
echo "=== Kurtarma tamamlandi ==="
echo "Lutfen verileri dogrulayin, sonra production'a tasiyiniz"
PostgreSQL İçin Yedekleme Stratejisi
pg_basebackup ve WAL Arşivleme
PostgreSQL’in WAL (Write-Ahead Log) mekanizması, MySQL’in binary log’una benzer ama daha entegre çalışır. postgresql.conf dosyasında WAL arşivlemeyi açarak continuous archiving kurabilirsiniz.
# postgresql.conf ayarlari
wal_level = replica
archive_mode = on
archive_command = 'test ! -f /backup/wal/%f && cp %p /backup/wal/%f'
archive_timeout = 300
max_wal_senders = 5
wal_keep_size = 1GB
# Anlik yedek almak icin:
# restore_command = 'cp /backup/wal/%f %p'
Base backup alma ve WAL tabanlı kurtarma scripti:
#!/bin/bash
# pg_backup_full.sh
BACKUP_BASE="/backup/postgresql"
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="$BACKUP_BASE/base_$DATE"
PG_DATA="/var/lib/postgresql/14/main"
PGUSER="postgres"
echo "[$(date)] PostgreSQL base backup basliyor..."
pg_basebackup
-D "$BACKUP_DIR"
-U "$PGUSER"
-Xs
-P
-R
--checkpoint=fast
--compress=9
--format=tar
if [ $? -ne 0 ]; then
echo "[$(date)] HATA: Base backup basarisiz!" >&2
# Onceki basarili yedeği kontrol et
LAST_GOOD=$(ls -td $BACKUP_BASE/base_* 2>/dev/null | sed -n '2p')
echo "Son basarili yedek: $LAST_GOOD"
# Alert gonder
echo "PostgreSQL backup BASARISIZ - $(hostname) - $(date)" |
mail -s "KRITIK: Backup Hatasi" [email protected]
exit 1
fi
# Yedek boyutunu logla
BACKUP_SIZE=$(du -sh "$BACKUP_DIR" | cut -f1)
echo "[$(date)] Backup tamamlandi. Boyut: $BACKUP_SIZE, Konum: $BACKUP_DIR"
# WAL dosyalarini temizle (arşivlenmiş olanları)
psql -U "$PGUSER" -c "SELECT pg_switch_wal();"
# Eski base backupları temizle (son 4 hafta tut)
find "$BACKUP_BASE" -maxdepth 1 -name "base_*" -type d -mtime +28 -exec rm -rf {} ;
PostgreSQL Point-in-Time Recovery (PITR)
Gerçek dünya senaryosu: Geliştirici ekibinden biri production’da test migration çalıştırdı ve kritik bir tablonun verilerini bozdu. Olayı fark etme zamanı: 14:45. Yanlış migration zamanı: 14:20.
#!/bin/bash
# pg_pitr_restore.sh
# Kullanim: ./pg_pitr_restore.sh "2024-01-15 14:19:00" /backup/postgresql/base_20240115_020000
TARGET_TIME="$1"
BASE_BACKUP="$2"
RESTORE_DATA="/var/lib/postgresql/14/restore_test"
WAL_ARCHIVE="/backup/wal"
if [ $# -ne 2 ]; then
echo "Kullanim: $0 'YYYY-MM-DD HH:MM:SS' /backup/yol"
exit 1
fi
# Dikkat: Bu islemi ONCE test ortaminda yapin!
echo "=== PostgreSQL PITR Baslıyor ==="
echo "Hedef: $TARGET_TIME"
echo "Base backup: $BASE_BACKUP"
echo ""
echo "UYARI: Bu islem test ortaminda calisiyor. Production icin"
echo "postgresl servisini durdurun ve data dizinini yedekleyin!"
echo ""
# Test ortami icin yeni data dizini olustur
mkdir -p "$RESTORE_DATA"
chmod 700 "$RESTORE_DATA"
chown postgres:postgres "$RESTORE_DATA"
# Base backup'ı aç
echo "Base backup aciliyor..."
tar -xzf "$BASE_BACKUP/base.tar.gz" -C "$RESTORE_DATA"
# recovery.conf veya postgresql.conf'a PITR ayarlarını ekle
# PostgreSQL 12+ için postgresql.conf kullanılır
cat > "$RESTORE_DATA/postgresql.conf.append" << EOF
restore_command = 'cp $WAL_ARCHIVE/%f %p'
recovery_target_time = '$TARGET_TIME'
recovery_target_action = 'promote'
EOF
# recovery.signal dosyası oluştur (PostgreSQL 12+)
touch "$RESTORE_DATA/recovery.signal"
echo "PITR yapilandirmasi tamamlandi."
echo "Test PostgreSQL instance'ini baslatin:"
echo " pg_ctl -D $RESTORE_DATA -l /tmp/pitr_restore.log start"
echo "Veriyi dogruladiktan sonra production'a alabilirsiniz."
Felaket Kurtarma Test Senaryoları
Yedek almak yetmez. Her ay en az bir kez restore testi yapmanız gerekiyor. Bunu otomatize etmezseniz, yapılmaz.
Otomatik Restore Doğrulama
#!/bin/bash
# dr_test_mysql.sh - Otomatik DR test scripti
# Cron: Her Pazar 03:00'da calistir
BACKUP_DIR="/backup/mysql"
TEST_CONTAINER="mysql_dr_test"
MYSQL_IMAGE="mysql:8.0"
TEST_PORT=3307
LOG_FILE="/var/log/dr_test/mysql_$(date +%Y%m%d).log"
ALERT_EMAIL="[email protected]"
mkdir -p "$(dirname $LOG_FILE)"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
fail() {
log "BASARISIZ: $1"
echo "DR Test BASARISIZ: $1 | $(hostname) | $(date)" |
mail -s "[KRITIK] DR Test Hatasi" "$ALERT_EMAIL"
# Test containerini temizle
docker rm -f "$TEST_CONTAINER" 2>/dev/null
exit 1
}
log "=== MySQL DR Test Basliyor ==="
# Son tam yedeği bul
LATEST_BACKUP=$(ls -td "$BACKUP_DIR"/full_* 2>/dev/null | head -1)
if [ -z "$LATEST_BACKUP" ]; then
fail "Hicbir tam yedek bulunamadi!"
fi
log "Test edilecek yedek: $LATEST_BACKUP"
# Test container'i baslat
log "Test MySQL container baslatiyor..."
docker run -d
--name "$TEST_CONTAINER"
-p "$TEST_PORT:3306"
-e MYSQL_ROOT_PASSWORD=testpass123
-v "$LATEST_BACKUP:/backup:ro"
"$MYSQL_IMAGE"
sleep 20
# Yedeği restore et
log "Restore islemi basliyor..."
docker exec "$TEST_CONTAINER" bash -c
"xtrabackup --copy-back --target-dir=/backup --datadir=/var/lib/mysql 2>/dev/null"
# Kritik tabloları kontrol et
log "Veri dogrulama basliyor..."
TABLES_OK=0
CRITICAL_TABLES="users orders products transactions"
for TABLE in $CRITICAL_TABLES; do
COUNT=$(mysql -h 127.0.0.1 -P "$TEST_PORT" -u root -ptestpass123
-e "SELECT COUNT(*) FROM production_db.$TABLE;" 2>/dev/null | tail -1)
if [ -z "$COUNT" ] || [ "$COUNT" -eq 0 ]; then
fail "Tablo bos veya erisemiyor: $TABLE"
else
log "OK: $TABLE - $COUNT kayit"
((TABLES_OK++))
fi
done
# Temizlik
docker rm -f "$TEST_CONTAINER"
log "=== DR Test BASARILI: $TABLES_OK/$( echo $CRITICAL_TABLES | wc -w) tablo dogrulandi ==="
echo "DR Test BASARILI | $(hostname) | $(date) | Yedek: $(basename $LATEST_BACKUP)" |
mail -s "[BILGI] Haftalik DR Test Basarili" "$ALERT_EMAIL"
MySQL Replikasyon ile Hızlı Failover
Felaket anında elle müdahale minimize edilmeli. Aşağıdaki script, primary sunucu yanıt vermediğinde replica’yı otomatik olarak promote eder:
#!/bin/bash
# mysql_failover.sh - Basit otomatik failover
# NOT: MHA veya Orchestrator gibi prod-grade cozumlerin yerine geçmez
# Kucuk ortamlar icin hizli cozum
PRIMARY_HOST="db-primary.internal"
REPLICA_HOST="db-replica.internal"
CHECK_INTERVAL=10
MAX_FAILURES=3
FAILURE_COUNT=0
VIP="10.0.1.100" # Virtual IP (keepalived ile yonetilmeli)
log() { echo "[$(date)] $1"; }
check_mysql() {
mysqladmin -h "$1" -u monitor -p"$MONITOR_PASS" ping --connect-timeout=5 &>/dev/null
return $?
}
promote_replica() {
log "FAILOVER BASLIYOR: $REPLICA_HOST primary olarak ataniyor..."
# Replikasyonu durdur ve primary moda al
mysql -h "$REPLICA_HOST" -u root -p"$ROOT_PASS" << 'EOF'
STOP SLAVE;
RESET SLAVE ALL;
SET GLOBAL read_only = OFF;
SET GLOBAL super_read_only = OFF;
EOF
if [ $? -eq 0 ]; then
log "BASARILI: $REPLICA_HOST artik primary!"
# VIP'i yeni primary'e tasi (arping ile eski ARP cache temizle)
ip addr add "$VIP/24" dev eth0
arping -c 3 -I eth0 "$VIP" &>/dev/null
# Ekibi uyar
echo "FAILOVER TAMAMLANDI
Eski Primary: $PRIMARY_HOST (DOWN)
Yeni Primary: $REPLICA_HOST
Zaman: $(date)
VIP $VIP yeni primary'e taşındı.
Acil durum prosedürü için: https://wiki.sirket.com/dr-runbook" |
mail -s "[KRITIK] Otomatik Failover Gerceklesti" [email protected]
else
log "HATA: Failover basarisiz! Manuel mudahale gerekiyor!"
echo "FAILOVER BASARISIZ - Manuel mudahale gerekiyor!" |
mail -s "[KRITIK ACIL] Failover Basarisiz" [email protected]
exit 1
fi
}
# Ana dongu
log "Failover monitor basliyor. Primary: $PRIMARY_HOST"
while true; do
if ! check_mysql "$PRIMARY_HOST"; then
((FAILURE_COUNT++))
log "Primary yanit vermiyor! ($FAILURE_COUNT/$MAX_FAILURES)"
if [ "$FAILURE_COUNT" -ge "$MAX_FAILURES" ]; then
promote_replica
exit 0
fi
else
FAILURE_COUNT=0
fi
sleep "$CHECK_INTERVAL"
done
Felaket Kurtarma Runbook Şablonu
Teknik scriptler kadar önemli olan bir şey de: felaket anında panik yapmadan izlenecek adım adım prosedür. Bu dokümanı her zaman erişilebilir tutun (sadece sunucuda değil, aynı zamanda confluence/notion/wiki’de).
Bir runbook’ta mutlaka bulunması gerekenler:
- Olay tespiti: Hangi alarm tetiklendi, ilk kontrol komutları nelerdir
- Etki analizi: Hangi servisler etkilendi, kaç kullanıcı etkileniyor
- Eskalasyon matrisi: Kim ne zaman aranır (DBA, uygulama ekibi, CTO)
- Kurtarma adımları: Hangi backup’tan, hangi sunucuya, hangi komutlarla
- Doğrulama kontrol listesi: Restore sonrası hangi testler yapılacak
- Geri alış planı: Eğer kurtarma da başarısız olursa B planı nedir
Runbook’u altı ayda bir masaüstü tatbikat (tabletop exercise) ile gözden geçirin. Ekibi toplayın, gerçek bir senaryo simüle edin ve prosedürleri baştan sonra gözden geçirin. Tatbikat sırasında bulunan açıklar, gerçek felakette sizi kurtarır.
İzleme ve Erken Uyarı
Felaket kurtarma planının gözden kaçan bir bölümü: felaketi felaketten önce tespit etmek. Aşağıdaki metrikleri mutlaka izleyin:
- Replikasyon gecikmesi (replication lag): MySQL için
Seconds_Behind_Master, PostgreSQL içinpg_stat_replication - Yedek yaşı: Son başarılı backup ne zaman alındı? Bunu da izleyin!
- Disk doluluk oranı: Özellikle binary log veya WAL birikiyor mu
- InnoDB/dead lock oranı: Veri bozulmasının habercisi olabilir
- Bağlantı havuzu doygunluğu: Felaket öncesi genellikle bu artar
#!/bin/bash
# db_health_check.sh - Temel veritabani saglik kontrolu
# Her 5 dakikada cron'dan calistir
MYSQL_HOST="localhost"
PG_HOST="localhost"
WARN_LAG=30 # saniye
CRIT_LAG=300 # saniye
MAX_BACKUP_AGE=86400 # 24 saat (saniye)
# MySQL replikasyon gecikme kontrolu
check_mysql_replication() {
LAG=$(mysql -h "$MYSQL_HOST" -u monitor -p"$MONITOR_PASS"
-e "SHOW SLAVE STATUSG" 2>/dev/null |
grep "Seconds_Behind_Master" | awk '{print $2}')
if [ "$LAG" = "NULL" ]; then
echo "CRITICAL: MySQL replikasyonu calısmiyor!"
return 2
elif [ "$LAG" -gt "$CRIT_LAG" ]; then
echo "CRITICAL: MySQL replikasyon gecikmesi: ${LAG}s"
return 2
elif [ "$LAG" -gt "$WARN_LAG" ]; then
echo "WARNING: MySQL replikasyon gecikmesi: ${LAG}s"
return 1
else
echo "OK: MySQL replikasyon gecikmesi: ${LAG}s"
return 0
fi
}
# Son yedek yaşını kontrol et
check_backup_age() {
LATEST=$(find /backup/mysql -name "xtrabackup_info" -newer /tmp/backup_check
2>/dev/null | sort -n | tail -1)
if [ -z "$LATEST" ]; then
LAST_BACKUP_FILE=$(find /backup/mysql -name "xtrabackup_info"
2>/dev/null | xargs ls -t 2>/dev/null | head -1)
if [ -z "$LAST_BACKUP_FILE" ]; then
echo "CRITICAL: Hic yedek dosyasi bulunamadi!"
return 2
fi
BACKUP_AGE=$(( $(date +%s) - $(stat -c %Y "$LAST_BACKUP_FILE") ))
if [ "$BACKUP_AGE" -gt "$MAX_BACKUP_AGE" ]; then
echo "CRITICAL: Son yedek ${BACKUP_AGE}s once alindi! (limit: ${MAX_BACKUP_AGE}s)"
return 2
fi
fi
echo "OK: Yedek yaşi kabul edilebilir sinirda"
return 0
}
check_mysql_replication
check_backup_age
Sonuç
Veritabanı felaket kurtarma planı bir kez yazılıp rafa kaldırılan bir belge değil, yaşayan bir süreç. MySQL veya PostgreSQL fark etmeksizin şu üç prensibi aklınızdan çıkarmayın:
Birincisi, 3-2-1 kuralını uygulayın: En az 3 kopya, 2 farklı medya, 1 off-site. Fidye yazılımı saldırılarında yedekler de şifreleniyor; immutable backup storage veya offline kopya şart.
İkincisi, restore testini otomatize edin. Bir backup’ın gerçekten çalışıp çalışmadığını ancak restore ederek anlarsınız. Yukarıdaki dr_test_mysql.sh gibi scriptleri haftalık cron’a bağlayın, başarısız restore’ları alarm olarak alın.
Üçüncüsü, RPO ve RTO hedeflerinizi düzenli ölçün. Kağıt üzerinde “1 saatte kurtarırız” demek yetmez. Son tatbikatta ne kadar sürdü? 6 ayda bir gerçek restore tatbikatı yapın, süreyi ölçün ve iyileştirin.
Yazının başındaki o müşteriyi hatırlıyor musunuz? O olaydan sonra hem off-site yedekleme hem de otomatik restore doğrulama kuruldu. İki yıl sonra başka bir disk arızasında, sistemi 47 dakikada ayağa kaldırdılar. Fark tam olarak bu.
