MySQL Veritabanı Yedekleme: Bacula Entegrasyonu

Veritabanı yedeklemesi, sistem yöneticilerinin en çok baş ağrısı yaşadığı konulardan biridir. MySQL verilerini kaybetmek, özellikle üretim ortamında, gerçek bir felakete dönüşebilir. Bacula ile MySQL yedeklemesini entegre etmek ise bu süreci hem otomatize hem de merkezi hale getirmenin en etkili yollarından biri. Bu yazıda, Bacula’nın MySQL ile nasıl entegre edileceğini, dikkat edilmesi gereken noktaları ve gerçek dünya senaryolarını ele alacağım.

Neden Bacula ile MySQL Yedeklemesi?

Bacula, kurumsal ortamlarda yaygın kullanılan, açık kaynaklı bir yedekleme çerçevesidir. Dosya sistemi yedeklemesinde gayet başarılıdır, ancak MySQL gibi aktif veritabanlarını yedeklerken özel bir yaklaşım gerektirir. Veritabanı dosyalarını doğrudan kopyalamak, tutarsız bir yedek almanıza yol açabilir çünkü yedekleme sırasında MySQL hala yazma işlemleri yapıyor olabilir.

Bacula’nın ClientRunBeforeJob ve ClientRunAfterJob direktifleri, bu sorunu çözmek için biçilmiş kaftan. Bu direktifler sayesinde yedekleme başlamadan önce mysqldump veya Percona XtraBackup ile tutarlı bir yedek alabilir, ardından Bacula bu yedeği kendi medya havuzuna aktarabilirsiniz.

Ön Gereksinimler

Başlamadan önce ortamınızın hazır olduğundan emin olun:

  • Bacula Director, Storage Daemon ve File Daemon kurulu ve çalışır durumda
  • MySQL 5.7+ veya MariaDB 10.3+
  • Yedekleme scriptlerini çalıştıracak bir servis kullanıcısı
  • Yeterli disk alanı (geçici dump dosyaları için)

MySQL’de Bacula’nın kullanacağı ayrı bir kullanıcı oluşturmanızı şiddetle tavsiye ederim. Root ile çalışmak hem güvenlik açısından hem de denetleme açısından kötü bir pratik.

mysql -u root -p
CREATE USER 'bacula_backup'@'localhost' IDENTIFIED BY 'GuvenliSifre123!';
GRANT SELECT, SHOW DATABASES, LOCK TABLES, RELOAD, REPLICATION CLIENT ON *.* TO 'bacula_backup'@'localhost';
FLUSH PRIVILEGES;
EXIT;

Bu kullanıcıya verilen yetkiler bilinçli olarak seçilmiştir. SELECT ve LOCK TABLES mysqldump için zorunlu, RELOAD ise flush işlemleri için gerekli. REPLICATION CLIENT ise binary log pozisyonunu almak için kullanılır ki bu, point-in-time recovery açısından çok değerlidir.

MySQL Dump Script’i Hazırlama

Bacula yedeklemesi öncesinde çalışacak temel script şu şekilde olabilir:

#!/bin/bash
# /usr/local/bin/mysql_bacula_backup.sh

BACKUP_DIR="/var/backup/mysql"
DATE=$(date +%Y%m%d_%H%M%S)
MYSQL_USER="bacula_backup"
MYSQL_PASS="GuvenliSifre123!"
LOG_FILE="/var/log/mysql_bacula_backup.log"
RETENTION_HOURS=48

# Log fonksiyonu
log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}

# Backup dizinini oluştur
mkdir -p "$BACKUP_DIR"

log "MySQL yedekleme başladı"

# Tüm veritabanlarını listele ve tek tek yedekle
DATABASES=$(mysql -u"$MYSQL_USER" -p"$MYSQL_PASS" -e "SHOW DATABASES;" | grep -Ev "(Database|information_schema|performance_schema|sys)")

