Backup einer Platte mit dd ?

Homwer

New Member
Hallo,
in meinem Setup habe ich zwei SSD (sda, sdb) als md0 (Boot) (sdd, sdc) md1 (/) und zwei drehende Platten als md2 (Daten)

Vor einem größeren Upgrade des Systems würde ich gerne ein Backup der ganzen Platte machen, statt wie üblich nur der Daten, so dass ich bei Problemen einfach alles zurückspielen kann.

Daher die Frage, ist dd eine valide Möglichkeit? Ich würde dann einfach die komplette SSD (sda) per dd als image sichern, das müsste man dann doch einfach zurück spielen können wenn es Probleme gibt, oder?
 

d3p

Blog Benutzer
Ich würde dann einfach die komplette SSD (sda) per dd als image sichern, das müsste man dann doch einfach zurück spielen können wenn es Probleme gibt, oder?
Theoretisch könnte das klappen.
Das steht und fällt mit den jeweiligen Services die darauf laufen.

Läuft da eine Datenbank drauf und du sicherst die Kiste via dd, ohne dass die Datenbank einen konsistenten Stand hat, bringt dir das nichts.
 

d4f

Müder Benutzer
Falls es nicht bereits so geplant ist - das dd-Image lässt sich generell gut mit "pigz" (gzip mit multithreading) komprimieren.
 

DeaD_EyE

Blog Benutzer
Boote vom Rescue-System, dann kannst du die Platten sichern. Das remounten in read-only funktioniert nicht, solange Prozesse auf das FS zugreifen. Im Betrieb ist das nicht möglich. Das Backup vom Rescue-System aus zu machen, ist besser.
 

Joe User

Zentrum der Macht
Warum zum Teufel will man sein Backup völlig unnötig mit "freiem Speicherplatz" aufblähen und obendrein auch noch potentielle Filesystem-Schäden/-Bugs wiederherstellen und damit das Backup ad absurdum führen?


dd ist kein Backup-Tool, noch nie gewesen und wird es niemals sein!


Ein richtiges Backup beinhaltet ausschliesslich Nutzdaten und grundsätzlich auf einem frisch installiertem und konfiguriertem System eingespielt.
 

Homwer

New Member
Danke für die Hilfreichen Antworten.
Schade dass @Joe User wohl keine Zeit hatte den Beitrag zu lesen und zu verstehen.

Habe mir ein Backup der Platte Offline gemacht und das zurückspielen auf einer anderen Platte ausprobiert. Funktioniert ohne Probleme. Das anschließende Upgrade des Systems auf das aktuelle Debian lief danach problemlos so dass ich das Backup nicht brauchte.
 

DeaD_EyE

Blog Benutzer
Er hat aber schon Recht. Probleme die das FS hat, kopierst du einfach mit. Die Abstraktionsschicht des FS kümmert sich darum, dass die Daten, die du bekommst, auch Konsistent sind. Die Logik gibt es bei dd nicht. Das kopieren vom FS geht auch schneller, als mit dd.

Ist so ähnlich wie ein Script, welches Backups von Datenbanken anlegt, der Inhalt des Backups aber nie geprüft wird.
Man freut sich dann beim Wiederherstellen des kaputten Backups. Da haben schon einige Admins eine hässliche Überraschung erlebt.

Check wenigstens das FS vorher nochmal, bevor du ein "Backup" mit dd machst.
Am besten sind die Tools, die nach dem Kopieren feststellen, ob auch das geschrieben worden ist, was gelesen wurde.
Somit lassen sich Überaschungen vermeiden.

Borgbackup ist z.B. recht bekannt.
Das komplette OS zu sichern soll laut DevOps auch falsch sein. Dafür hat man Tools wie Ansible, Puppet usw.
Richtig konfiguriert können dir die Tools einen kompletten Webserver + DB + EMail einrichten. Das hat den Vorteil, dass man es nur einmal richtig machen muss :-D
 

d4f

