Veritabanı yedeklemesi, sysadmin’in uyku düzenini doğrudan etkileyen konuların başında gelir. Gece 3’te çalan telefon, “veritabanı uçtu” haberiyle seni uyandırıyorsa ve elinde güncel bir yedek yoksa, o gece çok uzun geçer. Bu yüzden MySQL/MariaDB yedekleme sistemini bir kez doğru kurman, sonraki onlarca geceyi rahat uyuman anlamına gelir. Bu yazıda, cron ile çalışan, production’da gerçekten kullandığım yedekleme scriptini adım adım inceleyeceğiz.
Neden Özel Script, Neden Hazır Araç Değil?
Piyasada Percona XtraBackup, MySQLDump gibi araçlar var, hatta Bacula, Amanda gibi enterprise çözümler de mevcut. Ama basit bir VPS’te ya da orta ölçekli bir sunucuda, kendi yazdığın bir bash scripti sana çok daha fazla kontrol sağlar. Ne zaman çalıştığını bilirsin, log formatını sen belirlersin, hata durumunda ne olacağını sen yazarsın. Kara kutu çözümlere güvenmek yerine şeffaf, okunabilir bir sistem kurmak her zaman kazandırır.
Ayrıca şunu da söyleyeyim: hazır araçlar bozulduğunda debug etmek için o aracın tüm dokümantasyonunu okuman gerekir. Kendi scriptinde bir şey yanlış giderse, 50 satır bash kodu okuyarak sorunu 5 dakikada bulursun.
Temel Gereksinimler
Scripti yazmadan önce ortamı kontrol edelim.
Gerekli paketler:
mysql-clientveyamariadb-client: mysqldump komutunu sağlargzipveyapigz: Yedekleri sıkıştırmak için (pigz çok çekirdekli sistemlerde çok daha hızlı)mailutilsveyamsmtp: Mail bildirimleri içinopenssl: Şifreli yedekleme istiyorsan
Ubuntu/Debian üzerinde kurulum:
apt update && apt install -y mysql-client gzip mailutils
CentOS/RHEL için:
yum install -y mysql mariadb gzip mailx
Bir de MySQL kullanıcısı oluşturalım. Root şifresini script içine gömmek hem güvensiz hem de kötü bir alışkanlık. Sadece dump alabilecek yetkiyle sınırlı bir kullanıcı oluşturalım:
CREATE USER 'backup_user'@'localhost' IDENTIFIED BY 'guclu_bir_sifre_buraya';
GRANT SELECT, SHOW DATABASES, LOCK TABLES, RELOAD, SHOW VIEW, EVENT, TRIGGER ON *.* TO 'backup_user'@'localhost';
FLUSH PRIVILEGES;
Bu kullanıcı sadece okuma ve dump işlemleri yapabilir, veri yazmaz, tablo silemez. Principle of least privilege, yani en az yetki prensibi. Production’da her zaman bunu uygula.
MySQL Option File ile Güvenli Kimlik Bilgisi Saklama
Şifreleri script içine yazma. Bunun yerine MySQL’in .my.cnf formatını kullan. Bu dosyayı backup_user‘ın home dizinine ya da özel bir konuma koyarız:
cat > /etc/mysql/backup.cnf << 'EOF'
[client]
user=backup_user
password=guclu_bir_sifre_buraya
host=localhost
EOF
chmod 600 /etc/mysql/backup.cnf
Bu dosyanın izinleri mutlaka 600 olmalı, yoksa MySQL okumayı reddeder. chown ile de doğru sahipliği ver:
chown root:root /etc/mysql/backup.cnf
Ana Yedekleme Scripti
Şimdi asıl işe gelelim. Aşağıdaki script, production’da kullandığım versiyonun basitleştirilmiş ama tam işlevsel hali:
#!/bin/bash
# =============================================================
# MySQL Otomatik Yedekleme Scripti
# Versiyon: 2.1
# Yazan: [email protected]
# =============================================================
# --- YAPILANDIRMA ---
BACKUP_DIR="/var/backups/mysql"
MYSQL_OPTS="--defaults-extra-file=/etc/mysql/backup.cnf"
DATE=$(date +%Y-%m-%d_%H-%M-%S)
RETENTION_DAYS=7
LOG_FILE="/var/log/mysql_backup.log"
MAIL_TO="[email protected]"
HOSTNAME=$(hostname -s)
# Sıkıştırma: gzip veya pigz (çok çekirdekli sistemler için pigz tercih edilir)
if command -v pigz &> /dev/null; then
COMPRESS="pigz"
else
COMPRESS="gzip"
fi
# --- FONKSİYONLAR ---
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
send_mail() {
local subject="$1"
local body="$2"
echo "$body" | mail -s "[$HOSTNAME] MySQL Backup: $subject" "$MAIL_TO"
}
# --- HAZIRLIK ---
mkdir -p "$BACKUP_DIR"
log "=========================================="
log "Yedekleme başladı"
BACKUP_SUCCESS=0
BACKUP_FAILED=0
FAILED_DBS=""
# --- VERİTABANLARINI LİSTELE ---
DATABASES=$(mysql $MYSQL_OPTS -e "SHOW DATABASES;" | grep -Ev "^(Database|information_schema|performance_schema|sys)$")
if [ -z "$DATABASES" ]; then
log "HATA: Veritabanı listesi alınamadı veya yedeklenecek DB bulunamadı"
send_mail "KRITIK HATA" "Veritabanı listesi alınamadı. Sunucu: $HOSTNAME"
exit 1
fi
# --- HER VERİTABANINI YEDEKLE ---
for DB in $DATABASES; do
log "Yedekleniyor: $DB"
BACKUP_FILE="${BACKUP_DIR}/${DB}_${DATE}.sql.gz"
if mysqldump $MYSQL_OPTS
--single-transaction
--routines
--triggers
--events
--hex-blob
--databases "$DB" | $COMPRESS > "$BACKUP_FILE"; then
SIZE=$(du -sh "$BACKUP_FILE" | cut -f1)
log "OK: $DB -> $BACKUP_FILE ($SIZE)"
((BACKUP_SUCCESS++))
else
log "HATA: $DB yedeklenemedi!"
FAILED_DBS="$FAILED_DBS $DB"
((BACKUP_FAILED++))
rm -f "$BACKUP_FILE"
fi
done
# --- ESKİ YEDEKLERİ TEMİZLE ---
log "Eski yedekler temizleniyor (${RETENTION_DAYS} günden eski)..."
DELETED=$(find "$BACKUP_DIR" -name "*.sql.gz" -mtime +$RETENTION_DAYS -delete -print | wc -l)
log "$DELETED adet eski yedek silindi"
# --- SONUÇ RAPORU ---
log "Tamamlandı: $BACKUP_SUCCESS başarılı, $BACKUP_FAILED başarısız"
log "=========================================="
if [ $BACKUP_FAILED -gt 0 ]; then
send_mail "HATA - Bazı yedekler alınamadı"
"Başarılı: $BACKUP_SUCCESSnBaşarısız: $BACKUP_FAILEDnHatalı DB'ler: $FAILED_DBSnLog: $LOG_FILE"
exit 1
else
send_mail "BAŞARILI"
"Tüm veritabanları yedeklendi.nBaşarılı: $BACKUP_SUCCESSnTarih: $DATE"
fi
Scripti kaydedelim ve çalıştırılabilir yapalım:
chmod +x /usr/local/bin/mysql_backup.sh
mysqldump Parametrelerini Anlamak
Scripteki mysqldump parametrelerine bakalım, her biri önemli:
–single-transaction: InnoDB tablolar için kritik. Dump sırasında tabloları kilitlemez, transaction başlatır. Production’da veri tutarlılığı için şart.
–routines: Stored procedure ve function’ları da yedekler. Bunları atlarsan schema restore’da sorun yaşarsın.
–triggers: Trigger’ları dahil eder. –routines gibi varsayılan olarak kapalıdır.
–events: Scheduled event’leri yedekler.
–hex-blob: BLOB alanlarını hex formatında yazar. Özellikle binary data içeren tablolarda karakter encoding sorunlarını önler.
–databases: Veritabanı adını CREATE DATABASE ve USE statement’larıyla birlikte yazar. Restore sırasında veritabanının var olup olmadığını kontrol etmene gerek kalmaz.
MyISAM tablolar da varsa dikkatli ol. --single-transaction sadece InnoDB’de işe yarar. Karma ortamlarda --lock-tables eklemeyi düşün, ama bu production trafiğini etkiler.
Cron ile Zamanlama
Script hazır, şimdi zamanlamayı yapalım. Crontab’ı düzenle:
crontab -e
Gece 2’de her gün çalışacak şekilde ayarla:
# MySQL Yedekleme - Her gece 02:00'de
0 2 * * * /usr/local/bin/mysql_backup.sh >> /var/log/mysql_backup_cron.log 2>&1
Eğer büyük bir veritabanın varsa ve yedekleme süresinin öngörülebilir olmasını istiyorsan, nice ve ionice ile işlem önceliğini düşür. Bu sayede yedekleme sistemi yavaşlatmaz:
0 2 * * * nice -n 10 ionice -c 2 -n 7 /usr/local/bin/mysql_backup.sh >> /var/log/mysql_backup_cron.log 2>&1
nice -n 10: CPU önceliğini düşürür (0 normal, 19 en düşük, biz 10 seçtik)
ionice -c 2 -n 7: Disk I/O önceliğini en düşük best-effort sınıfına alır
Ayrıca /etc/cron.d/ altına da koyabilirsin, bu genellikle daha düzenlidir:
cat > /etc/cron.d/mysql-backup << 'EOF'
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
MAILTO=""
# MySQL Yedekleme
0 2 * * * root /usr/local/bin/mysql_backup.sh >> /var/log/mysql_backup_cron.log 2>&1
EOF
Gelişmiş Özellik: Şifreli Yedekleme
Yedekleri uzak bir konuma göndereceksen ya da yedekler hassas veri içeriyorsa, şifreleme şart. OpenSSL ile basit ama etkili bir yöntem:
#!/bin/bash
# Şifreli yedekleme örneği - ana scripte entegre et
ENCRYPTION_KEY_FILE="/etc/mysql/backup.key"
BACKUP_FILE_ENCRYPTED="${BACKUP_FILE}.enc"
# Anahtar dosyası oluşturma (sadece bir kez çalıştır)
# openssl rand -base64 32 > /etc/mysql/backup.key
# chmod 400 /etc/mysql/backup.key
# Şifreli yedekleme
mysqldump $MYSQL_OPTS --single-transaction --routines --triggers "$DB" |
$COMPRESS |
openssl enc -aes-256-cbc -salt -pbkdf2
-pass file:"$ENCRYPTION_KEY_FILE"
-out "${BACKUP_DIR}/${DB}_${DATE}.sql.gz.enc"
# Restore için:
# openssl enc -aes-256-cbc -d -pbkdf2
# -pass file:/etc/mysql/backup.key
# -in backup.sql.gz.enc | gunzip | mysql $MYSQL_OPTS database_name
Anahtar dosyasını yedeklerden ayrı bir yerde sakla. Yedek ve anahtarı aynı yerde tutarsan şifrelemenin anlamı kalmaz.
Yedek Doğrulama Scripti
Yedek aldım diyip geçmek yetmez. “Yedek var” ile “yedek çalışıyor” arasında dağlar kadar fark vardır. Şu senaryoyu duymuşsunuzdur: yıllarca yedek alınmış, restore günü gelmiş, yedekler bozuk çıkmış. Bu yüzden düzenli olarak yedeklerin açılıp açılamadığını test etmek gerekir.
#!/bin/bash
# /usr/local/bin/mysql_backup_verify.sh
# Yedeklerin bütünlüğünü kontrol eder
BACKUP_DIR="/var/backups/mysql"
LOG_FILE="/var/log/mysql_backup_verify.log"
MAIL_TO="[email protected]"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
log "Yedek doğrulama başladı"
ERROR_COUNT=0
# Son 24 saatte alınan yedekleri kontrol et
find "$BACKUP_DIR" -name "*.sql.gz" -mtime -1 | while read BACKUP_FILE; do
# gzip bütünlük kontrolü
if gzip -t "$BACKUP_FILE" 2>/dev/null; then
SIZE=$(du -sh "$BACKUP_FILE" | cut -f1)
log "OK: $(basename $BACKUP_FILE) [$SIZE]"
else
log "BOZUK YEDEK: $BACKUP_FILE"
echo "Bozuk yedek tespit edildi: $BACKUP_FILE" |
mail -s "[KRITIK] MySQL Backup Bozuk - $(hostname)" "$MAIL_TO"
((ERROR_COUNT++))
fi
# Dosya boyutu sıfır mı kontrol et
if [ ! -s "$BACKUP_FILE" ]; then
log "BOŞ DOSYA: $BACKUP_FILE"
((ERROR_COUNT++))
fi
done
log "Doğrulama tamamlandı. Hata sayısı: $ERROR_COUNT"
Bu scripti de cron’a ekle, her sabah çalıştır:
# Yedek doğrulama - Her sabah 08:00'de
0 8 * * * root /usr/local/bin/mysql_backup_verify.sh >> /var/log/mysql_backup_verify_cron.log 2>&1
Uzak Sunucuya Yedek Aktarımı
Yedekler sadece aynı sunucuda duruyorsa felaketten korunma sağlamaz. Sunucu yanarsa, yedekle birlikte yanar. Uzak bir konuma aktarmalısın:
#!/bin/bash
# Yedekleri uzak sunucuya rsync ile gönder
# Bu kodu ana scriptin sonuna ekle
REMOTE_SERVER="[email protected]"
REMOTE_DIR="/backup/mysql/$(hostname -s)"
SSH_KEY="/root/.ssh/backup_key"
log "Yedekler uzak sunucuya aktarılıyor..."
rsync -avz --delete
-e "ssh -i $SSH_KEY -o StrictHostKeyChecking=yes -o BatchMode=yes"
"$BACKUP_DIR/"
"${REMOTE_SERVER}:${REMOTE_DIR}/"
if [ $? -eq 0 ]; then
log "Uzak aktarım başarılı: ${REMOTE_SERVER}:${REMOTE_DIR}"
else
log "HATA: Uzak aktarım başarısız!"
send_mail "UYARI - Uzak Aktarım Hatası"
"Yedekler $REMOTE_SERVER'a aktarılamadı. Manuel kontrol gerekiyor."
fi
SSH key’i önceden oluştur ve uzak sunucuya kopyala:
ssh-keygen -t ed25519 -f /root/.ssh/backup_key -N ""
ssh-copy-id -i /root/.ssh/backup_key.pub [email protected]
Log Rotasyonu
Uzun süre çalışan bir sistemde log dosyaları büyür. Logrotate ile bunu yönet:
cat > /etc/logrotate.d/mysql-backup << 'EOF'
/var/log/mysql_backup.log
/var/log/mysql_backup_verify.log
/var/log/mysql_backup_cron.log {
daily
rotate 30
compress
delaycompress
missingok
notifempty
create 640 root root
}
EOF
Restore Prosedürü
Yedek almak kadar önemli olan konu restore’u bilmek. Panikleyerek restore yapmak hata üretir. Standart prosedürünü önceden yaz ve bir yere kaydet.
Tek bir veritabanını restore etmek için:
# Sıkıştırılmış yedeği aç ve yükle
gunzip -c /var/backups/mysql/mydb_2024-01-15_02-00-01.sql.gz |
mysql --defaults-extra-file=/etc/mysql/backup.cnf
# Eğer veritabanı yoksa önce oluştur
mysql --defaults-extra-file=/etc/mysql/backup.cnf -e "CREATE DATABASE IF NOT EXISTS mydb;"
gunzip -c /var/backups/mysql/mydb_2024-01-15_02-00-01.sql.gz |
mysql --defaults-extra-file=/etc/mysql/backup.cnf mydb
Restore süresini önceden test et. 50GB’lık bir veritabanı restore’unun kaç saat sürdüğünü kriz anında değil, sessiz bir günde öğren.
Gerçek Dünya Senaryosu: E-ticaret Sitesi
Diyelim ki bir e-ticaret platformunun veritabanını yönetiyorsun. Üç kritik veritabanın var:
shop_production: Ana veritabanı, 15GBshop_analytics: Raporlama DB’si, 80GBshop_sessions: Oturum verileri, 2GB
shop_sessions için gerçek bir yedeklemeye gerek yok, kullanıcı session’ları anlık veri. shop_analytics büyük ve kritik değil, haftada bir tam yedek yeterli. shop_production ise her gece tam yedek, her 6 saatte bir artımlı yedek alınmalı.
Bu senaryoda tek bir script yerine özelleştirilmiş iki script çalıştırırsın. Ana scripti EXCLUDE_DBS değişkeniyle genişletebilirsin:
# Belirli DB'leri hariç tut
EXCLUDE_DBS="shop_sessions information_schema performance_schema sys"
DATABASES=$(mysql $MYSQL_OPTS -e "SHOW DATABASES;" | grep -v "^Database$" |
grep -Ev "^($(echo $EXCLUDE_DBS | tr ' ' '|'))$")
Bu küçük değişiklik, scriptini çok daha esnek hale getirir.
İzleme ve Alarm
Scriptin çalışıp çalışmadığını dışarıdan da izlemelisin. Cron çalıştı mı, script tamamlandı mı, son başarılı yedek ne zaman? Bunları Nagios, Zabbix ya da basit bir heartbeat servisiyle izleyebilirsin.
En basit yöntem: başarılı yedeklemeden sonra bir dosyanın timestamp’ini güncelle, monitoring da bu dosyanın yaşını kontrol etsin.
# Ana scriptin sonuna ekle, başarılı tamamlanınca:
touch /var/run/mysql_backup_last_success
Zabbix ya da Nagios’ta bu dosyanın son değiştirilme zamanını izleyecek bir kontrol yazarsın. 26 saatten eski olduysa alarm ver.
Sonuç
Doğru yapılandırılmış bir yedekleme sistemi kurmak bir kez yapılır ama sürekli meyvesini verir. Bu yazıda anlattığımız sistemi özetleyelim:
- Minimum yetkili ayrı bir MySQL kullanıcısı oluşturuldu
- Şifreler option file ile güvenli tutuldu
- Her veritabanı ayrı ayrı yedeklendi, hata durumunda rapor gönderildi
- Eski yedekler otomatik temizlendi
- Yedeklerin bütünlüğü ayrı bir script ile doğrulandı
- Uzak sunucuya rsync ile aktarım eklendi
- Log rotasyonu ayarlandı
Bunların hepsini kurup “tamam” demeden önce bir test restore yap. Boş bir test ortamında bir veritabanını restore et, verilerin tutarlı olduğunu kontrol et. Sadece o zaman gerçekten güvende olduğunu söyleyebilirsin.
En iyi yedekleme sistemi, restore’u test edilmiş olandır. Bunu hiç unutma.