Tips en Trucs 2022

Incrementele back-ups met RSync

Ondertussen is iedereen wel overtuigd dat reservekopieën (back-ups) onontbeerlijk zijn om gebeurlijke ongelukken te herstellen. Maar nog handiger zijn incrementele back-ups. Incrementele back-ups slaan alleen de gegevens op die zijn gewijzigd sinds de vorige back-up werd gemaakt. In een incrementele back-up strategie, is alleen de eerste back-up van de reeks een "volledige back-up"; de volgende, zullen alleen de incrementele verschillen opslaan. Dit heeft het voordeel dat je met een minimum aan extra schijfruimte ook beschikt over bestanden die nog in een vorige back-up werden opgeslagen. Je kunt als het ware terugkeren in de tijd.

Harde en symbolische koppelingen

Voordat we verder gaan, en bekijken hoe we incrementele back-ups kunnen maken met rsync, moeten we even de tijd nemen om het verschil tussen symbolische en harde links duidelijk te maken, aangezien de laatste een cruciale rol zullen spelen in onze implementatie.

Op Unix-gebaseerde systemen zoals Linux hebben we twee soorten "links": harde en symbolische. De ln opdracht genereert standaard harde links; als we symbolische links willen maken, moeten we de opdracht aanroepen met de -s (--symbolic) optie.

Om te begrijpen hoe harde links werken, moeten we ons concentreren op het concept van de inode. Een inode is een gegevensstructuur op het bestandssysteem die informatie bevat over een bestand of een map (wat overigens gewoon een "speciaal" soort bestand is), zoals de rechten en de locatie van de blokken op de schijf die de eigenlijke gegevens bevatten.

Op dit punt zou je kunnen denken dat de naam van een bestand ook "opgeslagen" is in zijn inode: dit is niet het geval. Wat wij gewoonlijk "bestandsnamen" noemen, zijn slechts mensvriendelijke verwijzingen naar inodes die in mappen zijn vastgelegd.

Een map kan meer dan één verwijzing naar dezelfde inode bevatten: die verwijzingen noemen we hard_links. Alle bestanden hebben (uiteraard) ten minste één hard_link.

Harde links hebben twee grote beperkingen: ze werken niet over bestandssystemen heen en kunnen niet gebruikt worden voor mappen.

Wanneer het aantal harde links voor een inode 0 bereikt, wordt de inode zelf verwijderd en worden de blokken op de schijf waarnaar verwezen wordt bruikbaar voor het besturingssysteem (de eigenlijke gegevens worden niet verwijderd, en kunnen soms hersteld worden, tenzij ze overschreven worden door nieuwe gegevens). Het aantal harde links dat met een inode geassocieerd is, wordt gerapporteerd in de uitvoer van de ls opdracht wanneer het aangeroepen wordt met de -l optie:

dany@pindabook:~> ls -l ~/.bashrc
-rw-r--r-- 1 dany users 1177 13 jun  2021 /home/dany/.bashrc

In de uitvoer hierboven, net na de rechten-notatie, kunnen we duidelijk zien dat ~/.bashrc de enige verwijzing is (de enige harde link) naar zijn specifieke inode. Laten we een andere harde link maken, en zien hoe de uitvoer van de opdracht verandert:

dany@pindabook:~> ln ~/.bashrc bashrc
dany@pindabook:~> ls -l ~/.bashrc
-rw-r--r-- 2 dany users 1177 13 jun  2021 /home/dany/.bashrc

Zoals verwacht, is het aantal harde links met één eenheid verhoogd en is nu 2. Nogmaals: ~/.bashrc en ~/bashrc zijn niet twee verschillende bestanden; het zijn gewoon twee directory-items die naar dezelfde inode wijzen. Dit kan eenvoudig worden gedemonstreerd door ls te starten, deze keer met de -i optie (--inode): het zorgt ervoor dat de inode-index wordt opgenomen in de uitvoer:

dany@pindabook:~> ls -li ~/.bashrc ~/bashrc
394031 -rw-r--r-- 2 dany users 1177 13 jun  2021 /home/dany/.bashrc
394031 -rw-r--r-- 2 dany users 1177 13 jun  2021 /home/dany/bashrc

Zoals je kunt zien, is de inode waarnaar verwezen wordt 394031 in beide regels.

Symbolische koppelingen zijn anders. Ze zijn een moderner concept en overwinnen de twee beperkingen van hard links: ze kunnen gebruikt worden voor mappen en kunnen over bestandssystemen heen gezet worden. Een symbolische link is een speciaal soort bestand dat naar een heel ander bestand verwijst (zijn doel). Het verwijderen van een symbolische koppeling heeft geen invloed op zijn doel: het verwijderen van alle symbolische koppelingen naar een bestand heeft niet tot gevolg dat het oorspronkelijke bestand wordt verwijderd. Aan de andere kant, het verwijderen van het "doel" bestand, verbreekt de symbolische koppeling(en) die er naar verwijzen.

Nu zou het duidelijk moeten zijn waarom, in termen van ruimtebeslag op de schijf, het maken van hard links handiger is: wanneer we een hard link toevoegen, maken we geen nieuw bestand, maar een nieuwe verwijzing naar een reeds bestaand bestand.

Incrementele back-ups maken met rsync

Hoe kunnen we rsync gebruiken om incrementele back-ups te maken? Stel dat we incrementele back-ups willen maken van onze $HOME map: eerst maken we er een volledige back-up van en slaan die op in een map met als naam de huidige tijdstempel (datum en tijd). We maken dan een koppeling naar deze map, en we noemen hem latest om een gemakkelijk herkenbare referentie te hebben.

De volgende back-ups worden gemaakt door de verschillen te berekenen tussen de huidige toestand van de $HOME map en de laatste bestaande back-up. Elke keer dat een nieuwe back-up wordt gemaakt, zal de huidige latest koppeling, die nog steeds naar de vorige back-up verwijst, worden verwijderd; hij zal dan opnieuw worden aangemaakt met de nieuwe back-up map als doel. De koppeling zal altijd naar de laatst beschikbare back-up verwijzen.

Zelfs als de back-ups incrementeel zijn, zullen we, als we in elke map kijken, altijd de volledige set bestanden zien, niet alleen de bestanden die veranderd zijn: dit komt omdat de ongewijzigde bestanden zullen worden vertegenwoordigd door harde links. De bestanden die gewijzigd zijn sinds de laatste back-up zullen de enigen zijn die nieuwe ruimte op de schijf innemen.

Om onze back-up strategie te implementeren zullen we gebruik maken van de --link-dest optie van rsync. Deze optie neemt een map als argument. Bij het aanroepen van rsync zullen we dan specificeren:

De inhoud van de bronmap zal worden vergeleken met die van de map die is doorgegeven aan de --link-dest optie. Nieuwe en gewijzigde bestanden die in de bronmap bestaan, worden zoals altijd naar de doelmap gekopieerd (en bestanden die in de bronmap zijn verwijderd, verschijnen ook niet in de back-up als de optie --delete wordt gebruikt); ongewijzigde bestanden verschijnen ook in de back-up map, maar het zijn slechts harde links die naar inodes wijzen die in de eerder gemaakte back-ups zijn aangemaakt.

Het back-up script

Hier is een eenvoudig bash script met een implementatie van onze strategie:

#!/bin/bash

# A script to perform incremental backups using rsync

set -o errexit
set -o nounset
set -o pipefail

# Aan te passen parameters

readonly SOURCE_DIR="${HOME}"
readonly BACKUP_DIR="/mnt/data/backups"
readonly DATETIME="$(date '+%Y-%m-%d_%H:%M:%S')"
readonly BACKUP_PATH="${BACKUP_DIR}/${DATETIME}"
readonly LATEST_LINK="${BACKUP_DIR}/latest"

mkdir -p "${BACKUP_DIR}"

rsync -av --delete \
  "${SOURCE_DIR}/" \
  --link-dest "${LATEST_LINK}" \
  --exclude=".cache" \
  "${BACKUP_PATH}"

rm -rf "${LATEST_LINK}"
ln -s "${BACKUP_PATH}" "${LATEST_LINK}"

Het eerste wat we deden was het declareren van een aantal alleen-lezen variabelen: SOURCE_DIR die het absolute pad bevat van de map waarvan we een back-up willen maken (onze thuismap in dit geval), BACKUP_DIR die het pad bevat naar de map waar alle back-ups zullen worden opgeslagen, DATETIME die de huidige tijdstempel opslaat, BACKUP_PATH die het absolute pad is van de back-up map die wordt verkregen door BACKUP_DIR en de huidige DATETIME te 'verbinden'. Tenslotte stellen we de LATEST_LINK variabele in die het pad bevat van de symbolische koppeling die altijd naar de laatste back-up zal verwijzen.

We starten dan de rsync opdracht met de -a optie (--archive) om de belangrijkste attributen van de bron bestanden te behouden, de -v optie om de opdracht meer informatie te laten weergeven (optioneel), en de --delete optie om ervoor te zorgen dat bestanden verwijderd uit de bron map ook verwijderd worden op de bestemming.

Merk op dat we een schuine streep hebben toegevoegd aan de SOURCE_DIR in de rsync opdracht: dit zorgt ervoor dat alleen de inhoud van de bron map gesynchroniseerd wordt, niet de map zelf.

We voeren de opdracht uit met de --link-dest optie, en geven de LATEST_LINK map als argument. De eerste keer dat we het script starten zal deze map niet bestaan: dit zal geen fout genereren, maar zal ervoor zorgen dat een volledige back-up wordt uitgevoerd, zoals verwacht.

We besloten om de .cache map uit te sluiten van de back-up met de --exclude optie, en tenslotte gaven we de BACKUP_PATH map mee om rsync te instrueren waar de back-up moet worden gemaakt.

Nadat de opdracht met succes is uitgevoerd, wordt de koppeling die naar de vorige back-up verwijst, verwijderd, en wordt een andere koppeling met dezelfde naam, die naar de nieuwe back-up verwijst, aangemaakt.

Klaar! Voordat we het script in de echte wereld gaan gebruiken, kunnen we er beter wat foutafhandeling aan toevoegen (we kunnen bijvoorbeeld de nieuwe back-up map verwijderen als de back-up niet succesvol is afgerond), en, omdat de rsync opdracht een vrij lange tijd kan draaien (in ieder geval de eerste keer, wanneer een volledige back-up wordt gemaakt) willen we misschien een vorm van signaal doorgifte implementeren van het ouder script naar het kind proces (hoe dit te doen zou een leuk onderwerp voor een andere tip kunnen zijn).

RSync Incrementele back-up script

Regelmatig uitvoeren

Het spreekt voor zich dat je back-ups regelmatig moet uitvoeren. Het liefst naar externe en los te koppelen schijven en/of naar een internet locatie, m.a.w. zorg dat BACKUP_DIR naar de juiste doelmap wijst. En controleren of de back-ups wel correct zijn aangemaakt, M.a.w. alles bevatten wat ze moeten bevatten. Hieronder een voorbeeld nadat het script werd aangepast en de hard link bashrc werd verwijderd.

dany@pindabook:~> nano backup-script.sh 
dany@pindabook:~> rm bashrc
dany@pindabook:~> bash backup-script.sh 
sending incremental file list
created directory /mnt/data/backups/2022-04-12_18:37:09
./
backup-script.sh
.config/
.config/dolphinrc
.local/share/baloo/index
.local/share/baloo/index-lock
.local/share/kactivitymanagerd/resources/database-shm
.local/share/kactivitymanagerd/resources/database-wal
.local/share/sddm/xorg-session.log

sent 3,240,808 bytes  received 2,692 bytes  6,487,000.00 bytes/sec
total size is 302,932,339  speedup is 93.40