LVM-Snapshots

Grundlagen

LVM, der Linux Logical Volume Manager, bietet die Möglichkeit, von einzelnen logischen Laufwerken (Logical Volumes, LVs) einen sog. Snapshot zu erzeugen. Ein Snapshot verhält sich wie eine unabhängige Kopie des ursprünglichen Volume. Allerdings speichert der Snapshot nur die Veränderungen gegenüber dem Original, d.h. es wird potenziell erheblich weniger Platz benötigt. Tatsächlich kann die Größe eines Snapshot-LV beliebig gewählt werden (unabhängig von der Größe des Originals). Wenn allerdings der Platz im Snapshot nicht ausreicht, um alle Veränderungen aufzunehmen, wird der Snapshot ungültig. Wenn man auf Nummer sicher gehen will, sollte daher der Snapshot die gleiche Größe haben wie das Original.

Technisch handelt es sich bei einem Snapshot um eine COW-Tabelle (copy on write). Das bedeutet: Jede Veränderung, die am Snapshot vorgenommen wird, wird in dieser Tabelle vermerkt, und das Original-Volume wird nicht verändert. Wird hingegen in das Original-Volume geschrieben, so werden die ursprünglichen Daten ebenfalls in die COW-Tabelle übertragen. Somit ist die Veränderung am Original im Snapshot nicht sichtbar. Falls die gleichen Daten im Snapshot bereits früher geändert worden sind (d.h. es existiert bereits ein Eintrag in der COW-Tabelle), so wird diese Veränderung natürlich nicht überschrieben.

Der Device Mapper

Die Funktionalität von LVM wird größtenteils durch den Device Mapper erledigt. Dieser kann virtuelle Block Devices bereitstellen und sorgt dafür, dass jeder Zugriff auf diese virtuellen Devices auf ein anderes darunter liegendes Device umgeleitet wird. Dabei gibt es verschiedene Targets, d.h. Kernel-Module, die das eigentliche Mapping übernehmen.

Ein einfaches LV wird normalerweise über das Target linear realisiert. Dieses bildet einen zusammenhängenden Bereich des virtuellen Devices auf einen ebenfalls zusammenhängenden Bereich des darunter liegenden Devices ab. Ein Beispiel:

test:~ # lvcreate -l 2 -n base vg0
 Logical volume "base" created
test:~ # dir /dev/vg0
total 0
lrwxrwxrwx 1 root root 7 Jul 4 09:33 base -> ../dm-0
test:~ # dir /dev/mapper
total 0
crw------- 1 root root 10, 236 Jul 4 09:28 control
lrwxrwxrwx 1 root root 7 Jul 4 09:33 vg0-base -> ../dm-0
test:~ # dmsetup table vg0-base
0 16384 linear 202:4 2048

Dabei ist das Gerät 202,4 das physische Laufwerk /dev/xvda4, das zum Aufbau der Volume Group vg0 verwendet wurde:

test:~ # dir /dev/xvda*
brw-rw---- 1 root disk 202, 0 Jul 7 16:16 /dev/xvda
brw-rw---- 1 root disk 202, 1 Jul 7 16:16 /dev/xvda1
brw-rw---- 1 root disk 202, 2 Jul 7 16:16 /dev/xvda2
brw-rw---- 1 root disk 202, 3 Jul 7 16:16 /dev/xvda3
brw-rw---- 1 root disk 202, 4 Jul 7 16:16 /dev/xvda4

Vorbereitung des Original-Volumes

Für die späteren Tests wird das LV mit definiertem Inhalt gefüllt, z.B. mit dem Text „base“. Da die Volume Group eine Physical Extent Size von 4 MB hat und das LV mit einer Größe von 2 Extents angelegt wurde, beträgt die Größe 8 MB:

test:~ # lvdisplay /dev/vg0/base | grep 'LV Size'
 LV Size 8.00 MiB

Zum Füllen wird der Befehl dd verwendet:

test:~ # for i in $(seq 1 2097152); do 
> echo -n base
> done | dd of=/dev/vg0/base bs=4
 2097152+0 records in
 2097152+0 records out
 8388608 bytes (8.4 MB) copied, 56.5065 s, 148 kB/s

Um zu überprüfen, ob die Daten korrekt geschrieben worden sind, kann ebenfalls dd verwendet werden:

test:~ # dd if=/dev/vg0/base bs=1 count=32
 basebasebasebasebasebasebasebase

Anlegen des Snapshot

Nun wird der Snapshot angelegt, und zwar mit einer Größe von 1 Extent (4 MB):

test:~ # lvcreate -s /dev/vg0/base -n snap -l 1
Logical volume "snap" created

Im Verzeichnis /dev/vg0 sieht man anschließend den Snapshot als eigenes LV. Außerdem ist natürlich weiterhin das Original-Volume vorhanden:

test:~ # dir /dev/vg0
 total 0
 lrwxrwxrwx 1 root root 7 Jul 4 09:35 base -> ../dm-0
 lrwxrwxrwx 1 root root 7 Jul 4 09:35 snap -> ../dm-1

Deutlich interessanter ist jedoch der Blick in das Verzeichnis /dev/mapper. Hier sieht man, dass neben den beiden nach außen sichtbaren Geräten (die man auch in /dev/vg0 findet) zwei weitere Geräte angelegt worden sind, nämlich vg0-base-real und vg0-snap-cow:

test:~ # dir /dev/mapper
 total 0
 crw------- 1 root root 10, 236 Jul 4 09:28 control
 lrwxrwxrwx 1 root root 7 Jul 4 09:35 vg0-base -> ../dm-0
 lrwxrwxrwx 1 root root 7 Jul 4 09:35 vg0-snap -> ../dm-1
 lrwxrwxrwx 1 root root 7 Jul 4 09:35 vg0-snap-cow -> ../dm-3
 lrwxrwxrwx 1 root root 7 Jul 4 09:35 vg0-base-real -> ../dm-2

Ebenfalls interessant ist die Ausgabe von dmsetup für diese Geräte:

test:~ # dmsetup table vg0-base
0 16384 snapshot-origin 253:2
test:~ # dmsetup table vg0-base-real
0 16384 linear 202:4 2048
test:~ # dmsetup table vg0-snap
0 16384 snapshot 253:2 253:3 P 8
test:~ # dmsetup table vg0-snap-cow
0 8192 linear 202:4 18432

Wie man sieht, ist durch den Snapshot eine weitere Mapping-Ebene hinzugekommen. Das bisherige Gerät für das Original-LV (vg0-base) ist nicht länger vom Typ linear, sondern stattdessen vom Typ snapshot-origin. Dieser verweist auf das Gerät vg0-base-real. Das neu hinzugekommene Gerät vg0-snap ist vom Typ snapshot. Dieser verweist ebenfalls auf das Gerät vg0-base-real sowie auf vg0-snap-cow. Diese beiden Geräte wiederum verhalten sich wie „normale“ LVs, d.h. sie verweisen auf einen Bereich des physischen Volumes.

lvm-snapshots

Betrachtet man die Größe der vier Block Devices, so sind die beiden Devices vg0-base und vg0-base-real natürlich jeweils 8 MB groß. vg0-base-real ist ja das anfangs angelegte Original-Volume, und vg0-base ist lediglich eine darüber gelegte Mapping-Schicht. Auch das Device vg0-snap ist (virtuell) 8 MB groß, da ein Snapshot immer die gleiche Größe hat wie das darunter liegende Original-Volume. Die COW-Tabelle vg0-snap-cow ist hingegen nur 4 MB groß, genau die Größe, die beim Anlegen des Snapshot angegeben wurde.

test:~ # blockdev --getsize64 /dev/mapper/vg0-base /dev/mapper/vg0-snap /dev/mapper/vg0-snap-cow /dev/mapper/vg0-base-real
 8388608
 8388608
 4194304
 8388608

Veränderungen am Snapshot

Was passiert, wenn der Snapshot mit Daten beschrieben wird?