for DB in $DATABASES; do
    log "Yedekleniyor: $DB"
    mysqldump 
        -u"$MYSQL_USER" 
        -p"$MYSQL_PASS" 
        --single-transaction 
        --routines 
        --triggers 
        --events 
        --master-data=2 
        --compress 
        "$DB" | gzip > "$BACKUP_DIR/${DB}_${DATE}.sql.gz"

    if [ $? -eq 0 ]; then
        log "Başarılı: $DB -> ${DB}_${DATE}.sql.gz"
    else
        log "HATA: $DB yedeklenemedi!"
        exit 1
    fi
done

# Eski yedekleri temizle
find "$BACKUP_DIR" -name "*.sql.gz" -mtime +2 -delete
log "Eski yedekler temizlendi (48 saatten eski)"

log "MySQL yedekleme tamamlandı"
exit 0

Script’e çalıştırma yetkisi vermek için:

chmod 750 /usr/local/bin/mysql_bacula_backup.sh
chown root:bacula /usr/local/bin/mysql_bacula_backup.sh

Burada dikkat etmeniz gereken önemli bir nokta var: –single-transaction parametresi InnoDB tablolar için transaction tutarlılığı sağlar ve tabloları kilitlemez. MyISAM tablolarınız varsa bu parametre yeterli olmayabilir, o durumda –lock-tables eklemeniz gerekebilir. Ancak bu, yoğun yazma ortamlarında performans sorununa yol açabilir.

.my.cnf ile Şifre Güvenliği

Script içinde şifre yazmak iyi bir pratik değildir. Özellikle kurumsal ortamlarda, şifreyi bir konfigürasyon dosyasında saklamak ve script’te referans vermek çok daha güvenlidir.

# /etc/mysql/bacula_backup.cnf dosyasını oluştur
cat > /etc/mysql/bacula_backup.cnf << 'EOF'
[client]
user=bacula_backup
password=GuvenliSifre123!
host=localhost
EOF

chmod 600 /etc/mysql/bacula_backup.cnf
chown root:root /etc/mysql/bacula_backup.cnf

Script’i bu dosyayı kullanacak şekilde güncelleyin:

mysqldump 
    --defaults-extra-file=/etc/mysql/bacula_backup.cnf 
    --single-transaction 
    --routines 
    --triggers 
    --events 
    --master-data=2 
    "$DB" | gzip > "$BACKUP_DIR/${DB}_${DATE}.sql.gz"

Bu yöntemle hem process listesinde şifre görünmez hem de script’i versiyonlama sistemine eklerken yanlışlıkla şifre sızdırma riskini ortadan kaldırırsınız.

Bacula Job Konfigürasyonu

Şimdi asıl entegrasyon kısmına geliyoruz. Bacula Director konfigürasyon dosyasına aşağıdaki Job tanımını eklemeniz gerekiyor:

# /etc/bacula/bacula-dir.conf içine eklenecek kısım

Job {
    Name = "MySQL-Yedekleme"
    Type = Backup
    Level = Full
    Client = uretim-sunucu-fd
    FileSet = "MySQL-Dosyalari"
    Schedule = "MySQL-Gunluk-Schedule"
    Storage = File-Depolama
    Messages = Standard
    Pool = MySQL-Pool
    Priority = 10
    ClientRunBeforeJob = "/usr/local/bin/mysql_bacula_backup.sh"
    ClientRunAfterJob = "/usr/local/bin/mysql_bacula_cleanup.sh"
    Write Bootstrap = "/var/lib/bacula/mysql-yedekleme.bsr"
}

