Xen-Server sichern mit LVM-Snapshots

(2009-10-08)

Wenn man einen Server mit Xen virtualisiert, steht man beim Einrichten des Backups vor der Frage, wie man das Backup organisiert. Hier gibt es (mindestens) die folgenden Ansätze:

  1. Man betrachtet jede virtuelle Instanz als eigenständig und installiert dort jeweils die Backupsoftware.

    Vorteil: Keine zusätzliche/neue Konfiguration nötig für den Host-Rechner.
    Nachteil: Pro Instanz ein Backup-Job nötig, je nach Anzahl der Instanzen wird es schnell unübersichtlich.
  2. Man fertigt vor dem Backup einen kompletten Snapshot der Instanzen an und sichert diesen dann auf dem Hostrechner.

    Vorteil: Weniger Konfigurationsaufwand
    Nachteil: Zeitaufwändig, Speicherplatzaufwändig, Wiederherstellung einzelner Dateien kompliziert/zeitaufwändig.
  3. Man benutzt LVM und fertigt einen Snapshot vor der Sicherung an, den man dann ins Dateisystem einhängt und einfach den kompletten Hostrechner sichert.

    Vorteil: Spart Zeit, Speicherplatz, ermöglicht Wiederherstellung einzelner Dateien
    Nachteil: Je nach Konfiguration ist es etwas umständlich, die Partitionen aus den einzelnen Logical Volumes zu mounten.

Ich möchte in diesem Artikel näher auf die dritte Methode eingehen, da bei allen anderen die Nachteile überwiegen. Weiterhin bietet sich die Methode mit den LVM-Snapshots ohnehin an wenn man seine Instanzen in jeweils einem Logical Volume aufgesetzt hat (damit man später bei Bedarf die Größe ändern kann – das ist bei einer herkömmlichen Installation deutlich komplizierter).

Installation von einer domU auf einem LV

Für die Installation einer domU auf einem LV geht man ganz normal vor (wie beispielsweise im Debian Wiki beschrieben), nur dass man bei der Konfiguration der Festplatte den LVM-Pfad zu seinem LV angibt. Wenn also die Volume Group in.zekjur.net heißt und das LV domu-infra, sieht der passende Eintrag folgendermaßen aus:

disk = ['phy:in.zekjur.net/domu-infra,xvda,w']

Der Name des LVs ist übrigens später noch wichtig. Über den Prefix domu- identifizieren wir die LVs, von denen wir einen Snapshot anfertigen werden.

Das Prinzip von LVM-Snapshots

Snapshots werden angefertigt, indem man ein neues LV erstellt und beim Erstellen angibt, von welchem LV dieses ein Snapshot werden soll. Bei der Erstellung kann man, wie üblich, Name und Größe angeben. Anschließend hat man ein neues LV, welches bei Zugriff dieselben Daten enthält wie das Original zum Zeitpunkt der Anfertigung des Snapshots. Das sieht in etwa so aus:

# lvcreate -n snap_infra -L 1G -s in.zekjur.net/domu-infra
Logical volume "snap_infra" created
# lvs
LV         VG            Attr   LSize  Origin     Snap%  Move Log Copy% Convert
domu-infra in.zekjur.net owi-ao 10,00G                                          
root       in.zekjur.net -wi-ao 10,00G                                          
snap_infra in.zekjur.net swi-a-  1,00G domu-infra   0,00                        
swap_1     in.zekjur.net -wi-ao  9,47G                                          

Die vorhin angegebene Größe dient nun dazu, die Änderungen an einer der beiden Versionen (Original oder Snapshot) zu speichern. Das ist zum Beispiel dann nötig, wenn sich die Originaldaten verändern (Logfiles etc.), aber auch, wenn sich Daten im Snapshot verändern (wir werden das Journal später im Snapshot „reparieren” müssen). Ich benutze als Größe 1 GB, was locker ausreichen sollte für meine Daten. Wenn sich mehr Daten ändern, muss man diesen Wert natürlich entsprechend erhöhen.

Die Problematik: Partitionen im LV

