Veritabanı Yedekleme Entegrasyonu: rsync ile Eksiksiz Rehber

Veritabanlarını yedeklemek, “bir gün lazım olur” diye yapılan bir rutin değil, sistemin can sigortasıdır. Peki ya bu yedekleri güvenli bir şekilde uzak sunucuya, NAS cihazına veya bulut depolamaya taşımak? İşte burada rsync devreye giriyor. Sadece dosya kopyalamakla kalmayıp, değişen blokları tespit eden, bant genişliğini verimli kullanan ve kesintisiz çalışabilen rsync, veritabanı yedekleme pipeline’larının vazgeçilmez aracı haline gelmiştir. Bu yazıda PostgreSQL, MySQL ve genel veritabanı dump dosyalarını rsync ile nasıl entegre edeceğinizi, production ortamlarında karşılaşılan gerçek sorunları ve bunların çözümlerini adım adım inceleyeceğiz.

rsync Neden Veritabanı Yedeklemede Tercih Edilir?

Klasik cp veya scp ile yedekleme yapıyorsanız, her seferinde tüm dosyayı baştan aktarırsınız. 50 GB’lık bir PostgreSQL dump dosyası için bu durum hem zaman hem de bant genişliği israfı demektir. rsync’in fark aktarımı (delta transfer) algoritması sayesinde, sadece değişen bloklar iletilir.

Veritabanı yedekleme senaryolarında rsync’i öne çıkaran başlıca özellikler:

  • Delta transfer algoritması: Önceki yedekle karşılaştırarak yalnızca değişen kısımları gönderir
  • Atomik operasyonlar: --inplace ve --partial bayraklarıyla yarıda kalan aktarımları devam ettirir
  • Sıkıştırma desteği: -z bayrağıyla aktarım sırasında sıkıştırma yapar
  • SSH tüneli: Şifreli kanal üzerinden güvenli aktarım sağlar
  • Checksum doğrulama: -c bayrağıyla dosya bütünlüğünü garanti altına alır
  • Bant genişliği limitleme: --bwlimit ile üretim trafiğini etkilemez

Temel Kurulum ve Hazırlık

Önce ortamınızı düzgün hazırlamak, sonraki her adımı kolaylaştırır.

SSH Anahtar Tabanlı Kimlik Doğrulama

Otomatik yedekleme scriptlerinde parola sormayan SSH bağlantısı şarttır.

# Yedekleme sunucusunda anahtar çifti oluştur
ssh-keygen -t ed25519 -C "backup-rsync-key" -f ~/.ssh/backup_rsa -N ""

# Public key'i hedef sunucuya kopyala
ssh-copy-id -i ~/.ssh/backup_rsa.pub [email protected]

# Bağlantıyı test et
ssh -i ~/.ssh/backup_rsa [email protected] "echo 'SSH bağlantısı başarılı'"

SSH config dosyasına kısayol eklemek uzun vadede işinizi kolaylaştırır:

# ~/.ssh/config dosyasına ekle
cat >> ~/.ssh/config << 'EOF'
Host backup-server
    HostName 192.168.1.100
    User backup
    IdentityFile ~/.ssh/backup_rsa
    StrictHostKeyChecking no
    ServerAliveInterval 60
    ServerAliveCountMax 3
EOF

Dizin Yapısını Oluşturma

Düzenli bir dizin yapısı, yedeklerinizi ileride bulmayı ve yönetmeyi kolaylaştırır.

# Kaynak sunucuda (veritabanı sunucusu)
mkdir -p /var/backup/databases/{postgresql,mysql,dumps}
mkdir -p /var/backup/logs

# Hedef sunucuda (yedekleme sunucusu)
mkdir -p /data/backups/{postgresql,mysql}/{daily,weekly,monthly}
chown -R backup:backup /data/backups
chmod 750 /data/backups

PostgreSQL Yedekleme + rsync Entegrasyonu

PostgreSQL’de canlı veritabanı dosyalarını doğrudan kopyalamak tutarsız yedeklere yol açar. Önce pg_dump veya pg_basebackup ile tutarlı bir snapshot almalı, ardından rsync ile taşımalısınız.

pg_dump ile Dump Alıp rsync ile Aktarma

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

set -euo pipefail

