sparse file

Published: 16-11-2016

Updated: 10-05-2017

By: Maxime de Roucy

tags: fallocate sparse-file system

Un sparse file est un fichier dont la taille apparente (affiché par ls -l ou du --apparent-size) est différente de sa taille réelle sur le disque (affiché par ls -s ou du). C’est possible via l’utilisation de métadata prenant très peu de place (moins que les données qu’elles représantent). Si un fichier contient des longue portion de \0, le noyau peut les enregistré sous forme de métadata (eg. que des \0 entre la position 1GiB et 2GiB), plutôt que de les physiquement un par un sur le disque.

max@testhost % ls -l test
-rw-r--r-- 1 max users 16K 20 janv. 17:16 test
max@testhost % du -k --apparent-size test
16      test
max@testhost % ls -s test
8,0K test
max@testhost % du -k test
8       test

création

Pour faire des tests (ou autre) vous pouvez créé un fichier sparse facilement via les outils suivant.

dd

max@testhost % dd if=/dev/urandom of=testfile bs=4KiB count=1 seek=1
1+0 enregistrements lus
1+0 enregistrements écrits
4096 bytes (4,1 kB, 4,0 KiB) copied, 0,00189629 s, 2,2 MB/s
max@testhost % du -k --apparent-size testfile ; du -k testfile
8       testfile
4       testfile

fallocate

max@testhost % fallocate -l 8KiB testfile
max@testhost % du -k --apparent-size testfile ; du -k testfile
8       testfile
8       testfile
max@testhost % fallocate --punch-hole --offset 0 --length 4KiB testfile
max@testhost % du -k --apparent-size testfile ; du -k testfile
8       testfile
4       testfile

C++

sparse.cpp créé un fichier sparse de 10GiB et ajoute, toutes les secondes, un nombre en fin de fichier.

truncate sur fichier ouvert

Lorsqu’un fichier est ouvert en écriture simple (>, flags: XXXX0XX[12] dans /proc/…/fdinfo/…) par un processus et qu’un truncate (ou assimilé) est effectué sur le fichier (eg. par copytruncate de logrotate) la position d’écriture du processus dans le fichier n’est pas modifié. Au début de la première écriture qui suit le truncate, le noyau créé des \0 sous forme de métadata (le noyau créé donc un sparse file) pour combler l’espace entre la fin du fichier et la position d’écriture du processus. Si on prend l’exemple d’un fichier de deux blocs, que l’on truncate à un bloc.

init
état du fichier, et position d’écriture du processus dans le fichier: <data:truc><data:much>|
Position d’écriture du processus dans le fichier: 2 blocs après le début du fichier.
action: truncate
état du fichier, et position d’écriture du processus dans le fichier: <data:truc>-----------|
Position d’écriture du processus dans le fichier: 2 blocs après le début du fichier. Note: la position d’écriture du processus dans le fichier correspond à une position qui n’existe pas dans le fichier. Ce n’est pas grave, le noyau et le processus s’en foutent.
action: écriture
état du fichier, et position d’écriture du processus dans le fichier: <data:truc><metadata:\0><data:plop>|
Position d’écriture du processus dans le fichier: 3 blocs après le début du fichier. Note: en début d’écriture, le noyau enregistre des \0 sous forme de métadata pour combler l’écart entre la fin du fichier et la position d’écriture du processus dans le fichier à ce moment là ; transformant le fichier en sparse file.

Par exemple avec un fichier de quatre blocs de 4KiB :

