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"