FileSet {
    Name = "MySQL-Dosyalari"
    Include {
        Options {
            signature = MD5
            compression = GZIP
        }
        File = /var/backup/mysql
    }
    Exclude {
        File = /var/backup/mysql/*.tmp
    }
}

Schedule {
    Name = "MySQL-Gunluk-Schedule"
    Run = Full daily at 02:00
}

Pool {
    Name = MySQL-Pool
    Pool Type = Backup
    Recycle = yes
    AutoPrune = yes
    Volume Retention = 30 days
    Maximum Volume Bytes = 50G
    Maximum Volumes = 20
    Label Format = "MySQL-"
}

ClientRunBeforeJob ve ClientRunAfterJob direktiflerinin önemi şurada: Bu komutlar Bacula File Daemon üzerinde, yani istemci sunucuda çalışır. Dolayısıyla script’in File Daemon’ın kurulu olduğu sunucuda mevcut olması gerekir.

Cleanup Script’i

Yedekleme tamamlandıktan sonra geçici dosyaları temizlemek için bir script daha hazırlayın:

#!/bin/bash
# /usr/local/bin/mysql_bacula_cleanup.sh

BACKUP_DIR="/var/backup/mysql"
LOG_FILE="/var/log/mysql_bacula_backup.log"

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}

log "Cleanup başladı - Geçici dosyalar temizleniyor"

# Başarıyla Bacula'ya aktarılan dosyaları sil
# Sadece 24 saatten eski dosyaları sil (güvenlik payı)
find "$BACKUP_DIR" -name "*.sql.gz" -mmin +1440 -delete

if [ $? -eq 0 ]; then
    log "Cleanup tamamlandı"
    exit 0
else
    log "HATA: Cleanup sırasında sorun oluştu"
    exit 1
fi
chmod 750 /usr/local/bin/mysql_bacula_cleanup.sh
chown root:bacula /usr/local/bin/mysql_bacula_cleanup.sh

Gerçek Dünya Senaryosu: Büyük Veritabanı Sorunları

Bir e-ticaret şirketinde çalışırken 200GB’ı aşan bir MySQL veritabanıyla karşılaştım. Standart mysqldump yaklaşımı bu ölçekte çok yavaş kalıyordu ve yedekleme penceresi iş saatlerine uzuyordu. Bu tür durumlarda Percona XtraBackup devreye girer.

XtraBackup’ın avantajı, InnoDB tablolarını kilitlemeden hot backup alabilmesidir. Bacula entegrasyonunda bunu şöyle kullanabilirsiniz:

#!/bin/bash
# /usr/local/bin/xtrabackup_bacula.sh

BACKUP_DIR="/var/backup/mysql_xtra"
DATE=$(date +%Y%m%d_%H%M%S)
LOG_FILE="/var/log/xtrabackup_bacula.log"
XTRABACKUP_USER="bacula_backup"

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}

log "XtraBackup başladı"

# Backup dizini oluştur
mkdir -p "$BACKUP_DIR/$DATE"

# Full backup al
xtrabackup 
    --defaults-extra-file=/etc/mysql/bacula_backup.cnf 
    --backup 
    --target-dir="$BACKUP_DIR/$DATE" 
    --parallel=4 
    --compress 
    --compress-threads=4 
    2>> "$LOG_FILE"

if [ $? -ne 0 ]; then
    log "HATA: XtraBackup başarısız oldu!"
    exit 1
fi

# Prepare aşaması - restore için hazırla
xtrabackup 
    --prepare 
    --target-dir="$BACKUP_DIR/$DATE" 
    2>> "$LOG_FILE"

if [ $? -eq 0 ]; then
    log "XtraBackup başarıyla tamamlandı: $BACKUP_DIR/$DATE"
    exit 0
else
    log "HATA: Prepare aşamasında sorun oluştu!"
    exit 1
fi

XtraBackup kullandığınızda Bacula FileSet’inde /var/backup/mysql_xtra dizinini işaret etmeniz yeterli.

Yedek Doğrulama: Test Etmeden Yedek Sayılmaz

Bu konuda çok net bir görüşüm var: Test edilmemiş bir yedek, yedek değildir. Bacula’nın restore işlemini düzenli olarak test etmeniz şarttır. Aşağıdaki script, alınan MySQL yedeklerini otomatik olarak doğrular:

#!/bin/bash
# /usr/local/bin/mysql_backup_verify.sh

BACKUP_DIR="/var/backup/mysql"
TEST_DB="backup_test_$$"
LOG_FILE="/var/log/mysql_backup_verify.log"
ALERT_EMAIL="[email protected]"

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}

LATEST_BACKUP=$(ls -t "$BACKUP_DIR"/*.sql.gz 2>/dev/null | head -1)

if [ -z "$LATEST_BACKUP" ]; then
    log "HATA: Hiç yedek dosyası bulunamadı!"
    echo "MySQL yedek doğrulama başarısız: Dosya yok" | mail -s "ALARM: Yedek Doğrulama Hatası" "$ALERT_EMAIL"
    exit 1
fi

log "Doğrulanacak yedek: $LATEST_BACKUP"

# Test veritabanı oluştur
mysql --defaults-extra-file=/etc/mysql/bacula_backup.cnf 
    -e "CREATE DATABASE $TEST_DB;" 2>> "$LOG_FILE"

# Yedeği test veritabanına restore et
zcat "$LATEST_BACKUP" | mysql 
    --defaults-extra-file=/etc/mysql/bacula_backup.cnf 
    "$TEST_DB" 2>> "$LOG_FILE"

RESTORE_STATUS=$?

# Test veritabanını sil
mysql --defaults-extra-file=/etc/mysql/bacula_backup.cnf 
    -e "DROP DATABASE IF EXISTS $TEST_DB;" 2>> "$LOG_FILE"

if [ $RESTORE_STATUS -eq 0 ]; then
    log "Doğrulama başarılı: $LATEST_BACKUP"
    exit 0
else
    log "HATA: Doğrulama başarısız: $LATEST_BACKUP"
    echo "MySQL yedek doğrulama başarısız: $LATEST_BACKUP" | mail -s "ALARM: Yedek Bozuk!" "$ALERT_EMAIL"
    exit 1
fi

Bu script’i cron’a ekleyerek haftalık otomatik doğrulama yapabilirsiniz:

# crontab -e
0 4 * * 1 /usr/local/bin/mysql_backup_verify.sh

Sık Karşılaşılan Sorunlar ve Çözümleri

Bacula ile MySQL entegrasyonunda en sık şu problemlerle karşılaşıyorum:

ClientRunBeforeJob başarısız oluyor, Job devam ediyor: Bacula varsayılan olarak pre-job script hata verse bile devam edebilir. Bunu engellemek için script’inizin hata durumunda sıfırdan farklı bir çıkış kodu döndürdüğünden emin olun. Bacula, sıfırdan farklı çıkış kodunu hata olarak yorumlar ve Job’ı durdurur.

Disk dolması sorunu: Büyük veritabanlarında dump dosyaları çok yer kaplayabilir. FileSet’e bir boyut sınırı koymanızı öneririm. Ayrıca /var/backup/mysql için ayrı bir partition veya LVM volume kullanmak, ana sistemi korumanın akıllıca bir yolu.

Yedekleme sırasında performans düşüşü: --single-transaction kullanıldığında bile çok büyük tablolarda sorgu süreleri uzayabilir. Bunu minimize etmek için yedekleme saatini en düşük trafik dönemine (genellikle gece 02:00-04:00 arası) almak mantıklıdır. Ayrıca mysqldump’a --max-allowed-packet=512M parametresi eklemek büyük BLOB içeren tablolarda sorunları önler.

Binary log tutarsızlığı: Master-slave replikasyon ortamında yedeklemeyi slave üzerinden almak hem master’ı yüklememek hem de tutarlı bir binary log pozisyonu almak açısından idealdir.

Bacula Director’da Bildirim Ayarları

Yedekleme başarısız olduğunda haberdar olmak için Bacula’nın Messages konfigürasyonunu düzgün ayarlamak önemli:

# bacula-dir.conf Messages bölümü
Messages {
    Name = Standard
    mailcommand = "/usr/sbin/bsmtp -h localhost -f "(Bacula) <%r>" -s "Bacula: %t %e of %c %l" %r"
    mail = [email protected] = all, !skipped
    console = all, !skipped, !saved
    append = "/var/log/bacula/bacula.log" = all, !skipped
    catalog = all
}

mail = [email protected] = all, !skipped satırı, atlanan işlemler hariç tüm durumlar için e-posta gönderir. Üretim ortamında bu bildirimler hayat kurtarır.

Restore Prosedürü

Felaket anında paniğe kapılmamak için restore prosedürünü önceden bilmek ve tatbikat yapmak şart. Bacula üzerinden MySQL yedeğini restore etmek şu adımlarla yapılır:

Önce Bacula konsolundan dosyaları geri yükleyin:

bconsole
*restore
# İlgili Job'ı seçin, restore dizinini belirleyin
# Varsayılan olarak /tmp/bacula-restores altına gelir

Dosyalar geri yüklendikten sonra MySQL’e import edin:

# Restore edilen dizindeki dump dosyasını bul
ls /tmp/bacula-restores/var/backup/mysql/

# İlgili veritabanını restore et
zcat /tmp/bacula-restores/var/backup/mysql/production_db_20240115_020001.sql.gz | 
    mysql --defaults-extra-file=/etc/mysql/bacula_backup.cnf production_db

# Restore başarılı mı kontrol et
mysql --defaults-extra-file=/etc/mysql/bacula_backup.cnf 
    -e "SELECT COUNT(*) FROM production_db.orders;"

XtraBackup ile alınan yedekleri restore ederken süreç farklıdır:

# MySQL servisini durdur
systemctl stop mysql

# Mevcut data dizinini yedekle
mv /var/lib/mysql /var/lib/mysql_eski

# Restore et
xtrabackup --copy-back 
    --target-dir=/tmp/bacula-restores/var/backup/mysql_xtra/20240115_020001

# Yetkileri düzelt
chown -R mysql:mysql /var/lib/mysql

# MySQL'i başlat
systemctl start mysql

Monitoring ve Raporlama

Günlük yedekleme durumunu takip etmek için basit bir kontrol scripti oldukça işe yarar:

#!/bin/bash
# /usr/local/bin/check_mysql_backup_status.sh

BACKUP_DIR="/var/backup/mysql"
MAX_AGE_HOURS=25
ALERT_EMAIL="[email protected]"

# En son yedek dosyasının yaşını kontrol et
LATEST=$(find "$BACKUP_DIR" -name "*.sql.gz" -mmin -$((MAX_AGE_HOURS * 60)) | wc -l)

if [ "$LATEST" -eq 0 ]; then
    echo "KRITIK: Son $MAX_AGE_HOURS saat içinde MySQL yedeği alınmamış!" | 
        mail -s "ALARM: MySQL Yedek Eksik" "$ALERT_EMAIL"
    exit 2
fi

# Toplam yedek boyutunu raporla
TOTAL_SIZE=$(du -sh "$BACKUP_DIR" | cut -f1)
echo "MySQL yedekleri normal. Toplam boyut: $TOTAL_SIZE"
exit 0

Bu script’i Nagios, Zabbix veya benzeri bir monitoring sistemiyle entegre edebilirsiniz. Cron’a 5’er dakikalık aralıklarla eklemek yerine sabah kontrol saatinde çalıştırmak daha mantıklı:

0 8 * * * /usr/local/bin/check_mysql_backup_status.sh

Sonuç

Bacula ile MySQL entegrasyonu, başta karmaşık görünse de adım adım ele alındığında son derece yönetilebilir bir yapıya kavuşuyor. Bu yazıda anlattıklarımı özetleyecek olursam:

  • Ayrı bir MySQL kullanıcısıyla en az yetki prensibini uygulayın
  • Şifreleri script içine yazmak yerine .cnf dosyasında saklayın
  • InnoDB için --single-transaction kullanarak kilitlemesiz yedek alın
  • Büyük veritabanlarında Percona XtraBackup’ı değerlendirin
  • ClientRunBeforeJob ve ClientRunAfterJob direktiflerini etkili kullanın
  • Yedekleri düzenli olarak test edin, doğrulanmamış yedek yok hükmündedir
  • Monitoring ve bildirim mekanizmalarını kurmayı atlamamayın
  • Restore prosedürünü yazılı hale getirin ve tatbikat yapın

Yedekleme sistemi kurmak bir kez yapılan iş değil, sürekli bakım gerektiren bir süreçtir. Bacula loglarını düzenli gözden geçirin, zaman zaman restore testi yapın ve ortamınız değiştikçe konfigürasyonunuzu güncelleyin. Bir gün bu sistem sizi gerçekten kurtardığında, harcanan her dakikanın değerini anlayacaksınız.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir