PostgreSQL Point-in-Time Recovery (PITR) Uygulaması

Veritabanı yönetiminde en kritik becerilerden biri, bir felaketten sonra tam olarak istediğin noktaya dönebilmektir. PostgreSQL’in Point-in-Time Recovery (PITR) özelliği tam da bunu sağlar: saniyesi saniyesine, belirli bir transaction’a kadar geri dönme imkanı. “Dün saat 14:37’de kim o tabloyu truncate etti?” sorusunun cevabını bulmak ve o anı geri getirmek artık mümkün.

Bu yazıda PITR’ı sıfırdan kuracağız, gerçek dünya senaryolarıyla test edeceğiz ve production ortamınızda güvenle kullanabileceğiniz bir yapı oluşturacağız.

PITR Nedir ve Neden Önemlidir?

Standart bir yedek sistemi size yalnızca belirli anlardaki snapshot’ları geri getirir. Gece 02:00’deki yedeğiniz var ama felaket öğleden sonra 15:45’te gerçekleştiyse, o aradaki 13 saatlik veriyi kaybedersiniz. PITR ise WAL (Write-Ahead Log) dosyalarını kullanarak bu boşluğu doldurur.

PostgreSQL her yazma işlemini önce WAL’a kaydeder. Bu loglar sıralı ve deterministik olduğu için, bir base backup üzerine WAL dosyalarını sırayla uygulayarak istediğiniz herhangi bir noktaya ulaşabilirsiniz. Bu yaklaşım hem RPO’nuzu (Recovery Point Objective) dakikalar hatta saniyelere indirir hem de “tam olarak şu transaction öncesine git” gibi cerrahi müdahaleler yapmanıza olanak tanır.

Ortam Hazırlığı

Önce mevcut PostgreSQL kurulumumuzu kontrol edelim:

# PostgreSQL versiyonunu ve veri dizinini kontrol et
psql -U postgres -c "SELECT version();"
psql -U postgres -c "SHOW data_directory;"
psql -U postgres -c "SHOW wal_level;"
psql -U postgres -c "SHOW archive_mode;"

PITR için temel gereksinimler şunlar:

  • wal_level: en az replica olmalı (PostgreSQL 9.6+ için)
  • archive_mode: on olmalı
  • archive_command: WAL dosyalarını nereye kopyalayacağını söyleyen komut

Bu yazıda şu ortamı kullanacağız:

  • PostgreSQL 15, Ubuntu 22.04
  • Base backup dizini: /backup/base
  • WAL arşiv dizini: /backup/wal_archive
  • Test veritabanı: pitr_test

WAL Arşivlemeyi Yapılandırma

İlk adım PostgreSQL’i WAL dosyalarını arşivleyecek şekilde ayarlamak. postgresql.conf dosyasını düzenleyelim:

# postgresql.conf dosyasını bul ve düzenle
sudo nano /etc/postgresql/15/main/postgresql.conf

Şu parametreleri ayarlayın:

# Bu satırları postgresql.conf içine ekle veya güncelle
wal_level = replica
archive_mode = on
archive_command = 'test ! -f /backup/wal_archive/%f && cp %p /backup/wal_archive/%f'
archive_timeout = 300
max_wal_senders = 3
wal_keep_size = 1GB

Parametrelerin anlamları:

  • wal_level = replica: Replikasyon ve PITR için gerekli WAL bilgisini yaz
  • archive_mode = on: WAL arşivlemeyi etkinleştir
  • archive_command: WAL segment tamamlandığında çalışacak komut. %p kaynak yolu, %f dosya adı
  • archive_timeout = 300: 5 dakikada bir zorla WAL segmenti tamamla (aktif olmayan sistemler için)
  • max_wal_senders = 3: Eş zamanlı WAL gönderici sayısı
  • wal_keep_size = 1GB: pg_wal dizininde tutulacak minimum WAL boyutu

Arşiv dizinini oluşturun ve izinleri ayarlayın:

sudo mkdir -p /backup/base /backup/wal_archive
sudo chown postgres:postgres /backup/base /backup/wal_archive
sudo chmod 750 /backup/base /backup/wal_archive

# PostgreSQL'i yeniden başlat
sudo systemctl restart postgresql@15-main

# Arşivlemenin aktif olduğunu doğrula
psql -U postgres -c "SHOW archive_mode;"
psql -U postgres -c "SHOW archive_command;"

Base Backup Alma

WAL arşivleme aktif olduktan sonra base backup almanın zamanı geldi. pg_basebackup aracını kullanacağız:

# Base backup al
pg_basebackup 
  -U postgres 
  -D /backup/base/$(date +%Y%m%d_%H%M%S) 
  --format=tar 
  --gzip 
  --compress=9 
  --checkpoint=fast 
  --wal-method=stream 
  --progress 
  --verbose