Sofern man nicht absichtlich die domU auf die komplette Platte (/dev/xvda) installiert hat, steht man nun vor einem kleinen Problem: Innerhalb des LVs befindet sich nun eine Partitionstabelle. Auf die einzelnen Partitionen kann man nun leider nicht bequem zugreifen. Stattdessen muss man das Offset der gewünschten Datenpartition herausfinden und diese dann loop-mounten. Damit es einfach bleibt, gehe ich davon aus, dass die gewünschte Datenpartition die erste ext3-Partition ist, alle anderen Partitionen werden ignoriert.

Um besagtes Offset zu finden, kann man sich parted zu nutze machen, welchem man folgendermaßen die Partitionstabelle in der passenden Einheit entlocken kann:

# parted /dev/mapper/in.zekjur.net-snap_infra
GNU Parted 1.8.8
Using /dev/mapper/in.zekjur.net-snap_infra
Welcome to GNU Parted! Type 'help' to view a list of commands.
(parted) unit                                                             
Unit?  [compact]? B                                                       
(parted) print                                                            
Model: Linux device-mapper (snapshot) (dm)
Disk /dev/mapper/in.zekjur.net-snap_infra: 10737418240B
Sector size (logical/physical): 512B/512B
Partition Table: msdos

Number  Start         End           Size          Type      File system  Flags
 1      32256B        10232248319B  10232216064B  primary   ext3         boot 
 2      10232248320B  10733990399B  501742080B    extended                    
 5      10232280576B  10733990399B  501709824B    logical   linux-swap        

(parted)                                                                  

In diesem Fall müssen wir also das LV ab Stelle 32256 benutzen, was folgendermaßen machbar ist:

# losetup -o 32256 -f /dev/mapper/in.zekjur.net-snap_infra 
# losetup -a
/dev/loop0: [000c]:2119316 (/dev/mapper/in.zekjur.net-snap_infra), offset 32256

Wenn man nun allerdings versucht, /dev/loop0 direkt zu mounten, wird dies nicht gelingen (ich gehe von ext3 als Dateisystem aus). Das liegt daran, dass das Journal nicht auf die „Platte” geschrieben wurde, schließlich haben wir das Dateisystem nicht korrekt unmounted innerhalb unserer domU.

Glücklicherweise kann man in LVM2 auch schreibend auf Snapshots zugreifen, sodass wir mit einem Aufruf von fsck das Journal „reparieren” können:

# fsck.ext3 -y /dev/loop0
e2fsck 1.41.3 (12-Oct-2008)
/dev/loop0: recovering journal
/dev/loop0: clean, 53600/624624 files, 366091/2498099 blocks

Anschließend kann man das Dateisystem ganz normal mounten und sichern.

Automatisieren

Da ich diesen Vorgang gerne automatisiert in meine Backup-Software einbinden würde, habe ich dazu ein Script geschrieben, welches alle oben genannten Schritte durchführt. Aufgeteilt sind die Scripts in mount und unmount, weiterhin gibt es ein foreach-domu-Script, welches für alle LVs, deren Name mit domu- beginnt (muss er ohnehin für die anderen Scripts) die passende Aktion ausführt.

Herunterladen kannst du dir die Scripts via git:

# git clone git://code.stapelberg.de/xen-lvm-snapshot

Anschauen kannst du dir sie im Webinterface.

Einbinden in bacula

Durch die „Client Run Before Job”- und „Client Run After Job”-Optionen von bacula ist das Einbinden ziemlich einfach:

Job {  
        Name = "in.zekjur.net"
        Type = Backup
        Client = in.zekjur.net-fd
        FileSet = "in.zekjur.net-set"
        Schedule = "in.zekjur.net-sched"
        Storage = in.zekjur.net-storage
        Messages = Standard
        Priority = 10
        Write Bootstrap = "/raid/bacula/in.zekjur.net/bootstrap"
        Pool = in.zekjur.net
        Maximum Concurrent Jobs = 1
        Spool Attributes = no
        Client Run Before Job = "/root/bin/xen-lvm-snapshot/foreach-domu.sh mount"
        Client Run After Job = "/root/bin/xen-lvm-snapshot/foreach-domu.sh unmount"
}

Anschließend sollte man sicherstellen, dass /mnt im FileSet nicht von der Sicherung ausgenommen wird.