Maildir yedeği "aldım" sanıp kutuyu bozmanın üç yolu
Maildir yedeklemesi teoride en kolay iştir: her mesaj kendi dizininde, kendi başına, bir daha asla değişmeyecek tek bir dosya. Ne kilit var, ne binary bir mbox'ın ortasına yazma derdi. "rsync -a yeter" diye düşünürsünüz ve aylarca yeşil yanan bir cron işiyle içiniz rahat eder. Sorun restore gününde çıkar: kutuyu geri koyarsınız, kullanıcı IMAP'e bağlanır ve okunmuş binlerce mesaj tekrar okunmamış görünür, ya da Dovecot kutuyu açarken UID çakışmasından yakınıp yeniden indeksler, ya da hiç açılmaz.
Üç somut başarısızlık senaryosu var, ve üçünü de gerçekten yaşanmış hâliyle sayıyorum:
- Canlı Maildir üzerinden crash-consistent snapshot. Teslimat ya da bir IMAP oturumu tam ortasındayken alınan snapshot,
dovecot-uidlistdosyasınıcur/içindeki gerçek dosyalarla tutarsız yakalar. Restore edilen kutuda UID eşlemesi kayar. - `tmp/` ve indekslerin körü körüne kopyalanması. Yarım teslimatlar ve yeniden kurulabilir indeksler yedeğe girer; restore'da hem çakışma hem gereksiz şişme yaratır.
- Hiç test edilmemiş depo. En tehlikelisi. Yedek yeşil görünür ama içinde ne olduğunu, açılıp açılmayacağını kimse bilmez.
Bu yazının sözü bir "yedek işi" kurmak değil. Söz şu: her hafta otomatik olarak bir sandbox'a geri yüklenen, `doveadm` ile kanıtlanan ve "en son ne zaman gerçekten restore edildi?" sorusuna bir sayıyla cevap veren bir kurtarma hattı kurmak. Snapshot ve offsite depo herkesin bildiği kısım; asıl değer sağ yarıda.
Maildir anatomisi: neyi yedekle, neyi atla
Bir Maildir kutusu üç dizinden ibaret gibi görünür ama asıl kritik olan, yanındaki meta dosyalardır. Teslimat atomiktir: MDA önce mesajı tmp/ altına yazar, fsync eder, sonra new/ altına rename eder. IMAP istemcisi görünce mesaj cur/ altına taşınır ve dosya adının sonuna bayrak eklenir.
Bayraklar dosya adının içinde kodlanır. :2, bilgi alanından sonrası: :2,S = Seen, :2,RS = Replied+Seen, :2,FS = Flagged+Seen, :2,T = silinmek üzere işaretli, :2,D = taslak. Bu son ek kaybolursa kutunun tüm okundu/yanıtlandı durumu sıfırlanır — kullanıcı için felaket, sizin için "yedek çalıştı ama yanlış çalıştı" davası.
Yedeklenmesi zorunlu olan meta dosyalar:
dovecot-uidlist— UID ile dosya adı eşlemesi. Bu olmadan istemciler her mesajı yeni sanar.dovecot-keywords— özel etiketlerin adları.maildirsize— Maildir++ kota sayacı.subscriptions— IMAP klasör abonelikleri.
Atlanacaklar (yeniden kurulabilir, restore edilirse zarar verir):
dovecot.index,dovecot.index.cache,dovecot.index.log,dovecot.index.log.2— Dovecot bunlarıuidlistve mesaj dosyalarından yeniden üretir. Restore ederseniz eski indeks yeni UID'lerle çakışır.tmp/— her zaman hariç. İçinde yarım teslimatlar var.
Snapshot katmanı: crash-consistent yetmez, app-consistent lazım
Dosya sistemi snapshot'ı tek başına eksiktir. Maildir'in atomik teslimatı cur/ ve new/ içindeki mesaj dosyalarını güvende tutar, ama dovecot-uidlist bir teslimat ya da EXPUNGE ortasında güncellenirken snapshot alınırsa, uidlist ile fiziksel dosyalar arasında bir an için uyumsuzluk yakalanabilir. Sonuç: restore'da yeniden indekslenen bir kutu ve kaybolmuş UID sürekliliği.
İki doğru yaklaşım var:
- App-consistent kopya çıkar, snapshot'ı onun üstünden al. Dovecot'un
dsyncaltyapısı bunun için var:
# Kutuyu uidlist ile uyumla (indeksi tazeler)
doveadm force-resync -u [email protected] '*'
# Ya da tutarlı, bağımsız bir kopya üret
doveadm backup -u [email protected] maildir:/backup/staging/test- ZFS/Btrfs snapshot + doveadm senkronu. Canlı
vmaildataset'inde önce resync yapıp hemen ardından snapshot almak pratikte tutarlı bir görüntü verir. Snapshot atomiktir ve saniyeler sürer — canlı trafiği durdurmaz.
zfs snapshot rpool/vmail@$(date +%Y%m%d-%H%M)
zfs list -t snapshot rpool/vmail
# Artımlı offsite gönderim
zfs send -i rpool/vmail@dun rpool/vmail@bugun | ssh backup 'zfs recv tank/vmail'LVM tarafında lvcreate -L 5G -s -n vmail_snap /dev/vg0/vmail, Btrfs tarafında btrfs subvolume snapshot -r /var/mail /var/mail/.snap/$(date +%F) aynı işi görür. Hangisi olursa olsun kural sabit: snapshot'ı restic'e verilecek tutarlı bir kaynak olarak kullan, canlı Maildir'e doğrudan restic salma.
Offsite şifreli depo: restic ile dedup + retention
3-2-1 kuralı: en az 3 kopya, 2 farklı ortam, 1'i offsite. Snapshot yerelde ilk iki kopyayı verir; offsite şifreli depo üçüncüyü. Milyonlarca küçük dosyadan oluşan bir Maildir'de restic (ya da borg) çıplak rsync'e göre iki net üstünlük sağlar: blok seviyesinde dedup ve şifreleme. Aynı mesaj birden fazla kutuda ya da bir taşımadan sonra iki yerde duruyorsa disk üzerinde bir kez saklanır.
restic init --repo sftp:backup@offsite:/srv/restic/evilmail
restic backup /var/mail/vhosts \
--exclude='dovecot.index*' \
--exclude='tmp/' \
--tag maildir
restic forget --keep-daily 7 --keep-weekly 8 --keep-monthly 12 --pruneDepo bütünlüğünü düzenli tara. restic check metadata'yı doğrular; asıl bit-çürümesini yakalamak için veriyi de oku, ama her gece hepsini okumak pahalı olduğundan haftaya böl:
# Her gün verinin 1/7'sini oku; hafta sonunda tüm depo taranmış olur
restic check --read-data-subset=1/7Otomasyonu systemd timer ile kur
Cron çalıştırır ama gözlemlemez. systemd timer başarısızlığı journald'a yazar, OnFailure= ile alarm ünitesi tetikler ve Persistent=true ile makine kapalıyken kaçan çalıştırmayı telafi eder. Sıralı hat tek scriptte: doveadm force-resync → ZFS snapshot → restic backup → restic forget --prune.
# /etc/systemd/system/maildir-backup.service
[Unit]
Description=Maildir app-consistent yedek + restic push
OnFailure=alert@%n.service
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/maildir-backup.sh# /etc/systemd/system/maildir-backup.timer
[Unit]
Description=Gunluk Maildir yedegi
[Timer]
OnCalendar=*-*-* 03:15:00
Persistent=true
RandomizedDelaySec=600
[Install]
WantedBy=timers.targetScript'in başında flock ile üst üste binmeyi engelleyin — uzayan bir prune bir sonraki çalıştırmayla çakışmasın:
#!/usr/bin/env bash
exec 9>/run/maildir-backup.lock
flock -n 9 || { echo "Onceki calisma surunuyor, atliyorum"; exit 0; }
set -euo pipefailÇoklu domain ortamında her kutu için ayrı iş kurmayın; tek vmail dataset'ini bütün olarak yedekleyin. /var/mail/vhosts/{domain}/{user}/ yapısı tek bir kaynağın altında olduğundan snapshot bütün domainleri tutarlı bir anda yakalar, restic dedup zaten tekrarları eler.
Asıl mesele: haftalık otomatik ve doğrulanabilir geri yükleme
İşte makalenin çekirdeği. Yukarıdaki her şey "yedek almak"tı. Şimdi onu her hafta gerçekten geri yüklüyoruz — canlı sistemi kirletmeden, ayrı bir sandbox yolunda.
restic restore latest \
--target /var/mail/restore-test \
--include /var/mail/vhosts/evilmail.pro/test
# restic hedef dizinin altında tam yolu korur:
SANDBOX=/var/mail/restore-test/var/mail/vhosts/evilmail.pro/testDoğrulama üç katman, hepsi yeşil olmadan test başarılı sayılmaz:
1. Mesaj sayısı. Kaynak snapshot'la restore edilenin dosya sayısı birebir tutmalı:
find "$SANDBOX"/{cur,new} -type f | wc -l
# Snapshot içindeki dosya sayısıyla karşılaştır2. İçerik checksum manifesti. Yedek alınmadan önce üretilmiş sha256 listesini restore sonrası doğrula. Tek bir bozuk mesaj bile yakalanır:
# Yedek öncesi (kaynak kutunun içinde):
cd /var/mail/vhosts/evilmail.pro/test
find cur new -type f -exec sha256sum {} + > manifest.sha256
# Restore sonrası (sandbox'ta, aynı göreli yollarla):
cd "$SANDBOX" && sha256sum -c manifest.sha2563. Uygulama doğrulaması. En önemlisi. Dosyalar sağlam olabilir ama Dovecot yine de açamayabilir. Doğrulamanın canlı kutuya bulaşmaması için mail konumunu sandbox'a sabitleyin (-o mail=maildir:...); force-resync hatasız dönmeli ve bilinen bir mesaj gerçekten çekilebilmeli:
doveadm -o mail=maildir:"$SANDBOX" force-resync -u verify@localhost '*' # çıkış kodu 0
doveadm -o mail=maildir:"$SANDBOX" search -u verify@localhost ALL | wc -l # beklenen sayı
doveadm -o mail=maildir:"$SANDBOX" fetch -u verify@localhost 'guid text' mailbox INBOX uid 1Üçü de geçtiyse restore kanıtlanmıştır. Test biter bitmez /var/mail/restore-test sandbox'ını temizleyin ki bir sonraki koşu temiz başlasın.
Kanıtı ölç: son doğrulanmış geri yükleme metriği ve alarm
Yedeğiniz yeşil yanabilir ama tek önemli soru şudur: "en son ne zaman gerçekten restore edilip doğrulandı?" Bunun cevabı bir cümle değil, bir sayı olmalı. Doğrulama scripti üç katmanı da geçtiğinde bir zaman damgası yazsın:
echo "maildir_restore_verified_timestamp $(date +%s)" \
> /var/lib/node_exporter/textfile/restore.promPrometheus tarafında alarmın kritik özelliği, sadece bozuk restore'u değil doğrulamanın hiç yapılmamasını da yakalamasıdır. Sessiz başarısızlık en tehlikeli senaryodur çünkü hiçbir hata mesajı üretmez:
# Doğrulama 8 günden eskiyse tetikle (haftalık + 1 gün pay)
time() - maildir_restore_verified_timestamp > 691200RPO hedefiniz günlük snapshot'la 24 saat. RTO'yu tahmin etmeyin, ölçün: restore + force-resync süresini script'te zamanlayıp bir metrik olarak yazın. Felaket anında "ne kadar sürer?" sorusunun cevabı elinizde hazır olsun.
Felaket provası checklist
- [ ] Yedek
doveadmile app-consistent kopyadan mı alınıyor (canlı Maildir'e doğrudan restic yok)? - [ ]
tmp/vedovecot.index*dışlanıyor mu (--exclude)? - [ ]
dovecot-uidlist,dovecot-keywords,maildirsize,subscriptionsyedeğe dahil mi? - [ ]
restic check --read-data-subsetson 7 günde tüm depoyu taradı mı? - [ ] Sandbox restore +
doveadm force-resynchatasız (çıkış kodu 0) mu? - [ ] Mesaj sayısı eşit ve
sha256sum -cmanifesti temiz mi?
Test etmediğiniz yedeği yedek saymayın.


