Eine Sicherung wird als erfolgreich gemeldet, doch nach der Wiederherstellung fehlen die neuesten Datensätze: Bei Dateidiensten, die SQLite im WAL-Modus einsetzen, wird ein solcher Fehler leicht falsch eingeordnet. Ein erfolgreich abgeschlossener Kopiervorgang bestätigt lediglich, dass Bytes übertragen wurden – nicht, dass die Datenbank konsistent ist. Auf Dedicated Servern, die fortlaufend Dateiindizes, Aufgabenstatus oder Sicherungsverzeichnisse erfassen, sollten deshalb zwei getrennte Schritte vorgesehen werden: eine Kopie erstellen und ihre Nutzbarkeit nachweisen.
1. WAL und Sicherung klar voneinander abgrenzen
Im WAL-Modus können bereits bestätigte Änderungen zunächst in der Datei -wal liegen. Erst ein Checkpoint überträgt sie in die eigentliche Datenbankdatei. Wird bei laufendem Betrieb cp service.db backup.db ausgeführt, können diese Änderungen fehlen. Auch das nacheinander erfolgende Kopieren der Hauptdatei, der WAL-Datei und der Shared-Memory-Datei ergibt nicht automatisch einen Snapshot desselben Zeitpunkts.
Die Online-Backup-Schnittstelle liest konsistente Inhalte über eine Datenbankverbindung, statt veränderliche Dateien blind zu kopieren. Sie eignet sich für Anwendungen, deren Schreibzugriffe nicht unterbrochen werden können, verursacht aber weiterhin Lesezugriffe und Schreiblast am Ziel. Eine Offline-Sicherung ist einfacher, wenn sich sämtliche Verbindungen schließen lassen. Die Schreibpause muss allerdings auch Hintergrundaufgaben einschließen; nur eingehende Anfragen anzuhalten reicht nicht aus.
WAL ist keine externe Sicherung, und auch eine Spiegelung ist kein Backup. Eine verlässliche Kopie muss außerhalb des Ausfallbereichs der Quelldatenbank liegen und durch unabhängige Lesezugriffe geprüft worden sein.
Dedicated Server von OnuCloud sind standardmäßig mit zwei direkt angebundenen 1TB-NVMe-Laufwerken ausgestattet, ohne Hardware-RAID. Wird die Kopie auf das andere Laufwerk geschrieben, lässt sich ein Teil der Lese- und Schreiblast trennen. Beide Laufwerke befinden sich jedoch weiterhin im selben Rechner und ersetzen keine Aufbewahrung an einem anderen Standort.
2. Einen reproduzierbaren Testbestand mit laufenden Schreibzugriffen erstellen
Der folgende Versuch eignet sich für Umgebungen wie Debian 12, in denen Python 3 und das Standardbibliotheksmodul sqlite3 verfügbar sind. Prüfen Sie zunächst das Modul und legen Sie ein eigenes temporäres Verzeichnis an. Alle Schritte betreffen ausschließlich die Testdateien; Produktionsdatenbanken werden nicht berührt, und Änderungen an Systemdiensten sind nicht erforderlich.
python3 -c 'import sqlite3; print(sqlite3.sqlite_version)'
umask 077
export LAB_DIR="$(mktemp -d)"
printf '%s\n' "$LAB_DIR"
Der Testbestand verwendet zwei Tabellen: events speichert die Einzelbuchungen, totals den kumulierten Wert. Jede Transaktion fügt eine Einzelbuchung hinzu und erhöht zugleich den Gesamtwert. In der Kopie muss daher gelten: Die Summe der Einzelbeträge entspricht dem kumulierten Wert. Diese Bedingung sagt mehr aus als die bloße Existenz einer Datei und verdeutlicht zugleich, dass strukturelle Integrität und fachliche Korrektheit unterschiedliche Prüfkriterien sind.
Das folgende Skript enthält sowohl einen Schreib-Thread als auch den Sicherungsablauf. Das Verzeichnis wird aus dem vorherigen Schritt übernommen. Legen Sie für jede Wiederholung ein neues Verzeichnis an, damit sich neue und alte Testdaten nicht vermischen. Produktionsdatenbanken sind in der Regel komplexer. Tabellenübergreifende Beziehungen und Anforderungen an die Anwendungsversion müssen gesondert festgelegt werden; dieses einfache Zählmodell lässt sich nicht unverändert übernehmen.
3. Eine Kopie über die Online-Schnittstelle erstellen und prüfen
Das Skript wartet zunächst auf eine abgeschlossene Transaktion. Anschließend läuft der Schreib-Thread weiter, während die Online-Sicherung ausgeführt wird. Als Ziel dient zuerst eine temporäre Datei. Erst wenn Integritätsprüfung, Fremdschlüsselprüfung und fachliche Invarianten erfolgreich geprüft wurden, wird sie in die freigegebene Testkopie umbenannt. Ein Fehler in einem beliebigen Schritt lässt das Skript scheitern; eine neue Kopie wird dann nicht freigegeben.
python3 - <<'PY'
import os
import sqlite3
import threading
import time
from pathlib import Path
base = Path(os.environ["LAB_DIR"])
source = base / "source.db"
pending = base / "backup.pending.db"
published = base / "backup.db"
db = sqlite3.connect(source)
mode = db.execute("PRAGMA journal_mode=WAL").fetchone()[0]
if mode.lower() != "wal":
raise RuntimeError("WAL mode unavailable")
db.executescript("""
CREATE TABLE events (
n INTEGER PRIMARY KEY,
amount INTEGER NOT NULL
);
CREATE TABLE totals (
id INTEGER PRIMARY KEY CHECK (id = 1),
balance INTEGER NOT NULL
);
INSERT INTO totals VALUES (1, 0);
""")
db.close()
stop = threading.Event()
ready = threading.Event()
errors = []
def writer():
conn = None
try:
conn = sqlite3.connect(source, timeout=5)
while not stop.is_set():
with conn:
conn.execute("INSERT INTO events(amount) VALUES (1)")
conn.execute(
"UPDATE totals SET balance = balance + 1 WHERE id = 1"
)
ready.set()
time.sleep(0.01)
except Exception as exc:
errors.append(exc)
ready.set()
finally:
if conn is not None:
conn.close()
worker = threading.Thread(target=writer)
worker.start()
try:
if not ready.wait(5):
raise RuntimeError("Writer did not become ready")
if errors:
raise RuntimeError("Writer failed") from errors[0]
src = sqlite3.connect(source, timeout=5)
dst = sqlite3.connect(pending, timeout=5)
try:
src.backup(dst, pages=128, sleep=0.01)
finally:
dst.close()
src.close()
finally:
stop.set()
worker.join()
if errors:
raise RuntimeError("Writer failed") from errors[0]
check = sqlite3.connect(pending)
try:
result = check.execute("PRAGMA integrity_check").fetchall()
if result != [("ok",)]:
raise RuntimeError("Integrity check failed")
if check.execute("PRAGMA foreign_key_check").fetchall():
raise RuntimeError("Foreign key check failed")
count, amount = check.execute(
"SELECT COUNT(*), COALESCE(SUM(amount), 0) FROM events"
).fetchone()
balance = check.execute(
"SELECT balance FROM totals WHERE id = 1"
).fetchone()[0]
if not (count > 0 and count == amount == balance):
raise RuntimeError("Business invariant failed")
finally:
check.close()
os.replace(pending, published)
print(f"Verified rows: {count}")
print(published)
PY
pages=128 bezeichnet die Anzahl der pro Durchlauf verarbeiteten Datenbankseiten, nicht die Anzahl der Bytes. sleep betrifft vor allem Wiederholungsversuche bei einer belegten Datenbank und ist keine feste Durchsatzbegrenzung. Im Produktivbetrieb sollten Laufzeit und Fehlerursachen protokolliert und ein Zeitlimit für den gesamten Auftrag festgelegt werden. Anhaltend hohe Schreiblast kann die Sicherung verlängern. Eine Zeitüberschreitung muss als Fehler gelten; eine unvollständige Datei darf nicht als erfolgreiche Sicherung markiert werden.
Die Umbenennung dient hier dazu, ungeprüfte Kandidaten von abgenommenen Kopien zu unterscheiden. Sie garantiert keine dauerhafte Speicherung bei einem Stromausfall. Für einen produktiven Sicherungsablauf muss die Persistenzgrenze unter Berücksichtigung von Datei- und Verzeichnissynchronisierung sowie der Semantik des Zielspeichers festgelegt werden.
4. Die Wiederherstellung isoliert prüfen, statt die Quelldatenbank als Nachweis zu verwenden
Nach Abschluss der Sicherung können bereits weitere Schreibzugriffe auf die Quelldatenbank erfolgt sein. Deshalb darf nicht verlangt werden, dass die Zeilenanzahl der Kopie mit einer später abgefragten Zeilenanzahl der Quelle übereinstimmt. Prüfen Sie zunächst die Konsistenz der Kopie selbst und beurteilen Sie ihre Aktualität anschließend anhand des Sicherungszeitfensters. Eine Online-Sicherung ist keine kontinuierliche Replikation und ermöglicht nicht automatisch eine Wiederherstellung zu einem beliebigen Zeitpunkt.
Aus der freigegebenen Kopie eine Arbeitskopie für die Wiederherstellung erstellen
Die Testkopie ist bereits geschlossen und kann offline in ein anderes Verzeichnis kopiert werden. Im folgenden Schritt wird diese Arbeitskopie schreibgeschützt geöffnet, um Tabellenstruktur und wesentliche Beziehungen zu prüfen. Richten Sie die Verbindung für den Wiederherstellungstest nicht auf source.db; andernfalls prüfen Sie die Originaldatenbank.
mkdir "$LAB_DIR/restore"
cp "$LAB_DIR/backup.db" "$LAB_DIR/restore/service.db"
python3 - <<'PY'
import os
import sqlite3
from pathlib import Path
path = Path(os.environ["LAB_DIR"]) / "restore" / "service.db"
conn = sqlite3.connect(path.as_uri() + "?mode=ro", uri=True)
try:
print(conn.execute("PRAGMA integrity_check").fetchall())
print(conn.execute("PRAGMA foreign_key_check").fetchall())
print(conn.execute("""
SELECT
(SELECT COALESCE(SUM(amount), 0) FROM events),
(SELECT balance FROM totals WHERE id = 1)
""").fetchone())
finally:
conn.close()
PY
Die Prüfung gilt als bestanden, wenn die Integritätsprüfung ok liefert, die Fremdschlüsselprüfung keine Zeilen zurückgibt und beide Summen übereinstimmen. Der Testbestand enthält keine Fremdschlüssel. Ein leeres Ergebnis der Fremdschlüsselprüfung belegt hier daher nur, dass sich der Prüfschritt ausführen lässt. Für reale Anwendungen müssen die tatsächlichen Constraints erhalten bleiben und geprüft werden.
Bei der produktiven Abnahme muss außerdem eine Anwendung in der passenden Version die Arbeitskopie öffnen und Dateiindizes oder Aufgabenlisten lesen. E-Mail-Versand, Callbacks und automatisch ausgeführte Aufgaben müssen dabei deaktiviert sein. Dass sich die Datenbank öffnen lässt, bedeutet nicht, dass auch alle externen Dateien vorhanden sind.
5. Aus dem einmaligen Versuch verbindliche Abnahmekriterien für Sicherungen machen
Vor dem Produktivstart sollten mindestens vier Nachweise vorliegen: Pfad der Quelldatenbank und Anwendungsversion, Beginn und Ende der Sicherung, Prüfergebnisse sowie Ergebnisse der isolierten Leseprüfung. Die Zeitangaben dienen zur Beurteilung der Datenaktualität und dürfen nicht als exakter Transaktionsstichtag verstanden werden. Anwendungen mit klar definierten Anforderungen an den Wiederherstellungspunkt benötigen ein gesondertes Konzept für Commit-Sequenznummern und Wiederherstellungsziele.
Der Zugriff auf das Sicherungsverzeichnis muss eingeschränkt werden, damit Tokens, Nutzerdaten oder Aufgabenparameter aus der Datenbank nicht über die Kopie offengelegt werden. Nach der Übertragung an einen anderen Standort sind die Dateiprüfsummen abzugleichen und die Wiederherstellungstests zu wiederholen. Übereinstimmende Prüfsummen belegen lediglich eine unveränderte Übertragung; sie ersetzen keine Datenbankabnahme.
Bei einem Fehler bleibt die letzte bereits geprüfte Kopie erhalten. Ungeprüfte Dateien werden getrennt behandelt; ein erfolgreicher Sicherungsstand darf nicht durch eine leere Datei überschrieben werden. Verweist die Datenbank auf Anhänge, Index-Shards oder weitere Datenbanken, stellt die Online-Sicherung einer einzelnen Datenbank keine ressourcenübergreifende Atomarität her. Dafür ist eine Schreibpause der Anwendung oder eine Koordination über ein Versionsmanifest erforderlich.
Das abschließende Abnahmekriterium lautet nicht „Der Kopierbefehl war erfolgreich“, sondern: „Die Kopie liegt eigenständig vor, die Strukturprüfung ist bestanden, die fachlichen Beziehungen sind gültig, und die Anwendung kann die Kopie isoliert lesen.“ Erst wenn alle vier Bedingungen erfüllt sind, besitzt die Sicherung einen nachweisbaren Wert für die Wiederherstellung.
Häufig gestellte Fragen
Reicht im WAL-Modus eine Kopie der SQLite-Hauptdatei?
Nicht als verlässliche Online-Sicherung. Bestätigte Transaktionen können noch im WAL liegen; getrennte Dateikopien bilden nicht zwingend denselben Zeitpunkt ab. Die Online-Backup-API verwenden oder nach dem Schließen aller Verbindungen offline kopieren.
Beweist integrity_check mit dem Ergebnis ok die Wiederherstellbarkeit?
Nein. Die Prüfung untersucht vor allem interne Datenbankstrukturen. Zusätzlich foreign_key_check ausführen und wichtige Tabellen, fachliche Invarianten sowie Lesezugriffe der Anwendung an einer isolierten Kopie testen.
AMD EPYC 7543P Dedicated Server
ab $472/Monat