Müder Benutzer
Ich mag die hier beschriebene Theorie der automatischen Einrichtung sehr, in der Annahme dass es um Privatserver geht ist das aber oft kein gangbarer Weg. Genau wie man (meistens) für private Bastelprogramme keine Test-Cases entwirft tut man (=ich) das auch für private Bastelserver nicht.

Ein Vollbackup des Systems hat gewisse Nachteile aber auch den Vorteil dass es garantiert known-working ist. Solange das Backup selber nicht schiefgelaufen ist wird eine exakte Rückspielung wieder genau den Stand des Backups wiederherstellen. Sofern es möglich ist sollte man applikationsbasierte Backups zumindest zusätzlich machen. In diesem Fall sehe ich die Grundidee des TE nicht als absolutes no-go an.

Persönlich bevorzuge ich aber zusätzlich Applikation und Hardware zu trennen weil es unter anderem Migration und Performance vereinfacht. In diesem Fall könnte man von dem meistens zugrunde liegenden LV einfach einen Snapshot erstellen. Praktisch wenngleich nicht technisch ist das Resultat aber äquivalent zu einem Vollbackup hier...
 
Das kopieren vom FS geht auch schneller, als mit dd.
Kommt drauf an. Wenn Deine HDD relativ voll ist und hauptsächlich kleine Dateien drauf liegen, wird dd ganz klar gewinnen. :D

Für ein "schnelles" Backup vor solchen Upgrades ist ein vollständiges Image mMn nicht verkehrt. Das geht relativ einfach, schnell und lässt sich problemlos zurückspielen. Ich persönlich lege aber keine HDD-Images mehr an, ohne eine SHA-Prüfsumme zu berechnen und nachher zu vergleichen. Im Idealfall in zwei Durchgängen, damit wirklich nochmals alle Daten verarbeiten werden müssen.

Dass man zusätzlich noch ein anderes Backup haben sollte, ist natürlich immer zu empfehlen.
 

DeaD_EyE

Blog Benutzer
Keine Unit-Tests? Oh ganz böse... Ich habe aber auch ganz oft gesündigt.
Solange die Tools klein sind, hat es kaum einen nutzen.

Wichtig wird es, wenn das Projekt weiter entwickelt wird und man verhindern will, dass es knallt.

Die Tools zur Automatisierung nutze ich selber auch nicht,
da ich nur einen Server habe und den nicht alle Monate neu einrichte.

Gibt es eigentlich Tools, die man mit dd pipen kann und dann z.B. auf stderr den Hash bekommt (ähnlich wie tee)?
Ich könnte mir das auch selbst programmieren, aber vielleicht gibt es da ja was fertiges.
 

Whistler

Blog Benutzer
Ich bevorzuge in solchen Fällen auf "echtem Blech" Acronis. Das macht im Prinzip ein byteweises Backup, ist aber "filesystem-aware" und läßt freie Sektoren aus. Zudem kann man das Backup wahlweise noch verschlüssen und komprimieren.
In virtualisierten Umgebungen ist mein Favorit Veeam, das kann per Change-Block-Tracking zusätzlich noch sehr kompakte Delta-Backups anlegen.
Mit einem entsprechenden Filesystem wie z.B. BTRFS (das neue SuSE-Default-Filesystem) kann man übrigens auch direkt Snapshots anlegen und dann z.B. nach einem mißglücktem Upgrade vom GRUB aus auf einem vorhergehenden Zustand booten.
 
Gibt es eigentlich Tools, die man mit dd pipen kann und dann z.B. auf stderr den Hash bekommt (ähnlich wie tee)?
"Process Substitution" ist da immer wieder ein super Helfer! :)
Bash:
# Speichert den Hash in einer eigenen Datei und kopiert die Eingabedatei gleichzeitig...
dd if=/tmp/input | tee >(sha512sum | cut -d ' ' -f 1 > /tmp/sha512sum) | dd of=/tmp/test-file

