Sparse files (rozrzedzone pliki)

Spis treści

Każdy system plików opiera się na blokach danych. Standardowo taki blok w systemie plików EXT4 ma 4 KiB (8 x 512 bajtów). Za każdym razem, gdy tworzymy jakiś plik na dysku, alokowana jest pewna część bloków, na których ten plik ma zostać zapisany. Te większe pliki rezerwują więcej bloków, choć nie zawsze są to bloki ciągłe. W taki sposób, plik o rozmiarze 10 GiB okupowałby dokładnie tyle miejsca na dysku ile sam waży. Niemniej jednak są pewne pliki, które może i ważą te 10 GiB ale system plików postrzega je tak jakby miały 100 MiB czy 200 MiB, w zależności od tego ile "faktycznie" taki plik zajmuje miejsca. Jak to możliwe? Pliki, o których mowa, to tzw. "rozrzedzone pliki" (Sparse files). Taki plik składa się z szeregu bloków pustych (mających same zera), których nie trzeba zapisywać na dysk. Zamiast tego, można jedynie zapisać metadane w strykturze systemu plików, które będą opisywać te puste bloki. Poniższy artykuł ma na celu pokazać do czego takie pliki sparse mogą nam się przydać i jak z nich korzystać.

Wady i zalety plików sparse

Pliki sparse dają nam możliwość bardzo szybkiego tworzenia plików o sporych rozmiarach. Stworzenie 10 GiB czy 100 GiB pliku odbywa się w ułamku sekundy. Dla porównania, korzystając z dd i zapisując plik samymi zerami, utworzenie go zajęłoby kilka czy kilkanaście minut. Do tego doszłaby także 100% utylizacja procesora na czas tworzenia tego pliku. W przypadku rozrzedzonych plików zapisujemy jedynie kilka bajtów w strukturze systemu plików. W ten sposób możemy tworzyć pliki, które są większe niż rozmiar całej partycji. Problem zaczyna się w momencie, gdy taki plik zaczniemy wypełniać faktycznymi danymi. Spójrzmy sobie na ten obrazek poniżej (źródło):

rozrzedone-pliki-sparse-files

Zielonym kolorem oznaczono faktyczne dane w pliku. Szarym kolorem zaś zaznaczono puste bloki, które nie są zapisane na dysku. Gdy teraz do takiego pliku będziemy chcieli dodać kolejną porcję danych, w strukturze systemu plików zostaną opisane któreś z tych pozostałych pustych bloków widocznych powyżej. Niemniej jednak, oznacza to alokację tych bloków i jest ogromne prawdopodobieństwo, że bloki w tym pliki stracą ciągłość. W efekcie może dojść do poważnej fragmentacji takiego pliku z biegiem czasu. Oczywiście, jeśli korzystamy z dysków SSD, to fragmentacja, nawet dość znaczna, nie stanowi dla nas problemu. Trzeba jednak liczyć się z nią w przypadku talerzowych dysków twardych. Zawsze też można co jakiś czas przeprowadzić defragmentację plików przy pomocy e4defrag .

Jak na linux'ie stworzyć plik sparse

Są co najmniej dwie metody, które umożliwiają tworzenie plików rozrzedzonych. Pierwszym z nich jest skorzystanie z dd . W tym przypadku nie podajemy parametru if=/dev/zero oraz inaczej określamy wartości w opcjach bs i count . Dodajemy także parametr seek i to w oparciu o niego w połączeniu z bs będzie wyliczana wielkość wynikowego pliku. Poniżej jest przykładowe polecenie tworzące plik sparse o rozmiarze 10 GiB:

# dd of=sparse-file bs=10M seek=1024 count=0

Drugą opcją na stworzenie pliku sparse jest skorzystanie z narzędzia truncate dostarczanego w pakiecie coreutils . W tym przypadku składnia polecenia jest nieco inna i sporo prostsza:

# truncate -s 10G sparse-file

W obu tych powyższych przypadkach powinniśmy w mgnieniu oka uzyskać plik o rozmiarze 10 GiB. Poniżej zostało to ukazane w ls oraz du :

# ls -al sparse-file
-rw-r--r-- 1 root root 10G 2016-06-02 14:05:25 sparse-file

# du -h --apparent-size sparse-file
10G     sparse-file

Jak odróżnić pliki sparse od zwykłych plików

Jeśli popatrzymy na statystyki wolnego miejsca systemu plików, w którym ten rozrzedzony plik został utworzony, to nie zanotujemy żadnej różnicy. Nasuwa się zatem pytanie, jak odróżnić pliki typu sparse od normalnych plików? Jakby nie patrzeć, wszystkie menadżery plików jak i narzędzia będące w stanie odczytać rozmiar plików zwrócą nam wyniki podobne do tego jaki otrzymaliśmy wyżej w przypadku poleceń ls i du .

