4 minutes
VM Backup mit ZFS send/receive
Da im Allgemeinen gilt “Kein Backup, kein Mitleid” müssen meine VMs auch irgendwie gesichert werden. Da diese auf ZFS Datasets liegen, bietet sich die ZFS Funktion send/receive für das Backup an. Hierbei muss auch nicht jedes Mal das komplette VM-Image gesichert werden, sondern nur die Blöcke, die sich seit dem letzten Snapshot geändert haben.
Wenn man jetzt allerdings hingeht und einfach das Image einer laufenden VM kopiert, wird man später auf jeden Fall einen fsck
benötigen, da das Filesystem der VM nicht in einem sauberen Zustand ist. Daher beginnt mein Backup-Skript damit einen VM-Snapshot, möglichst mit der Option -quiesce
, zu machen. Hierfür muss in der VM der qemu-guest-agent
installiert sein und laufen.
VMDISKS=$(virsh domblklist $VM | grep qcow2 | egrep -o [vs]d.)
DISKSPEC=""
for disk in $VMDISKS
do
DISKSPEC+="--diskspec $disk,file=/srv/snapshots/$VM/$VM-$disk-$SNAPNAME.qcow2,snapshot=external "
done
# create snapshot dataset
zfs create tank/snapshots/$VM
# create virsh xml-dump of vm
virsh dumpxml $VM > /srv/vms/$VM/vm-$VM.xml
echo "$(date +%Y-%m-%d_%H:%M:%S): Create VM snapshot of $VM"
virsh snapshot-create-as --domain $VM --name $SNAPNAME --quiesce --atomic --disk-only $DISKSPEC >/dev/null 2>&1
if [ $? -ne 0 ]
then
virsh snapshot-create-as --domain $VM --name $SNAPNAME --disk-only $DISKSPEC >/dev/null 2>&1
if [ $? -ne 0 ]
then
echo "VM snapshot creation failed" >&2
abortFunction
else
echo "$(date +%Y-%m-%d_%H:%M:%S): Snapshot created without --quiesce"
fi
fi
Im ersten Schritt werden die Disks der VM ausgelesen. Von der VM wird dann ein --disk-only
Snapshot, also Snapshot ohne RAM der VM, angelegt. Sollte dies nicht funktionieren, da z.B. der qemu-guest-agent
nicht läuft, wird ein Snapshot ohne --quiesce
probiert. Die Snapshot-Delta-Files werden auf ein anderes ZFS-Fileset abgelegt, da diese nicht mitgesichert werden sollen. Per virsh dumpxml
wird auch die Config der VM in das Dataset der VM gelegt, damit diese Config mitgesichert werden kann.
Nach diesen Vorarbeiten kann der ZFS-Snapshot für das spätere zfs send
gemacht werden. Das QCOW2-Image sollte jetzt in einem sauberen Zustand sein.
echo "$(date +%Y-%m-%d_%H:%M:%S): Creating ZFS snapshot $SRCZFS@$SNAPNAME"
zfs snapshot "$SRCZFS"@"$SNAPNAME"
if [ $? -ne 0 ]
then
echo "ZFS snapshot creation failed" >&2
abortFunction
fi
echo "$(date +%Y-%m-%d_%H:%M:%S): Deleting vm disk snapshots"
for disk in $VMDISKS
do
virsh blockcommit $VM $disk --pivot --active >/dev/null
done
Sobald der ZFS-Snapshot gemacht ist, kann der VM Snapshot wieder gelöscht werden. Dies läuft über den virsh blockcommit
Befehl ab. Hierbei werden die Änderungen im Snapshot-Delta in das Ursprungs-QCOW2 zurück geschrieben und der Pointer für die VM wieder auf das Original-Image gelegt. Da zwischen dem Anlegen des VM-Snapshots und dem Löschen desselben nur der Step des ZFS-Snapshots, der wirklich nur sehr kurz dauert, liegt, ist das Delta nur sehr klein und das blockcommit
geht schnell und ohne Probleme.
VMDISKS=$(virsh domblklist $VM | egrep [vs]d.)
for disk in $VMDISKS
do
if [[ $disk == *"snapshot"* ]]
then
echo "Snapshot deletion failed for $disk" >&2
abortFunction
fi
done
virsh snapshot-delete $VM $SNAPNAME --metadata > /dev/null
Wenn diese Aktion fertig ist, wird noch kurz geprüft ob die Pointer wirklich wieder auf den normalen Disks sind. Daraufhin kann der Snapshot auch aus libvirt gelöscht werden.
SUCCESS=$(zfs get backup:success -H -o value $SRCZFS)
LASTBACKUP=$(zfs get backup:date -H -o value $SRCZFS)
if [[ $SUCCESS == "true" ]]
then
LASTSNAP="backup_$LASTBACKUP"
echo "$(date +%Y-%m-%d_%H:%M:%S): Sending incremental snapshot from $LASTSNAP"
ssh $DESTHOST sudo zfs rollback "$DESTZFS"@"$LASTSNAP"
zfs send -R -i "$SRCZFS"@"$LASTSNAP" "$SRCZFS"@"$SNAPNAME" | pv -L $LIMIT | ssh $DESTHOST sudo zfs recv $DESTZFS
else
if [[ $LASTBACKUP != "-" ]]
then
LASTSNAP="backup_$LASTBACKUP"
echo "$(date +%Y-%m-%d_%H:%M:%S): Trying to send incremental snapshot from $LASTSNAP"
ssh $DESTHOST sudo zfs rollback "$DESTZFS"@"$LASTSNAP"
zfs send -R -i "$SRCZFS"@"$LASTSNAP" "$SRCZFS"@"$SNAPNAME" | pv -L $LIMIT | ssh $DESTHOST sudo zfs recv $DESTZFS
else
echo "$(date +%Y-%m-%d_%H:%M:%S): Sending full snapshot"
zfs send -R "$SRCZFS"@"$SNAPNAME" | pv -L $LIMIT | ssh $DESTHOST sudo zfs recv -F $DESTZFS
fi
fi
Daraufhin beginnt der eigentliche Backup-Step. Wenn es schon ein vorheriges Backup gibt, das erfolgreich übertragen wurde, wird nur der Diff zum letzten Snapshot übertragen, ansonsten der komplette Snapshot.
if [ $? -eq 0 ]
then
zfs set backup:success="true" $SRCZFS
zfs set backup:date="$DATE" $SRCZFS
zfs set backup:failed="-" $SRCZFS
if [ -n $LASTSNAP ]
then
echo "$(date +%Y-%m-%d_%H:%M:%S): Deleting old ZFS snapshot $LASTSNAP"
zfs destroy "$SRCZFS"@"$LASTSNAP"
fi
else
abortFunction
fi
zfs destroy tank/snapshots/$VM
# cleanup old backups
OLDSNAPS=$(ssh $DESTHOST zfs list -r -t snapshot -o name $DESTZFS 2>/dev/null | grep $DATEOLD)
if [ ${#OLDSNAPS} -gt 0 ]
then
for oldsnap in $OLDSNAPS
do
echo "$(date +%Y-%m-%d_%H:%M:%S): Deleting old snapshots on target"
ssh $DESTHOST sudo zfs destroy $oldsnap
done
else
echo "$(date +%Y-%m-%d_%H:%M:%S): No old snapshots to cleanup on target"
fi
echo "$(date +%Y-%m-%d_%H:%M:%S): Backup Completed"
Die letzten Steps sorgen noch dafür, dass der Timestamp des erfolgreichen Backups als ZFS Variable gespeichert wird. Außerdem werden auf dem Live-System alle, außer der letzte und auf dem Backup-System alle Snapshots, die älter sind als die konfigurierte Aufbewahrungsdauer, gelöscht.