max@testhost % mkfifo fifo ; dd if=/dev/urandom of=testfile bs=16KiB count=1
1+0 enregistrements lus
1+0 enregistrements écrits
16384 bytes (16 kB, 16 KiB) copied, 0,00196167 s, 8,4 MB/s
max@testhost % du -k --apparent-size testfile ; du -k testfile
16      testfile
16      testfile
max@testhost % ./open fifo testfile &
[1] 6507
max@testhost % lsof testfile
COMMAND  PID USER   FD   TYPE DEVICE SIZE/OFF    NODE NAME
open    6507  max    3w   REG   0,43    16384 3262087 testfile
max@testhost % cat /proc/6507/fdinfo/3
pos:    16384
flags:  02100001
mnt_id: 74
max@testhost % truncate -s 0 testfile
max@testhost % du -k --apparent-size testfile ; du -k testfile
0       testfile
0       testfile
max@testhost % echo "test" > fifo
[1]  + done       ./open fifo testfile
max@testhost % du -k --apparent-size testfile ; du -k testfile
17      testfile
4       testfile
max@testhost % hexdump testfile | head -5
0000000 0000 0000 0000 0000 0000 0000 0000 0000
*
0004000 6574 7473 000a
0004005

open est un petit programe créé pour cette démonstration, il ouvre testfile, se déplace à la fin du fichier est y écrit ce qu’il lit dans fifo.

Lorsqu’un fichier est ouvert en écriture/append (>>, flags: XXXX2XX[12] dans /proc/…/fdinfo/…) par un processus, la position d’écriture du processus n’est plus enregistré. Le noyau dirige toutes les écriture vers la fin du fichier. Il n’y a donc plus besoin de « comblé un espace » après un truncate. Après un truncate, le fichier n’est pas transformé en sparse file à la première écriture du processus.

max@testhost % mkfifo fifo ; dd if=/dev/urandom of=testfile bs=16KiB count=1
1+0 enregistrements lus
1+0 enregistrements écrits
16384 bytes (16 kB, 16 KiB) copied, 0,00414701 s, 4,0 MB/s
max@testhost % du -k --apparent-size testfile ; du -k testfile
16      testfile
16      testfile
max@testhost % ./open -append fifo testfile &
[1] 6674
max@testhost % lsof testfile
COMMAND  PID USER   FD   TYPE DEVICE SIZE/OFF    NODE NAME
open    6674  max    3w   REG   0,43    16384 3262169 testfile
max@testhost % cat /proc/6674/fdinfo/3
pos:    0
flags:  02102001
mnt_id: 74
max@testhost % truncate -s 0 testfile
max@testhost % du -k --apparent-size testfile ; du -k testfile
0       testfile
0       testfile
max@testhost % echo "test" > fifo
[1]  + done       ./open -append fifo testfile
max@testhost % du -k --apparent-size testfile ; du -k testfile
1       testfile
4       testfile
max@testhost % hexdump testfile | head -5
0000000 6574 7473 000a
0000005

Le fichier n’est pas non plus transformé en sparse s’il est uniquement ouvert en lecture (<, flags: XXXXXXX0 dans /proc/…/fdinfo/…).

L’article fallocate & truncate traite aussi de ce sujet.

problème

copytruncate

Normalement, avec lorsqu’on copie un fichier sparse avec cp la copie sera, elle aussi, un fichier sparse (cp ne s’ammuse pas à réellement écrire sur le disque des GiB de \0).

Pendant un temps logrotate soufrait d’un bug : copytruncate copiait les fichier sparse sous forme de fichier « normaux ». La taille de la copie sur le disque était égale à la taille apparente du fichier original. Ce bug a été corrigé dans le commit f1dc0d9adc67aafebc55df985b42475eb24646f8 inclue à partir de la version 3.9.0 de logrotate.

Les démons (mal écrit) qui n’offrait pas la possibilité de rouvrir leurs fichier de log (ouvert en écriture non append), obligaient l’utilisation du copytruncate de logrotate. Le truncate engendrait la création de sparse file.
Par exemple, sous Debian, SOLR log sa sortie/erreur standard via un truc du style suivant.

truc &> "console.log"

La taille apparente de ce fichier ne faisait qu’augmenter au file du temps. Le problème survenait au moment de la copie (dans le copytruncate). Le fichier créé n’était pas un sparse et pouvais prendre tout l’espace disque disponible.

Pour résoudre ce problème il suffisait de changer le script d’init du démon :

echo -n > "console.log"
truc &>> "console.log"