Część narzędzi potrafiących rozpoznawać pliki rozrzedzone ma dodatkowy przełącznik, który można wykorzystać do wykrycia takich plików. Dla przykładu weźmy sobie ten ls . On dysponuje opcją -s , która jest nam w stanie wskazać faktyczny rozmiar utworzonego powyżej pliku:

# ls -als sparse-file
0 -rw-r--r-- 1 root root 10G 2016-06-02 14:05:25 sparse-file

W du również jesteśmy w stanie podejrzeć faktyczny rozmiar tego pliku:

# du -h sparse-file
0       sparse-file

Widzimy zatem, że ten plik składa się z pustych bloków póki co, których nie trzeba zapisywać na dysk.

Tworzenie systemu plików dla plików sparse

Jednym z zastosowań, do których możemy wykorzystać taki plik rozrzedzony jest stworzenie na nim systemu plików. W ten sposób uzyskamy zwykły kontener na pliki, który będzie w stanie się rozrastać. Taki kontener może zostać wykorzystany do dowolnego celu. System plików tworzymy w poniższy sposób:

# mkfs.ext4 -m 0 -L obraz ./sparse-file
...
Superblock backups stored on blocks:
        32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632
...

Opcja -m ustawia procent zarezerwowanego miejsca dla użytkownika root. Natomiast -L to etykieta systemu plików. Jako, że struktura systemu plików sama w sobie trochę waży, to zobaczmy ile w obecnej chwili zajmuje nasz rozrzedzony plik:

# ls -als sparse-file
4.2M -rw-r--r-- 1 root root 10G 2016-06-02 14:05:25 sparse-file

Wcześniej było 0 bajtów, teraz już mamy nieco ponad 4 MiB. Podglądając strukturę tego pliku, to możemy zauważyć, że plik został pofragmentowany:

# filefrag -v /media/Grafi/sparse-file
Filesystem type is: ef53
File size of /media/Grafi/sparse-file is 10737418240 (2621440 blocks of 4096 bytes)
 ext:     logical_offset:        physical_offset: length:   expected: flags:
   0:        0..    1027:    1507328..   1508355:   1028:
   1:     1043..    1043:    1508371..   1508371:      1:
   2:     1059..    1059:    1508387..   1508387:      1:
   3:     9251..    9256:    1510435..   1510440:      6:    1516579:
   4:    32768..   32770:    1511424..   1511426:      3:    1533952:
   5:    98304..   98306:    1513472..   1513474:      3:    1576960:
   6:   163840..  163842:    1515520..   1515522:      3:    1579008:
   7:   229376..  229378:    1517568..   1517570:      3:    1581056:
   8:   294912..  294914:    1519616..   1519618:      3:    1583104:
   9:   524288..  524288:    1521664..   1521664:      1:    1748992:
  10:   819200..  819202:    1523712..   1523714:      3:    1816576:
  11:   884736..  884738:    1525760..   1525762:      3:    1589248:
  12:  1048576.. 1048577:    1527808..   1527809:      2:    1689600:
  13:  1081344.. 1081344:    1529856..   1529856:      1:    1560576:
  14:  1572864.. 1572864:    1531904..   1531904:      1:    2021376:
  15:  1605632.. 1605634:    1533952..   1533954:      3:    1564672:
  16:  2097152.. 2097152:    1536000..   1536000:      1:    2025472:
  17:  2097167.. 2097167:    1536015..   1536015:      1:             last
/media/Grafi/sparse-file: 15 extents found

Zatem mamy niby plik o faktycznym rozmiarze 4,2 MiB ale ma on 15 części. Plik został pokrojony za sprawą metadanych systemu plików. Jeśli się przyjrzymy uważnie, to w kolumnie logical_offset możemy dostrzec numery zapasowych sektorów głównego bloku systemu plików (te wpisane w mkfs.ext4 ). Im większy jest system plików, tym więcej jest kopi superbloka i więcej metadanych potrzebnych do opisu jego zawartości.

Gdybyśmy mieli zamiast tego pliku sparse zwykły plik wypełniony zerami i chcieli utworzyć na nim system plików, to on również by został tak pofragmentowany. Na dobrą sprawę będzie wyglądał tak samo jak ten wyżej. Przynajmniej w standardowej konfiguracji tworzenia systemu plików, tj. gdy tworzy się system plików bez opcji -E nodiscard . Jeśli się ją zastosuje, to wszystkie zera takiego pliku będą widoczne po stworzeniu systemu plików i taki kontener nie będzie ulegał fragmentacji, gdy będziemy go wypełniać jakąś zawartością. Natomiast problem z tymi plikami sparse jest taki, że tutaj nie mamy do dyspozycji zer. Dlatego te pliki będą się tak fragmenteować i praktycznie każdy nowy plik wgrany do kontenera sparse doda kolejny nieciągły zakres, czyli pofragmentuje kontener. W przypadku zwykłego systemu plików na partycji, to nie stanowi problemu ale, gdy w grę wchodzi jeden system plików opisany w innym systemie plików, to już tego typu mechanizm jest iście niewydolny właśnie przez fragmentację, której moim zdaniem się nie da uniknąć w przypadku plików sparse. Dlatego też zastanówmy się, czy aby na pewno potrzebujemy tych plików.

