dump-deallocate

Published: 25-01-2017

Updated: 20-02-2017

By: Maxime de Roucy

tags: fallocate sparse-file

Sur des infras saturées il m’est arrivé de tomber sur des fichiers de logs trop gros pour être copié ou compressé dans un fichier séparé.

dd et truncate

Lorsque le fichier n’est pas ouvert en écriture par un autre processus je le compresse généralement dans lui même puis je le truncate (suppression de la fin du fichier). Cette technique fonctionne car les données compressées prennent moins de place que les données initiales (et que j’ouvre le fichier en écriture avec l’option « notruncate »). Je n’écrase donc jamais de données non lus.

max@host-test % echo toto > toto
max@host-test % gzip -c toto | dd of=toto conv=notrunc
0+1 records in
0+1 records out
30 bytes (30 B) copied, 4,0995e-05 s, 732 kB/s
max@host-test % truncate -s 30 toto
max@host-test % mv toto toto.gz
max@host-test % gunzip toto.gz
max@host-test % cat toto
toto

écriture append et non append

Lorsqu’un fichier est ouvert en écriture « append », il ne conserve pas de pointeur de position pour ses opérations d’écriture, toutes les écritures se font en fin de fichier. Lorsqu’un fichier est ouvert en écriture « normal » (non append), il conserve un pointeur de position pour ses opérations d’écriture. Chaque écriture déplace le pointeur pour éviter d’écrire deux fois au même endroit (le processus peu quand même déplacer le pointeur où il veut, mais il doit le faire de façon explicite).

Imaginons un fichier de log de 100MiB ouvert en écriture « normal » par un processus α, généralement le pointeur d’écriture β est situé en fin de fichier, à 100MiB. Si on fait un truncate -s 0 de ce fichier, celui-ci est ramené à une taille de 0. Lors de l’écriture suivante d’α dans le fichier, celui-ci va chercher à écrire à la position β (100MiB). Le noyau créé une métadonné indiquant : de 0 à 100MiB le fichier ne contient que des \0. En réalité, aucun \0 n’est écrit sur le disque. De cette façon, le fichier fait bien 100MiB (en apparence), α peut faire son écriture et l’espace disque est conservé. Le fichier final resemble à <100MiB de \0 qui ne prenne en réalité que quelques octets sur le disque><données nouvellement écrite>.

Note : Attention, logrotate/copytruncate à longtemps posé problème lors du traitement de log de type sparse file.

dd/truncate et écriture

Pour les fichier ouvert en écriture ma technique gzip | dd puis truncate n’est pas vraiment conseillée. À la première écriture on se retrouve avec un fichier formaté comme suit : <gzip>< \0 ou ∅ ><nouvelles données>. Je rappel que < \0 ou ∅ > ne prend pas de place sur le disque.

Si le système de fichier possède assez de place il est possible de copier <gzip> dans un nouveau fichier et de faire un fallocate punch-hole ou collapse-range pour transformer le fichier inital en < \0 ou ∅ >< \0 ou ∅ ><nouvelles données>, soit < \0 ou ∅ ><nouvelles données>.