# Değişkenler
DB_HOST="localhost"
DB_PORT="5432"
DB_USER="postgres"
BACKUP_DIR="/var/backup/databases/postgresql"
REMOTE_HOST="backup-server"
REMOTE_DIR="/data/backups/postgresql/daily"
LOG_FILE="/var/backup/logs/pg_backup_$(date +%Y%m%d).log"
RETENTION_DAYS=7

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

log "PostgreSQL yedekleme başladı"

# Tüm veritabanlarını listele ve yedekle
for DB in $(psql -h "$DB_HOST" -U "$DB_USER" -t -c "SELECT datname FROM pg_database WHERE datistemplate = false AND datname != 'postgres'"); do
    DB=$(echo $DB | tr -d ' ')
    DUMP_FILE="${BACKUP_DIR}/${DB}_$(date +%Y%m%d_%H%M%S).dump"
    
    log "Yedekleniyor: $DB -> $DUMP_FILE"
    
    # Custom format ile dump al (sıkıştırılmış, paralel restore destekler)
    pg_dump -h "$DB_HOST" -U "$DB_USER" -Fc -Z 9 "$DB" -f "$DUMP_FILE"
    
    if [ $? -eq 0 ]; then
        log "$DB dump başarıyla alındı: $(du -sh $DUMP_FILE | cut -f1)"
    else
        log "HATA: $DB dump alınamadı!"
        exit 1
    fi
done

# rsync ile uzak sunucuya aktar
log "rsync aktarımı başlıyor..."

rsync -avz 
    --progress 
    --partial 
    --checksum 
    --delete 
    --bwlimit=50000 
    --log-file="${LOG_FILE}" 
    "${BACKUP_DIR}/" 
    "${REMOTE_HOST}:${REMOTE_DIR}/"

log "rsync aktarımı tamamlandı"

# Yerel eski yedekleri temizle
find "$BACKUP_DIR" -name "*.dump" -mtime "+${RETENTION_DAYS}" -delete
log "Yerel temizlik tamamlandı (${RETENTION_DAYS} günden eski dosyalar silindi)"

log "Yedekleme işlemi başarıyla tamamlandı"

pg_basebackup ile Fiziksel Yedekleme

Büyük veritabanlarında ya da Point-in-Time Recovery (PITR) gereksiniminde pg_basebackup tercih edilir:

#!/bin/bash
# Fiziksel PostgreSQL yedeklemesi

PGDATA="/var/lib/postgresql/14/main"
BACKUP_DIR="/var/backup/databases/postgresql/basebackup"
REMOTE_HOST="backup-server"
DATE=$(date +%Y%m%d)

# Mevcut base backup varsa rotate et
if [ -d "${BACKUP_DIR}/current" ]; then
    mv "${BACKUP_DIR}/current" "${BACKUP_DIR}/prev_${DATE}"
fi

# pg_basebackup ile yedek al
pg_basebackup 
    -h localhost 
    -U replication_user 
    -D "${BACKUP_DIR}/current" 
    -Fp 
    -Xs 
    -P 
    -R

# rsync ile aktarırken hard link kullan (disk alanı tasarrufu)
rsync -avz 
    --link-dest="${REMOTE_HOST}:/data/backups/postgresql/basebackup/prev" 
    --delete 
    "${BACKUP_DIR}/current/" 
    "${REMOTE_HOST}:/data/backups/postgresql/basebackup/current/"

MySQL / MariaDB Yedekleme + rsync Entegrasyonu

MySQL’de mysqldump, Percona XtraBackup veya mydumper kullanabilirsiniz. Her biri farklı senaryolara hitap eder.

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

set -euo pipefail

MYSQL_USER="backup_user"
MYSQL_PASS_FILE="/etc/mysql/backup.cnf"  # Güvenli: parolayı scriptte tutma!
BACKUP_DIR="/var/backup/databases/mysql"
REMOTE_HOST="backup-server"
REMOTE_DIR="/data/backups/mysql/daily"
DATE=$(date +%Y%m%d_%H%M%S)
LOG_FILE="/var/backup/logs/mysql_backup_$(date +%Y%m%d).log"

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

# MySQL bağlantı dosyası içeriği:
# [client]
# user=backup_user
# password=güçlü_parola_buraya

log "MySQL yedekleme başladı"

# Tüm veritabanlarını al
DATABASES=$(mysql --defaults-file="$MYSQL_PASS_FILE" -e "SHOW DATABASES;" | grep -Ev "(Database|information_schema|performance_schema|sys)")

for DB in $DATABASES; do
    DUMP_FILE="${BACKUP_DIR}/${DB}_${DATE}.sql.gz"
    
    log "Yedekleniyor: $DB"
    
    mysqldump 
        --defaults-file="$MYSQL_PASS_FILE" 
        --single-transaction 
        --routines 
        --triggers 
        --events 
        --hex-blob 
        "$DB" | gzip -9 > "$DUMP_FILE"
    
    # Dosya sıfır byte değilse başarılı say
    if [ -s "$DUMP_FILE" ]; then
        log "$DB başarıyla yedeklendi: $(du -sh $DUMP_FILE | cut -f1)"
    else
        log "HATA: $DB dump dosyası boş veya hatalı!"
        rm -f "$DUMP_FILE"
        exit 1
    fi
done

# rsync aktarımı
log "rsync başlıyor: ${BACKUP_DIR} -> ${REMOTE_HOST}:${REMOTE_DIR}"

rsync -avz 
    --partial 
    --partial-dir=".rsync-partial" 
    --timeout=300 
    --delete 
    --exclude="*.tmp" 
    --exclude=".rsync-partial/" 
    --bwlimit=30000 
    "${BACKUP_DIR}/" 
    "${REMOTE_HOST}:${REMOTE_DIR}/"

EXIT_CODE=$?

if [ $EXIT_CODE -eq 0 ]; then
    log "rsync başarıyla tamamlandı"
elif [ $EXIT_CODE -eq 24 ]; then
    log "UYARI: rsync tamamlandı ancak bazı kaynak dosyalar aktarım sırasında silindi (exit 24 - normal)"
else
    log "HATA: rsync başarısız! Exit code: $EXIT_CODE"
    exit $EXIT_CODE
fi

log "MySQL yedekleme tamamlandı"

Incremental Yedekleme: Hard Link Yöntemi (Snapshot Backup)

Günlük tam yedek almak disk alanını hızla doldurur. Hard link tabanlı incremental yedekleme, her gün “tam yedek gibi görünen” ama aslında sadece değişen dosyaları saklayan bir yapı kurar.

#!/bin/bash
# /usr/local/bin/incremental_backup.sh
# Her gün ayrı dizin, yalnızca değişen dosyalar gerçekten kopyalanır

BACKUP_SRC="/var/backup/databases"
REMOTE_HOST="backup-server"
REMOTE_BASE="/data/backups/incremental"
TODAY=$(date +%Y%m%d)
YESTERDAY=$(date -d "yesterday" +%Y%m%d)

# Önceki yedeği link-dest olarak kullan
# Bu sayede değişmeyen dosyalar hard link olarak işaret edilir (disk kullanmaz)
rsync -avz 
    --link-dest="${REMOTE_BASE}/${YESTERDAY}" 
    --delete 
    --checksum 
    --exclude="*.tmp" 
    --exclude="*.lock" 
    --log-file="/var/backup/logs/incremental_${TODAY}.log" 
    "${BACKUP_SRC}/" 
    "${REMOTE_HOST}:${REMOTE_BASE}/${TODAY}/"

# Uzak sunucuda 30 günden eski incremental yedekleri temizle
ssh "$REMOTE_HOST" "find ${REMOTE_BASE} -maxdepth 1 -type d -name '????????' | sort | head -n -30 | xargs rm -rf"

echo "Incremental yedekleme tamamlandı: $(date)"

Cron ile Otomatik Zamanlama

Scriptleri yazmak yetmez, doğru zamanlama kritiktir. Veritabanı yük profilinize göre zamanlama yapmalısınız.

# /etc/cron.d/database-backups dosyası
# Saat formatı: dakika saat gün_ay ay haftanın_günü kullanıcı komut

# PostgreSQL: Her gece 02:00'da
0 2 * * * postgres /usr/local/bin/pg_backup_rsync.sh >> /var/backup/logs/cron_pg.log 2>&1

# MySQL: Her gece 02:30'da (PG ile çakışmasın)
30 2 * * * root /usr/local/bin/mysql_backup_rsync.sh >> /var/backup/logs/cron_mysql.log 2>&1

# Haftalık incremental: Pazar 03:00
0 3 * * 0 root /usr/local/bin/incremental_backup.sh >> /var/backup/logs/cron_incremental.log 2>&1

# Yedek doğrulama: Her sabah 07:00
0 7 * * * root /usr/local/bin/verify_backups.sh >> /var/backup/logs/cron_verify.log 2>&1

Yedekleme Doğrulama Scripti

“Yedek aldım” demek yetmez. Aldığınız yedeğin gerçekten restore edilebilir olduğunu kanıtlamanız gerekir.

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

REMOTE_HOST="backup-server"
REMOTE_DIR="/data/backups"
ALERT_EMAIL="[email protected]"
MIN_FILE_SIZE=1024  # Minimum 1KB (byte cinsinden)
LOG_FILE="/var/backup/logs/verify_$(date +%Y%m%d).log"
ERRORS=0

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

log "Yedek doğrulama başladı"

# Uzak sunucudaki dosyaları kontrol et
ssh "$REMOTE_HOST" "find ${REMOTE_DIR} -name '*.dump' -o -name '*.sql.gz' | head -20" | while read REMOTE_FILE; do
    
    # Dosya boyutunu kontrol et
    FILE_SIZE=$(ssh "$REMOTE_HOST" "stat -c%s '${REMOTE_FILE}' 2>/dev/null || echo 0")
    
    if [ "$FILE_SIZE" -lt "$MIN_FILE_SIZE" ]; then
        log "HATA: Şüpheli küçük dosya: ${REMOTE_FILE} (${FILE_SIZE} bytes)"
        ERRORS=$((ERRORS + 1))
    else
        log "OK: ${REMOTE_FILE} ($(numfmt --to=iec ${FILE_SIZE}))"
    fi
done