# Macht das gleiche, aber zeigt Dir den Hash stattdessen auf STDERR an...
dd if=/tmp/input | tee >(sha512sum | cut -d ' ' -f 1 >&2) | dd of=/tmp/test-file
 

DeaD_EyE

Blog Benutzer
Ja, an tee hab ich gar nicht gedacht, obwohl ich die Funktion in Python sehr oft einsetze. Hat auch eine ähnliche Bedeutung.
Das hab ich mal zum Aufwachen geschrieben:

Python:
#!/usr/bin/env python3
"""
Program to copy a file from source to destination.
During the copy operation the hash value is calculated.
"""

import hashlib
import sys
from argparse import ArgumentParser
from argparse import ArgumentDefaultsHelpFormatter as HelpFormatter
from pathlib import Path


def copy(src, dst, buffer, hash_algo):
    hasher = hashlib.new(hash_algo)
    with src.open('rb') as src_fd:
        with dst.open('wb') as dst_fd:
            while True:
                data = src_fd.read(buffer)
                if not data:
                    break
                hasher.update(data)
                dst_fd.write(data)
    return hasher.hexdigest()


if __name__ == '__main__':
    hashes = hashlib.algorithms_available
    parser = ArgumentParser(description=__doc__, formatter_class=HelpFormatter)
    parser.add_argument('src', type=Path, help='Source file to copy')
    parser.add_argument('dst', type=Path, help='Destination file')
    parser.add_argument('hash', choices=hashes, help='Hash algorithm')
    parser.add_argument('--buffer', type=int, default=64 * 1024, help='Buffer size in bytes')
    parser.add_argument('--overwrite', action='store_true', help='Allow overwriting of destination file')
    args = parser.parse_args()
    if not args.src.exists():
        print('Source file does not exist.', file=sys.stderr)
        sys.exit(2)
    if not args.overwrite and args.dst.exists():
        print('Destination file exists.', file=sys.stderr)
        sys.exit(3)
    hex_digest = copy(args.src, args.dst, args.buffer, args.hash)
    args.dst.with_suffix('.' + args.hash).write_text(hex_digest)

Das wäre noch nicht das Optimum, da hier wie wild daten kopiert werden,
was mit mmap verhindert werden kann.

Hier nochmal mit mmap und tqdm (progressbar):

Python:
#!/usr/bin/env python3
"""
Program to copy a file from source to destination.
During the copy operation the hash value is calculated.
A progress bar is also supported.

If tqdm is not installed, it will install via
pip as a user site-package.
"""

import hashlib
import sys
import mmap
from argparse import ArgumentParser
from argparse import ArgumentDefaultsHelpFormatter as HelpFormatter
from pathlib import Path
from subprocess import call


def chunker(file_size, chunk_size):
    pos = 0
    while pos < file_size:
        next_pos = min(pos + chunk_size, file_size)
        yield slice(pos, next_pos)
        pos += chunk_size


def copy(src, dst, buffer_size, hash_algo, progress=False, check=False):
    file_size = src.stat().st_size
    if progress:
        progress_bar = tqdm(
            total=file_size,
            desc=f'Copy {src.name} > {dst.name}',
            unit="B", unit_scale=True,
            unit_divisor=1024,
            )
    hasher = hashlib.new(hash_algo)
    with src.open('rb') as src_fd:
        mm_src = mmap.mmap(src_fd.fileno(), 0, access=mmap.ACCESS_READ)
        with dst.open('r+b') as dst_fd:
            for data_slice in chunker(file_size, buffer_size):
                dst_fd.write(mm_src[data_slice])
                hasher.update(mm_src[data_slice])
                if progress:
                    progress_bar.update(data_slice.stop - data_slice.start)
            progress_bar.close()
            if check:
                dst_fd.seek(0)
                dst_hasher = hashlib.new(hash_algo)
                if progress:
                    progress_bar = tqdm(
                        total=file_size,
                        desc=f'Hashing {dst.name}',
                        unit="B", unit_scale=True,
                        unit_divisor=1024,
                    )
                while True:
                    chunk = dst_fd.read(buffer_size)
                    if not chunk:
                        break
                    if progress:
                        progress_bar.update(len(chunk))
                    dst_hasher.update(chunk)
                if progress:
                    progress_bar.close()
                check_result = True
                if dst_hasher.digest() == hasher.digest():
                    print('Written file is ok', file=sys.stderr)
                else:
                    print('Written file is different', file=sys.stderr)
                    check_result = False
    return hasher.hexdigest(), check_result