max@host-test % mkfifo fifo ; dd if=/var/log/pacman.log of=testfile bs=16KiB count=1
1+0 enregistrements lus
1+0 enregistrements écrits
16384 bytes (16 kB, 16 KiB) copied, 0,00157015 s, 10,4 MB/s
max@host-test % du -k --apparent-size testfile ; du -k testfile
16      testfile
16      testfile
max@host-test % ./open fifo testfile &
[1] 5049
max@host-test % lsof testfile
COMMAND  PID USER   FD   TYPE DEVICE SIZE/OFF    NODE NAME
open    5049  max    3w   REG   0,43    16384 3321029 testfile
max@host-test % # testfile : <données1><données2>
max@host-test % gzip -c testfile | dd of=testfile conv=notrunc
5+1 enregistrements lus
5+1 enregistrements écrits
2736 bytes (2,7 kB, 2,7 KiB) copied, 0,000168001 s, 16,3 MB/s
max@host-test % # testfile : <gzip données1 & 2><données2>
max@host-test % truncate -s 2736 testfile
max@host-test % # testfile : <gzip données1 & 2> ; maintenant appelé <gzip>
max@host-test % echo "test" > fifo
[1]  + done       ./open fifo testfile
max@host-test % # testfile : <gzip><\0><test>
max@host-test % hexdump -C testfile | tail -5
00000aa0  fd 8e 2d bc 73 f1 fe bf  07 49 94 d8 00 40 00 00  |..-.s....I...@..|
00000ab0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00004000  74 65 73 74 0a                                    |test.|
00004005
max@host-test % # le * indique que les 0000 vont de la position ab0 à 4000
max@host-test % # les 0000 ne prennent pas de place sur le disque, il s'agit d'un sparse file
max@host-test % du -k --apparent-size testfile ; du -k testfile
17      testfile
12      testfile
max@host-test % dd if=testfile of=testfile.gz bs=512 count=$((2736/512 + 1))
6+0 enregistrements lus
6+0 enregistrements écrits
3072 bytes (3,1 kB, 3,0 KiB) copied, 0,00179942 s, 1,7 MB/s
max@host-test % # testfile : <gzip><\0\0><test>
max@host-test % # testfile.gzip : <gzip><\0>
max@host-test % truncate -s 2736 testfile.gz
max@host-test % # testfile.gzip : <gzip>
max@host-test % fallocate --punch-hole --offset 0 --length 2736 testfile
max@host-test % # testfile : <\0\0\0><test>
max@host-test % hexdump -C testfile
00000000  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00004000  74 65 73 74 0a                                    |test.|
00004005
max@host-test % du -k --apparent-size testfile ; du -k testfile
17      testfile
8       testfile

open est un petit programe créé pour cette démonstration, qui ouvre testfile (par défaut, en mode écriture « normal »), se déplace à la fin du fichier est y écrit ce qu’il lit dans fifo.

Si on utilise fallocate collapse-range en lieu et place de fallocate punch-hole les \0 sont supprimés et la taille apparente du fichier correspond à sa taille sur le disque.

On voit que, pendant quelques temps, on se retrouve avec <gzip><\0\0><test> et <gzip><\0> sur le disque. <gzip> pouvant être très gros, cette technique n’est parfois pas possible.

dump-deallocate

C’est là qu’entre en scène dump-deallocate.

C’est un program en go qui permet de dumper un fichier sur sa sortie standard, tout en supprimant du fichier les données déjà dumpées. Il libère l’espace disque au fure est à mesure de l’avancement du dump. D’une certaine façon, il « consome » le fichier.

dump-deallocate gros.log | gzip > petit.gz

En interne :

  1. lecture de 32KiB de donnée du fichier
  2. écriture de ces données dans la sortie standard
  3. fallocate punch-hole des 32KiB précédemment lus
  4. retour à l’étape 1

Tout est décrit dans l’help :

max@laptop % dump-deallocate -h
Usage: ../personnel/dump-deallocate/dump-deallocate [-b BYTES] [-c|-t|-r] FILE
 Dump FILE on stdout and deallocate it at the same time.
 More precisely:
   1. read BYTES bytes from FILE
   2. write thoses bytes on stdout
   3. deallocate BYTES bytes from FILE (fallocate punch-hole)
      and go back to 1.

Options:
 -b, --buffer-size BYTES
        Memory buffer size in byte (default 32KiB).
        BYTES  may  be followed by the following multiplicative suffixes:
         - IEC unit:
           - KiB = 1024
           - MiB = 1024×1024
           …
           - EiB = 1024⁶
         - SI unit:
           - KB = 1000
           - MB = 1000×1000
           …
           - EB = 1000⁶

 -c, --collapse
        At the end of the whole dump, remove/collapse (with fallocate collapse-range)
        the greatest number of filesystem blocks already dumped.
        On normal condition, at the end, FILE will size one filesystem block.

        Supported on ext4 from Linux 3.15.

 -C, --collapse-test
        Test the collapse functionnality.
        Create a file named dump-deallocate-collapse-test-<random str>
        in the working directory and try to collapse it.
        Remove file after the test.

 -t, --truncate
        Truncate FILE (to size 0) at the end of the whole dump
        It is not recommended since another process can write in FILE between
        the last read and the truncate call.
        On normal condition, at the end, FILE will size 0.

 -r, --remove
        Remove FILE at the end of the whole dump
        It is not recommended since another process might be using FILE.

Un autre avantage de dump-deallocate est le gain de place dès le lancement de la commande. On a plus besoin d’attendre que l’archive soit fini pour récupérer de l’espace disque. Du coup je l’utilise tout le temps, même pour les fichier non ouvert par d’autre processus.