# Backup tamamlandı mı kontrol et
ls -lh /backup/base/
# backup_label dosyasının varlığını doğrula
tar -tzf /backup/base/20240115_143000/base.tar.gz | grep backup_label

--wal-method=stream parametresi backup sırasında WAL’ı da stream ederek tutarlı bir backup almanızı sağlar. Production’da bu oldukça kritik.

Backup etiketini kontrol edelim:

# backup_label içeriğini görüntüle
tar -xOf /backup/base/20240115_143000/base.tar.gz backup_label
# Çıktıda START WAL LOCATION ve CHECKPOINT LOCATION bilgileri olacak

Test Senaryosu: Kazara Tablo Silme

Şimdi gerçek hayattan bir senaryo simüle edelim. Bir geliştirici yanlışlıkla kritik bir tabloyu truncate etmiş, siz de tam olarak o anın öncesine dönmeniz gerekiyor.

# Test veritabanı ve tablosu oluştur
psql -U postgres <<'EOF'
CREATE DATABASE pitr_test;
c pitr_test

CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    customer_name VARCHAR(100),
    amount DECIMAL(10,2),
    created_at TIMESTAMP DEFAULT NOW()
);

-- 10.000 satır test verisi ekle
INSERT INTO orders (customer_name, amount)
SELECT
    'Musteri_' || generate_series,
    (random() * 1000)::DECIMAL(10,2)
FROM generate_series(1, 10000);

SELECT COUNT(*) FROM orders;
-- Çıktı: 10000

-- Mevcut zamanı not al (bu bizim kurtarma hedefimiz olacak)
SELECT NOW();
-- Örnek: 2024-01-15 15:20:33.123456+00
EOF

Birkaç dakika bekleyin, sonra “felaketi” gerçekleştirin:

# Felaket anı - birisi tabloyu sildi!
psql -U postgres -d pitr_test <<'EOF'
-- Yeni siparişler geldi
INSERT INTO orders (customer_name, amount)
SELECT 'Yeni_Musteri_' || generate_series, (random() * 500)::DECIMAL(10,2)
FROM generate_series(1, 500);

-- FELAKET: Tablo truncate edildi
TRUNCATE TABLE orders;

-- Hatta bir tane daha eklenmiş gibi görünsün
INSERT INTO orders (customer_name, amount) VALUES ('Son_Kayit', 1.00);

SELECT COUNT(*) FROM orders;
-- Çıktı: 1 (10500 satır gitti!)
EOF

WAL’ın arşivlendiğinden emin olun:

# Manuel WAL switch yap ki son WAL arşivlensin
psql -U postgres -c "SELECT pg_switch_wal();"
sleep 5
ls -la /backup/wal_archive/ | tail -5

PITR ile Kurtarma

Şimdi asıl sihir başlıyor. Hedefimiz TRUNCATE öncesindeki ana dönmek.

# Önce PostgreSQL'i durdur
sudo systemctl stop postgresql@15-main

# Mevcut veri dizinini yedekle (güvenlik için)
sudo mv /var/lib/postgresql/15/main /var/lib/postgresql/15/main_broken
sudo mkdir /var/lib/postgresql/15/main
sudo chown postgres:postgres /var/lib/postgresql/15/main

# Base backup'ı geri yükle
cd /var/lib/postgresql/15/main
sudo -u postgres tar -xzf /backup/base/20240115_143000/base.tar.gz

# Eğer pg_wal ayrı bir tar dosyasıysa onu da çıkar
# sudo -u postgres tar -xzf /backup/base/20240115_143000/pg_wal.tar.gz -C pg_wal/

Şimdi recovery konfigürasyonunu hazırlayın. PostgreSQL 12+ sürümlerinde recovery.conf artık yok, bunun yerine postgresql.conf ve recovery.signal dosyası kullanılıyor:

# recovery.signal dosyası oluştur - bu PostgreSQL'e recovery modunda başlamasını söyler
sudo -u postgres touch /var/lib/postgresql/15/main/recovery.signal

# postgresql.conf'a recovery parametrelerini ekle
sudo -u postgres tee -a /var/lib/postgresql/15/main/postgresql.conf <<'EOF'

# PITR Recovery Parametreleri
restore_command = 'cp /backup/wal_archive/%f %p'
recovery_target_time = '2024-01-15 15:20:33'
recovery_target_action = 'promote'
recovery_target_inclusive = false
EOF

Recovery parametrelerinin anlamları:

  • restore_command: Arşivden WAL dosyalarını almak için kullanılacak komut
  • recovery_target_time: Hangi zamana dönmek istediğimiz (TRUNCATE öncesi)
  • recovery_target_action: Recovery tamamlandığında ne yapılacak (promote = okuma-yazma moduna geç)
  • recovery_target_inclusive = false: Hedef zamandaki transaction’ı dahil etme