# PostgreSQL dump dosyalarını syntax kontrolü ile doğrula
for DUMP in /var/backup/databases/postgresql/*.dump; do
    if [ -f "$DUMP" ]; then
        pg_restore --list "$DUMP" > /dev/null 2>&1
        if [ $? -eq 0 ]; then
            log "PostgreSQL dump geçerli: $(basename $DUMP)"
        else
            log "HATA: Geçersiz PostgreSQL dump: $(basename $DUMP)"
            ERRORS=$((ERRORS + 1))
        fi
    fi
done

# MySQL dump gzip bütünlüğünü kontrol et
for GZIP_FILE in /var/backup/databases/mysql/*.sql.gz; do
    if [ -f "$GZIP_FILE" ]; then
        gzip -t "$GZIP_FILE" 2>/dev/null
        if [ $? -eq 0 ]; then
            log "MySQL dump gzip geçerli: $(basename $GZIP_FILE)"
        else
            log "HATA: Bozuk gzip dosyası: $(basename $GZIP_FILE)"
            ERRORS=$((ERRORS + 1))
        fi
    fi
done

# Hata varsa e-posta gönder
if [ $ERRORS -gt 0 ]; then
    log "TOPLAM HATA: $ERRORS - E-posta gönderiliyor"
    mail -s "[KRITIK] Yedek Doğrulama Hatası: $ERRORS adet sorun tespit edildi" 
        "$ALERT_EMAIL" < "$LOG_FILE"
else
    log "Tüm doğrulamalar başarılı"
fi

Gerçek Dünya Sorunları ve Çözümleri

Sorun 1: “Broken Pipe” Hatası Büyük Dosyalarda

Production’da sıkça karşılaşılan bu sorun, ağ kesintisi veya SSH timeout kaynaklıdır.

# Çözüm: --partial ve timeout ayarları
rsync -avz 
    --partial 
    --partial-dir=".rsync-partial" 
    --timeout=600 
    --compress-level=6 
    -e "ssh -o ServerAliveInterval=30 -o ServerAliveCountMax=10 -o ConnectTimeout=60" 
    /var/backup/databases/ 
    backup-server:/data/backups/

Sorun 2: rsync Sonrası Disk Doluluğu

--delete bayrağı olmadan rsync eski dosyaları biriktirerek disk doldurabilir. Kontrollü bir yaklaşım:

# Silmeden önce ne silineceğini göster (dry-run)
rsync -avz --delete --dry-run /var/backup/databases/ backup-server:/data/backups/

# Eğer çıktı makul görünüyorsa gerçek çalıştır
rsync -avz 
    --delete 
    --delete-before 
    --delete-excluded 
    --exclude="*.tmp" 
    /var/backup/databases/ 
    backup-server:/data/backups/

Sorun 3: Aktarım Sırasında Değişen Dump Dosyaları

Uzun süren dump aktarımlarında kaynak dosya yazılmaya devam ediyor olabilir. Çözüm: önce dump’ı tamamla, sonra rsync’i çalıştır. Ek güvence için checksum kullanın:

# Checksumle doğrulamalı aktarım
rsync -avzc 
    --whole-file 
    /var/backup/databases/ 
    backup-server:/data/backups/
# --whole-file: Delta algoritmasını devre dışı bırakır, tüm dosyayı aktarır
# -c (--checksum): MD5 ile doğrulama yapar

İzleme ve Uyarı Sistemi

Yedekleme işlemini izlemeden bırakmak, emniyet kemeri takmadan araba sürmek gibidir.

#!/bin/bash
# rsync çıkış kodlarını yorumla ve Slack/mail bildir

RSYNC_EXIT_CODE=$?
WEBHOOK_URL="https://hooks.slack.com/services/XXXXXX"
HOSTNAME=$(hostname)

interpret_rsync_exit() {
    case $1 in
        0)  echo "Başarılı" ;;
        1)  echo "HATA: Syntax hatası" ;;
        2)  echo "HATA: Protokol uyumsuzluğu" ;;
        11) echo "HATA: Dosya I/O hatası" ;;
        23) echo "UYARI: Bazı dosyalar aktarılamadı (izin hatası)" ;;
        24) echo "UYARI: Bazı kaynak dosyalar aktarım sırasında silindi" ;;
        25) echo "HATA: --max-delete limiti aşıldı" ;;
        30) echo "HATA: Bağlantı timeout" ;;
        35) echo "HATA: Bekleme timeout" ;;
        *)  echo "HATA: Bilinmeyen exit kodu: $1" ;;
    esac
}

MESSAGE=$(interpret_rsync_exit $RSYNC_EXIT_CODE)

if [ $RSYNC_EXIT_CODE -ne 0 ] && [ $RSYNC_EXIT_CODE -ne 24 ]; then
    # Slack bildirimi
    curl -s -X POST "$WEBHOOK_URL" 
        -H 'Content-type: application/json' 
        -d "{"text": ":rotating_light: *${HOSTNAME}* yedekleme hatası: ${MESSAGE} (exit: ${RSYNC_EXIT_CODE})"}"
fi

Güvenlik Hususları

Yedekleme scriptleri genellikle root veya özel servis kullanıcılarıyla çalışır. Birkaç kritik güvenlik kuralı:

  • Ayrı backup kullanıcısı oluşturun: root ile rsync çalıştırmaktan kaçının
  • Hedef sunucuda read-only rsync: --read-only modunda rsyncd daemon kurun
  • Parola dosyaları: MySQL bağlantı bilgilerini /etc/mysql/backup.cnf içinde saklayın, chmod 600 yapın
  • SSH authorized_keys kısıtlaması: Public key’e komut kısıtlaması ekleyin
# ~/.ssh/authorized_keys içinde rsync'i kısıtla
command="rsync --server --sender -avz . /data/backups/",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-ed25519 AAAA... backup-rsync-key
  • Yedek şifreleme: Hassas veriler için gpg ile şifreli dump alın
# GPG şifreli dump
pg_dump -Fc mydb | gpg --cipher-algo AES256 -e -r [email protected] > mydb_encrypted.dump.gpg

Sonuç

rsync, doğru parametreler ve iyi yazılmış wrapper scriptlerle veritabanı yedekleme altyapınızın bel kemiği olabilir. Özetlemek gerekirse dikkat edilmesi gereken noktalara değinelim:

  • Dump önce, rsync sonra: Canlı veritabanı dosyalarını asla doğrudan rsync ile kopyalamayın
  • --partial her zaman: Büyük dosyalarda yarıda kesilen aktarımlar için şart
  • --bwlimit ile nazik olun: Production saatlerinde bant genişliğini kısıtlayın
  • Doğrulama zorunlu: Yedek aldım demek yetmez, restore test edin
  • Log her şeyi: --log-file ile rsync loglarını saklayın, sorun çözümünde hayat kurtarır
  • Exit kodlarını yorumlayın: exit 0 ve exit 24 dışındaki tüm kodlar alarm vermeli
  • Bant genişliği planlaması: Incremental ve hard-link yöntemi ile hem disk hem de ağ kullanımını optimize edin

İyi bir yedekleme sistemi, felaket anında paniği önler. rsync ile kurduğunuz bu pipeline’ı düzenli aralıklarla test edin, restore senaryolarını deneyin ve loglarınızı takip edin. Yedek almak rutin, restore etmek ise gerçek testtir.

Bir yanıt yazın

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