Tak czy inaczej, stworzony w ten sposób system plików możemy zamontować przy pomocy urządzeń loop i użytkować jak każdą inną partycję dysku twardego:

# mount -o loop sparse-file /mnt/

Zapiszmy teraz trochę danych w tym rozrzedzonym pliku (w katalogu /mnt/ ) i sprawdźmy co się stanie:

# ls -als sparse-file
2.1G -rw-r--r-- 1 root root 10G 2016-06-02 14:21:38 sparse-file

# filefrag /media/Grafi/sparse-file
/media/Grafi/sparse-file: 108 extents found

Można było się tego spodziewać. Zapisaliśmy 2,1 GiB danych i plik ma ponad 100 nieciągłych zakresów. Powyższy plik został umieszczony na partycji, która była praktycznie pusta. Pomimo tego faktu, plik uległ sporej fragmentacji.

Obsługa plików sparse przez aplikacje

Rozrzedzone pliki wymagają specjalnego traktowania. Cześć aplikacji nie potrafi rozpoznać tego typu plików i przy ich kopiowaniu czy pakowaniu przerabia te rozrzedzone pliki na zwykłe pliki. Niemniej jednak, spora część programów w linux'ie posiada wsparcie dla plików sparse i dysponuje odpowiednimi opcjami, które można podać przy wywoływaniu polecenia. Przykładem może być tworzenie paczki za pomocą tar czy kopiowanie plików z użyciem cp . Nawet rsync posiada stosowny parametr. Poniżej przykładowe polecenia:

# tar czpvfS file file.tar.gz
# rsync --sparse file another-file
# cp --sparse=always file another-file

Przerabianie zwykłych plików na sparse

W przypadku, gdy mamy do czynienia z plikami, które zawierają szereg pustych bloków, to nic nie stoi na przeszkodzie, by przerobić te pliki na pliki sparse. Przykładem mogą być obrazy partycji dysku, które robimy w formie cotygodniowego backupu. Trzeba tylko pamiętać, że wolne miejsce na takiej partycji nie oznacza automatycznie, że tam znajdują się same zera. Zawsze możemy zapisać wolną przestrzeń zerami przy pomocy dd . Jeśli mamy już utworzony obraz i chcielibyśmy go nieco odchudzić, to możemy wykopać w nim kilka dziur przy pomocy fallocate z pakietu util-linux . Poniżej jest plik 10 GiB, którego struktura zawiera całą masę bloków pustych. Statystyki w ls tego pliku wyglądają następująco:

# ls -als file.img
9.7G -rw-r--r-- 1 root root 10G 2016-06-02 14:59:07 file.img

Widzimy, że ten plik faktycznie zajmuje 9,7 GiB. Sporą część tego miejsca możemy odzyskać wpisując w terminalu to poniższe polecenie:

# fallocate -v -d file.img
file.img: 3.2 GiB (3415543808 bytes) converted to sparse holes.

# ls -als file.img
6.5G -rw-r--r-- 1 root root 10G 2016-06-02 15:04:40 file.img

W taki oto sposób odzyskaliśmy 3,2 GiB przestrzeni na dysku. Oczywiście, nie zalecałbym tego typu operacji na wszystkich plikach zgromadzonych na dysku. Niemniej jednak, w przypadku obrazów partycji, które leżą odłogiem, możemy im taki zabieg fundnąć bez wahania.

Tworzenie rozrzedzonego backupu dysku

Jeśli dopiero zamierzamy tworzyć backup dysku czy partycji, to istnieje nieco prostsza metoda oszczędzenia miejsca. Zamiast tworzyć backup przy pomocy dd i później kopać w nim dziury przez fallocate , można od razu zrobić obraz rozrzedzony. Do tego celu jednak będzie nam potrzebne narzędzie ddrescue dostępne w pakiecie gddrescue . Kopię partycji zaś wykonujemy w poniższy sposób:

# ddrescue -S -b 8Mi /dev/sda2 /mnt/2016-06-02-sda2

# ls -als 2016-06-02-sda2
241M -rw-r--r-- 1 root root 1.0G 2016-06-02 15:40:13 2016-06-02-sda2
Mikhail Morfikov avatar
Mikhail Morfikov
Po ponad 10 latach spędzonych z różnej maści linux'ami (Debian/Ubuntu, OpenWRT, Android) mogę śmiało powiedzieć, że nie ma rzeczy niemożliwych i problemów, których nie da się rozwiązać. Jedną umiejętność, którą ludzki umysł musi posiąść, by wybrnąć nawet z tej najbardziej nieprzyjemniej sytuacji, to zdolność logicznego rozumowania.