if __name__ == '__main__':
    hashes = hashlib.algorithms_available
    parser = ArgumentParser(description=__doc__, formatter_class=HelpFormatter)
    parser.add_argument('src', type=Path, help='Source file to copy')
    parser.add_argument('dst', type=Path, help='Destination file')
    parser.add_argument('hash', choices=hashes, help='Hash algorithm')
    parser.add_argument('--buffer', type=int, default=64 * 1024, help='Buffer size in bytes')
    parser.add_argument('--overwrite', action='store_true', help='Allow overwriting of destination file')
    parser.add_argument('--progress', action='store_true', help='Show a progress bar')
    parser.add_argument('--check', action='store_true', help='Read the destination file again and calculate the hash value')
    args = parser.parse_args()
    if not args.src.exists():
        print(f'Source file {args.src.name} does not exist.', file=sys.stderr)
        sys.exit(2)
    if not args.overwrite and args.dst.exists():
        print(f'Destination file {args.dst.name} exists.', file=sys.stderr)
        sys.exit(3)
    if args.progress:
        try:
            from tqdm import tqdm
        except ImportError:
            print('Python-Module tqdm is not installed, installing it now.', file=sys.stderr)
            call([sys.executable, '-m', 'pip', 'install', 'tqdm', '--user'])
            try:
                from tqdm import tqdm
            except ImportError:
                args.progress = False
            else:
                args.progress = True
        else:
            args.progress = True
    hex_digest, check_result = copy(args.src, args.dst, args.buffer, args.hash, args.progress, args.check)
    args.dst.with_suffix('.' + args.hash).write_text(f'{hex_digest}  {args.dst.name}\n')
    if not check_result:
        sys.exit(10)

Um die größtmögliche Inkompatibilität zu gewährleisten, hab ich noch extra Format-Strings mit eingebaut.
Das mache ich immer, um die Admins zu ärgern, die nicht so einfach auf eine neuere Python-Version wechseln dürfen/können.

Einer der Gründe wieso ich aufgehört habe massiv Shell-Scripts einzusetzen, ist der daraus resultierende Zeichensalat.
Perl ist da moch ein bisschen extremer (Zeilenrauschen).

Hier die Ausgabe:
Code:
[deadeye@nexus ~]$ python ddd.py test.bin test2.bin sha512 --progress --overwrite --check
Copy test.bin > test2.bin: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 2.93G/2.93G [00:13<00:00, 225MB/s]
Hashing test2.bin: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 2.93G/2.93G [00:06<00:00, 472MB/s]
Written file is ok
[deadeye@nexus ~]$ python ddd.py test.bin test2.bin sha512 --progress --overwrite --check && echo "Everything ok"
Copy test.bin > test2.bin: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 2.93G/2.93G [00:14<00:00, 224MB/s]
Hashing test2.bin: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 2.93G/2.93G [00:06<00:00, 458MB/s]
Written file is ok
Everything ok
[deadeye@nexus ~]$ python ddd.py test.bin test2.bin sha512 --progress --overwrite --check 2>/dev/null && echo "Everything ok"
Everything ok
[deadeye@nexus ~]$

Um das mit Shell-Scripts so zu erreichen, muss man defninitv mehr code schreiben.
Vor allem das Parsen der Argumente ist nicht gerade schön in Shell-Scripts.

