Bup – kopie zapasowe oparte o gita

12 listopad 2014

Przez bardzo długi czas do tworzenia kopii zapasowych korzystałem z rdiff-backup. Z czasem jednak zacząłem dostrzegać niedopracowane elementy, między innymi słabą wydajność, problemy z przywróceniem danych jeśli jedna z przyrostowych migawek została uszkodzona, na zerowym rozwoju kończąc. W związku z tym zacząłem rozglądać się za projektami używanymi obecnie przez technologicznych hipsterów i po szybkim porównaniu tempa rozwoju znalezionych programów zdecydowałem się na bup, system kopii zapasowych opartych o format packfile znany z gita.

W czym bup jest lepszy od rdiff-backup? Poza aktywnym rozwojem, zdecydowanie lepiej wypada pod względem deduplikacji. Podczas gdy rdiff-backup radził sobie jedynie z podobnie nazwanymi plikami o identycznej zawartości, bohater wpisu deduplikuje dane, dzieląc pliki na mniejsze części. Ponadto dane są współdzielone między kopiami w taki sposób, że nie wiedzą one o swoim istnieniu, efektywnie deduplikując dane pomiędzy odrębnymi maszynami i minimalizując użyty transfer. Dodatkowo bup korzysta z par2 w celu tworzenia danych naprawczych, umożliwiając przywrócenie kopii pomimo problemów z dyskiem. Kończąc moje zachwyty, bup umie także zapisywać kopie bezpośrednio na zdalny serwer.

Oczywiście bup ma też swoje wady. Pomijając wczesne stadium rozwoju projektu, najnowsze wydanie (0.26) nie posiada wsparcia przywracania danych ze zdalnej maszyny, skutkując koniecznością użycia sshfs. Inną słabością jest brak możliwości usuwania starszych kopii zapasowych. Pewnym obejściem problemu jest "obracanie" repozytoriów co kilka miesięcy, niemniej nie jest to zadowalająca metoda.

packfile

Nie ma sensu rozpisywać się o sposobie działania bupa bez pobieżnego omówienia formatu packfile. Załóżmy, że na dysku posiadamy repozytorium zawierające kilka scommitowanych plików, których rozmiar waha się między 30, a 12000 bitami. Dodatkowo utworzony został tag wskazujący na jeden z commitów. Łącznie w .git/objects można znaleźć ok. 14 plików.

% find .git/objects -type f
.git/objects/3d/114b79bef023027e43c1443738c9e2e17871af
.git/objects/62/1108e557e3605a835cb7c83ef72040688082da
.git/objects/d5/0653aa0c59adc4cd868f32eb9e3e69cd54a5ea
.git/objects/ed/15a7d2d1f427cf4b385f48f533abc5ef3714e4
.git/objects/c6/95bf83c5d2487549732ef239f628bfd75a6c51
.git/objects/c6/2e9d70bc5da094f20872874d5687603ad77dc4
.git/objects/68/39ea6df280ef41957b26c6bef9bc2c5b50a157
.git/objects/48/5750e9f9b50b0f59f22ae8781cc4527b3776f5
.git/objects/26/42e01f987f08049835752415bac4556af6b5eb
.git/objects/08/d3514072b1934d01936870306843815ac16585
.git/objects/45/a7d16bf4c862926e53674681ea3e5fba400a92
.git/objects/22/ca36260997dfd7f4a861a11135f0164ccf3989
.git/objects/35/2101d7fdbfe6d66c0214c9b2a812bcb37f7968
.git/objects/f6/948d959c734de9f849dc498dbc140376b9057c

Same obiekty są już poddane kompresji za pomocą zlib:

% git ls-tree master .
100644 blob f6948d959c734de9f849dc498dbc140376b9057c    date
100644 blob 45a7d16bf4c862926e53674681ea3e5fba400a92    hello.c
100644 blob 08d3514072b1934d01936870306843815ac16585    hello.cpp
100644 blob d50653aa0c59adc4cd868f32eb9e3e69cd54a5ea    random12000
100644 blob 6839ea6df280ef41957b26c6bef9bc2c5b50a157    random600

% du -b random12000
12001   random12000

% du -b .git/objects/d5/0653aa0c59adc4cd868f32eb9e3e69cd54a5ea
6301    .git/objects/d5/0653aa0c59adc4cd868f32eb9e3e69cd54a5ea

Co się stanie gdy plik random12000 zostanie zmodyfikowany?

% echo $RANDOM >> random12000
% git commit -am 'add $RANDOM to random12000'
% git ls-tree master . | grep random12000$
100644 blob c7198d7e05ecd8ea5e4a989ebe0f4ca03950e584    random12000

Zmodifykowany plik został zapisany jako zupełnie nowy obiekt. W efekcie na dysku przechowywane są dwa prawie identyczne pliki. Logicznym pomysłem byłoby znalezienie metody na przechowanie tylko jednego z nich, oraz różnicy pomiędzy jednym, a drugim. Na szczęście git to przemyślane oprogramowanie i istnieje na to sposób.

Początkowy format w jaki git przechowuje pliki nazywany jest "luźnym". Dodatkowo jednak git co pewien czas łączy kilka obiektów w jeden plik binarny nazwany jako packfile żeby zaoszczędzić miejsce na dysku i poprawić wydajność. Taka operacja odbywa się w przypadku znaczącej liczby luźnych obiektów, wysyłania zmian na serwer czy ręcznego wywołania git gc:

% git gc
Counting objects: 17, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (15/15), done.
Writing objects: 100% (17/17), done.
Total 17 (delta 4), reused 0 (delta 0)

% find .git/objects -type f
.git/objects/info/packs
.git/objects/pack/pack-7104cc6c47e7df6ab4dd544f7b7b796b0fbfa48e.idx
.git/objects/pack/pack-7104cc6c47e7df6ab4dd544f7b7b796b0fbfa48e.pack

Na miejscu poprzednich obiektów pojawiły się dwa nowe pliki: packfile i indeks. Ten pierwszy zawiera w sobie wszystkie obiekty, jakie wcześniej przechowywaliśmy, natomiast drugi zawiera informacje o lokalizacji konkretnych obiektów w zbiorczym pliku. Ponadto zajmują mniej, niż sumarycznie zajmowały osobne obiekty. Poleceniem git verify-pack -v można sprawdzić w jaki sposób pliki zostały podzielone na delty.

W bardzo podobny sposób działa bup, jednak nie korzysta bezpośrednio z gita. Jedną z różnic jest dzielenie dużych plików na mniejsze tak, żeby deduplikacja danych była jak najbardziej optymalna. Więcej informacji można znaleźć w dokumentacji poświęconej implementacji.

Instalacja

Użytkownicy takich dystrybucji jak Debian i Arch Linux mogą znaleźć bupa w oficjalnych repozytoriach. Pozostali interesanci do kompilacji będą potrzebować podstawowych pakietów do kompilacji, nagłówków developerskich Pythona 2, bindingów FUSE dla tegoż języka, do tego biblioteki pyxattr i pylibacl. Reszta to tradycyjne pobranie kodu źródłowego i make && make install.

Wykonywanie i przywracanie kopii zapasowych

Zanim zostanie wykonana jakakolwiek kopia, należy zainicjalizować repozytorium bup poleceniem bup init:

% bup init
Reinitialized existing Git repository in /home/barthalion/.bup/

Lokalizację tego katalogu można zmienić za pomocą zmiennej środowiskowej BUP_DIR. Kolejnym wymogiem utworzenia kopii jest stworzenie listy plików za pomocą bup index. Na pierwszy rzut oka może wydawać się, że bup może zindeksować plik, który zostanie potem zmodyfikowany, prowadząc do rozjechania się metadanych w repozytorium ze stanem faktycznym, albo skopiowania tylko części plików. Szczęśliwie autorzy bupa przewidzieli taką możliwość i program wykrywa zarówno inne znaczniki czasu jak i niezindeksowane pliki, o ile argumentem jest cały katalog.

% bup index -vv .config
/home/barthalion/.config/htop/htoprc
/home/barthalion/.config/htop/
/home/barthalion/.config/fish/fishd.001cc0c34171
/home/barthalion/.config/fish/
/home/barthalion/.config/
Indexing: 5, done (426 paths/s).

Istnieje również możliwość wyłączenia wybranych plików z procesu indeksowania. W tym celu należy posłużyć się parametrem --exclude= (który przyjmuje ścieżkę do pliku) albo --exclude-rx=, który przyjmuje pythonowe wyrażenie regularne. Dodatkowo można umieścić listę ścieżek lub wyrażeń regularnych w osobnych plikach i przekazać je za pomocą --exclude-from= i --exclude-rx-from=.

Po zindeksowaniu właściwych plików pozostaje wykonać włąściwą kopię zapasową poleceniem bup save -n etykieta.

% bup save -n barthalion-config .config
Reading index: 5, done.
Saving: 100.00% (13/13k, 5/5 files), done.
bloom: creating from 1 file (15 objects).

Na początku wpisu wspominałem o możliwości zapisywania kopii bezpośrednio na innej maszynie. W tym celu obie powinny mieć zainicjalizowane repozytorium bup. Aby zapisać dane na innym serwerze zamiast w $BUP_DIR należy podać parametr -r serwer.tld:.

Po wykonaniu kopii warto utworzyć dane naprawcze:

% bup fsck -vv -g
fsck: No filenames given: checking all packs.
fsck: checking pack-22942c46299eb7167bcf82de7c53776623ac12e9 (git)
Block size: 4
Source file count: 2
Source block count: 878
Recovery block count: 200
Recovery file count: 1

Opening: pack-22942c46299eb7167bcf82de7c53776623ac12e9.pack
Opening: pack-22942c46299eb7167bcf82de7c53776623ac12e9.idx
Computing Reed Solomon matrix.
Constructing: done.
Wrote 800 bytes to disk
Writing recovery packets
Writing verification packets
Done
pack-22942c46299eb7167bcf82de7c53776623ac12e9 generated
fsck done.

Pozostaje zweryfikować czy wszystkie pliki są na miejscu:

% bup ls
barthalion-config
% bup ls barthalion-config
2014-09-07-183138  latest
% bup ls barthalion-config/latest/
home
% bup ls barthalion-config/latest/home
barthalion

Kopie przywraca się poleceniem bup restore, wskazując docelowy katalog przywrócenia oraz etykietę wraz z datą kopii i ewentualną ścieżką:

% bup restore -C /tmp/bup_restored barthalion-config/latest/home
Restoring: 5, done.

Et voilà, pliki są na miejscu.