# PostgreSQL'i başlat ve recovery loglarını izle
sudo systemctl start postgresql@15-main
sudo tail -f /var/log/postgresql/postgresql-15-main.log

Logda şuna benzer satırlar görmelisiniz:

LOG: starting point-in-time recovery to 2024-01-15 15:20:33+00
LOG: restored log file "000000010000000000000001" from archive
...
LOG: recovery stopping before commit of transaction 789, time 2024-01-15 15:21:47.234
LOG: pausing at the end of recovery
HINT: Execute pg_wal_replay_resume() to promote.
# Recovery tamamlandıktan sonra doğrula
psql -U postgres -d pitr_test -c "SELECT COUNT(*) FROM orders;"
# Çıktı: 10000 (kurtarıldı!)

# Sunucuyu promote et (recovery.signal dosyası zaten silinmiş olacak)
# Eğer recovery_target_action = 'pause' kullandıysanız:
# psql -U postgres -c "SELECT pg_wal_replay_resume();"

Transaction ID ile Kurtarma

Bazen tam zamanı bilmeyebilirsiniz ama hangi transaction’ı geri almak istediğinizi biliyorsunuz. PostgreSQL bunu da destekliyor:

# Önce problem yaratan transaction ID'sini bul
# pg_waldump ile WAL'ı inceleyebilirsiniz
pg_waldump -p /backup/wal_archive -n 1000 000000010000000000000005 | grep -i truncate

# Transaction ID ile recovery
sudo -u postgres tee -a /var/lib/postgresql/15/main/postgresql.conf <<'EOF'

# Transaction ID bazlı recovery
restore_command = 'cp /backup/wal_archive/%f %p'
recovery_target_xid = '789'
recovery_target_inclusive = false
recovery_target_action = 'promote'
EOF

Named Restore Point Kullanımı

Proaktif bir yaklaşım olarak, kritik operasyonlar öncesinde named restore point oluşturabilirsiniz:

# Kritik bir deployment öncesinde restore point oluştur
psql -U postgres -c "SELECT pg_create_restore_point('before_migration_v2_1');"

# Şimdi riskli operasyonları yap
psql -U postgres -d myapp <<'EOF'
ALTER TABLE users ADD COLUMN preferences JSONB;
UPDATE users SET preferences = '{}';
-- Beklenmedik bir şey olursa...
EOF

# Eğer sorun çıktıysa bu noktaya dön
# postgresql.conf'a ekle:
# recovery_target_name = 'before_migration_v2_1'

Otomatik Backup Script’i

Production ortamı için düzgün bir otomasyon şart:

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

set -euo pipefail

BACKUP_BASE="/backup/base"
WAL_ARCHIVE="/backup/wal_archive"
RETENTION_DAYS=7
LOG_FILE="/var/log/pg_pitr_backup.log"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="${BACKUP_BASE}/${TIMESTAMP}"

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

# Disk alanını kontrol et
AVAILABLE=$(df -BG "${BACKUP_BASE}" | awk 'NR==2 {print $4}' | tr -d 'G')
if [ "${AVAILABLE}" -lt 20 ]; then
    log "HATA: Yetersiz disk alani: ${AVAILABLE}GB"
    exit 1
fi

log "Base backup basliyor: ${BACKUP_DIR}"

# Base backup al
pg_basebackup 
    -U postgres 
    -D "${BACKUP_DIR}" 
    --format=tar 
    --gzip 
    --compress=9 
    --checkpoint=fast 
    --wal-method=stream 
    --progress 2>>"${LOG_FILE}"

log "Base backup tamamlandi"

# Eski backupları temizle
find "${BACKUP_BASE}" -maxdepth 1 -type d -mtime "+${RETENTION_DAYS}" -exec rm -rf {} ;

# WAL arşivlerini temizle (en eski backup'tan öncesini sil)
OLDEST_BACKUP=$(ls -t "${BACKUP_BASE}" | tail -1)
if [ -n "${OLDEST_BACKUP}" ]; then
    OLDEST_START=$(tar -xOf "${BACKUP_BASE}/${OLDEST_BACKUP}/backup_label.gz" 2>/dev/null 
        | grep "START WAL" | awk '{print $5}' | tr -d '()')
    log "En eski WAL segmenti korunuyor: ${OLDEST_START}"
    # pg_archivecleanup ile eski WAL'ları temizle
    pg_archivecleanup "${WAL_ARCHIVE}" "${OLDEST_START}" 2>>"${LOG_FILE}"
fi

log "Backup islemi basariyla tamamlandi"