tqdm lässt sich wie pv einsetzen.
https://github.com/tqdm/tqdm

Interessant wäre noch zu wissen wie größe des Buffers die Geschwindigkeit beeinflusst.
Hier mal die Ausgabe von dd, wie du es vorgeschlagen hast:
Code:
[deadeye@nexus ~]$ dd if=test.bin | tee >(sha512sum | cut -d ' ' -f 1 > /tmp/sha512sum) | dd of=test2.bin
6144000+0 Datensätze ein
6144000+0 Datensätze aus
3145728000 bytes (3,1 GB, 2,9 GiB) copied, 27,523 s, 114 MB/s
6144000+0 Datensätze ein
6144000+0 Datensätze aus
3145728000 bytes (3,1 GB, 2,9 GiB) copied, 26,3515 s, 119 MB/s
[deadeye@nexus ~]$ dd if=/tmp/input | tee >(sha512sum | cut -d ' ' -f 1 >&2) | dd of=/tmp/test-file^C
[deadeye@nexus ~]$ dd if=test.bin bs=65536 | tee >(sha512sum | cut -d ' ' -f 1 > /tmp/sha512sum) | dd of=test2.bin
48000+0 Datensätze ein
48000+0 Datensätze aus
3145728000 bytes (3,1 GB, 2,9 GiB) copied, 25,8642 s, 122 MB/s
6144000+0 Datensätze ein
6144000+0 Datensätze aus
3145728000 bytes (3,1 GB, 2,9 GiB) copied, 25,7624 s, 122 MB/s
[deadeye@nexus ~]$ dd if=test.bin bs=524288 | tee >(sha512sum | cut -d ' ' -f 1 > /tmp/sha512sum) | dd of=test2.bin
6000+0 Datensätze ein
6000+0 Datensätze aus
3145728000 bytes (3,1 GB, 2,9 GiB) copied, 25,4184 s, 124 MB/s
6144000+0 Datensätze ein
6144000+0 Datensätze aus
3145728000 bytes (3,1 GB, 2,9 GiB) copied, 25,3229 s, 124 MB/s
[deadeye@nexus ~]$ dd if=test.bin bs=524288 | tee >(sha512sum | cut -d ' ' -f 1 > /tmp/sha512sum) | dd of=test2.bin bs=524288
6000+0 Datensätze ein
6000+0 Datensätze aus
3145728000 bytes (3,1 GB, 2,9 GiB) copied, 14,9355 s, 211 MB/s
0+258963 Datensätze ein
0+258963 Datensätze aus
3145728000 bytes (3,1 GB, 2,9 GiB) copied, 14,8387 s, 212 MB/s
[deadeye@nexus ~]$ dd if=test.bin bs=65536 | tee >(sha512sum | cut -d ' ' -f 1 > /tmp/sha512sum) | dd of=test2.bin bs=65536
48000+0 Datensätze ein
48000+0 Datensätze aus
3145728000 bytes (3,1 GB, 2,9 GiB) copied, 13,5992 s, 231 MB/s
396+227613 Datensätze ein
396+227613 Datensätze aus
3145728000 bytes (3,1 GB, 2,9 GiB) copied, 13,497 s, 233 MB/s

Wie man erkennen kann, beeinflusst die Blockgröße die Geschwindigkeit, sofern man es fürs Lesen und Schreiben festlegt.
Ich hab jetzt nicht getestet was passiert, wenn man die Angabe der Blockgröße beim lesen weglässt.
Kann ja jeder selbst mal probieren welche Auswirkungen das hat.
Rein theoretisch sollte es Auswirkungen haben, da die standard Blockgröße bei dd 512 bytes ist.
D.h. wenn die schreibende Seite 64KiB pro Schreibvorgang anfordert, muss die lesende Seite 128 blöcke lesen.
Die Blockgrößen 64 KiB - 1 MiB sollten ok sein. Hängt natürlich auf von der verwendeten Festplatte/SSD ab.
 
Last edited:

Top