test:~ # echo -n snapshot | dd of=/dev/vg0/snap bs=1
8+0 records in
8+0 records out
8 bytes (8 B) copied, 0.0341143 s, 0.2 kB/s

test:~ # dd if=/dev/mapper/vg0-snap bs=1 count=32
snapshotbasebasebasebasebasebase

test:~ # dd if=/dev/mapper/vg0-base bs=1 count=32
basebasebasebasebasebasebasebase

Erwartungsgemäß sind die Daten im Snapshot verändert worden, während das Original-Volume unverändert geblieben ist. Interessant ist der Blick auf die darunter liegenden Devices:

test:~ # dd if=/dev/mapper/vg0-base-real bs=1 count=32
basebasebasebasebasebasebasebase
test:~ # dd if=/dev/mapper/vg0-snap-cow | hexdump -C
00000000  53 6e 41 70 01 00 00 00  01 00 00 00 08 00 00 00  |SnAp............|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00001000  00 00 00 00 00 00 00 00  02 00 00 00 00 00 00 00  |................|
00001010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00002000  73 6e 61 70 73 68 6f 74  62 61 73 65 62 61 73 65  |snapshotbasebase|
00002010  62 61 73 65 62 61 73 65  62 61 73 65 62 61 73 65  |basebasebasebase|
*
00003000  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00400000

Die veränderten Daten sind in die COW-Tabelle geschrieben worden, während das Original-LV unverändert geblieben ist. Auffällig ist, dass trotz der kleinen Änderung (4 Bytes) der in die COW-Tabelle geschriebene Block mit 4096 Bytes erheblich größer ist. Die Größe der pro Änderung geschriebenen Blöcke ist die Snapshot chunk size, die bei der Anlage des Snapshot eingestellt werden kann. Aufgrund der Belegung der COW-Tabelle in Chunks lässt sich mit gleichmäßig über den Snapshot verteilten kleinen Änderungen die COW-Tabelle füllen, so dass der Snapshot ungültig wird, obwohl die Gesamtmenge der veränderten Daten weit unterhalb der Größe des Snapshot liegt.

Veränderungen am Original-Volume

Was passiert, wenn das Original-Volume verändert wird?

test:~ # echo -n test | dd of=/dev/vg0/base bs=1 seek=4096
4+0 records in
4+0 records out
4 bytes (4 B) copied, 0.0174384 s, 0.2 kB/s

test:~ # dd if=/dev/mapper/vg0-base bs=1 count=32 skip=4096
testbasebasebasebasebasebasebase

test:~ # dd if=/dev/mapper/vg0-base-real bs=1 count=32 skip=4096
testbasebasebasebasebasebasebase

test:~ # dd if=/dev/mapper/vg0-snap-cow | hexdump -C
00000000 53 6e 41 70 01 00 00 00 01 00 00 00 08 00 00 00 |SnAp............|
00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
00001000 00 00 00 00 00 00 00 00 02 00 00 00 00 00 00 00 |................|
00001010 01 00 00 00 00 00 00 00 03 00 00 00 00 00 00 00 |................|
00001020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
00002000 73 6e 61 70 73 68 6f 74 62 61 73 65 62 61 73 65 |snapshotbasebase|
00002010 62 61 73 65 62 61 73 65 62 61 73 65 62 61 73 65 |basebasebasebase|
*
00004000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
00400000

Dass die beiden Geräte vg0-base und vg0-base-real die Veränderung zeigen, ist nicht weiter überraschend. Spannender ist die COW-Tabelle: Auf den ersten Blick sieht es so aus, als hätte sich diese gar nicht verändert. Tatsächlich ist jedoch ein weiterer 4k-Block hinzugekommen, in den die ursprünglichen Daten („…basebasebasebase…“) kopiert worden sind. Daher ist die Veränderung im Snapshot nicht sichtbar. Die Tatsache, dass die COW-Tabelle in 4k-Blöcken belegt wird, ist übrigens auch der Grund, warum beim Beschreiben mittels seek=4096 weiter hinten in das Volume geschrieben worden ist.

Quellen / Links

Schreibe einen Kommentar