# Cron'a eklemek icin:
# 0 2 * * * /usr/local/bin/pg_pitr_backup.sh
chmod +x /usr/local/bin/pg_pitr_backup.sh
# Cron'a ekle
echo "0 2 * * * postgres /usr/local/bin/pg_pitr_backup.sh" | sudo tee /etc/cron.d/pg_pitr_backup

Monitoring ve Doğrulama

PITR altyapınızın sağlıklı çalışıp çalışmadığını düzenli olarak kontrol edin:

# Arşivleme istatistiklerini kontrol et
psql -U postgres -c "
SELECT
    archived_count,
    last_archived_wal,
    last_archived_time,
    failed_count,
    last_failed_wal,
    last_failed_time,
    NOW() - last_archived_time AS son_arsivden_gecen_sure
FROM pg_stat_archiver;"
# WAL üretim hızını ve arşiv gecikmesini izle
psql -U postgres -c "
SELECT
    pg_current_wal_lsn() AS current_lsn,
    pg_walfile_name(pg_current_wal_lsn()) AS current_wal_file,
    (SELECT count(*) FROM pg_ls_archive_statusdir() WHERE name LIKE '%.ready') AS arsivlenmeyi_bekleyen
;"

Dikkat edilmesi gereken durumlar:

  • failed_count > 0: Arşivleme başarısız oluyor, archive_command‘ı kontrol edin
  • arsivlenmeyi_bekleyen > 10: WAL üretim hızı arşivleme hızını geçiyor
  • son_arsivden_gecen_sure > 10 dakika: Arşivleme durmmuş olabilir, archive_timeout‘u gözden geçirin

Uzak Depolama ile PITR

Production’da arşivleri aynı sunucuda tutmak güvenli değil. S3 veya NFS kullanabilirsiniz:

# S3 için archive_command örneği (aws cli kurulu olmalı)
archive_command = 'aws s3 cp %p s3://mybucket/wal_archive/%f --storage-class STANDARD_IA'

# restore_command da buna uygun olmalı
restore_command = 'aws s3 cp s3://mybucket/wal_archive/%f %p'

# NFS mount için
archive_command = 'rsync -a %p backup-server:/mnt/pg_archive/%f'
restore_command = 'rsync -a backup-server:/mnt/pg_archive/%f %p'

Önemli not: archive_command sıfır olmayan exit code döndürürse PostgreSQL WAL segmentini arşivlenmiş saymaz ve tekrar dener. Bu güvenlik mekanizmasını asla görmezden gelmeyin.

Sık Yapılan Hatalar

Yıllar içinde gördüğüm en yaygın PITR hataları şunlar:

  • archive_mode’u açmadan backup almak: WAL arşivleme aktif olmadan alınan backup PITR için kullanılamaz. Her zaman arşivlemenin çalıştığını doğrulayın.
  • restore_command’ı test etmemek: Backup aldınız ama restore komutunu hiç çalıştırmadınız. Gerçek kriz anında çalışmayabilir. Ayda bir restore tatbikatı yapın.
  • Zaman dilimi karışıklığı: recovery_target_time‘da timezone belirtmeyi unutmayın. PostgreSQL sunucunuz UTC’de ama siz yerel saati yazdıysanız kurtarma yanlış noktada duracak. Her zaman 2024-01-15 15:20:33+03 formatında timezone ekleyin.
  • Yeterli WAL’ı saklamamak: wal_keep_size veya arşiv temizleme politikası çok agresif olursa bazı WAL dosyaları kaybolabilir. Recovery sırasında eksik WAL segmenti fatal hataya yol açar.
  • Base backup’ı doğrulamamak: pg_basebackup başarılı çıkış kodu verse de backup bozuk olabilir. Backup’tan hemen sonra pg_restore --list veya tar içeriğini kontrol edin.

Sonuç

PITR, PostgreSQL DBA’sının araç kutusundaki en güçlü özelliklerden biridir. Düzgün kurulduğunda size inanılmaz bir esneklik sağlar: dakikalar içinde herhangi bir geçmiş noktaya dönebilirsiniz. Ancak bu esneklik ancak altyapınızı önceden kurduğunuzda değer taşır; felaket geldiğinde kurmaya başlayamazsınız.

Bu yazıda anlattıklarından en kritik üç nokta şunlar:

  • WAL arşivlemenin aktif ve sağlıklı çalıştığını pg_stat_archiver ile düzenli olarak izleyin
  • Base backup ve restore süreçlerini düzenli olarak test edin, tatbikat yapmayan backup’a güvenmez
  • recovery_target_time‘da her zaman timezone belirtin

PITR’ı production’a almadan önce en az iki kez tam kurtarma testi yapın. Test ortamında sorunsuz çalışan bir PITR sistemi, gerçek krizde sizi sakin tutacak ve şirketinizin verilerini kurtaracaktır.

Yorum yapın