Compression sur plusieurs CPUs (pigz, pbzip2)

le

La compression c'est bon, mangez-en. Au fil du temps je me suis rendu compte que j'utilisais principalement la compression pour gagner du temps plutôt que pour gagner de la place. A priori c'est absurde, le compromis espace/temps ne permet de tirer que d'un côté à la fois. Mais ça dépend du contexte et du goulet d'étranglement de l'opération.

Le moindre CPU > 2GHz permet de compresser avec gzip à un débit proche de celui d'un disque en écriture (80 à 100 MB/s). Voici quelques mesures rapides sur un "vieux" serveur 4 cores (Xeon L5335 @2GHz, SAS RAID1) :

~# hdparm -t /dev/sda
 Timing buffered disk reads: 236 MB in  2.77 seconds =  85.17 MB/sec

~# pv -r </dev/zero > /dev/null 
[3.8GB/s]

~# pv -r </dev/zero | gzip >/dev/null
[84MB/s]

Le disque peut donc soutenir un accès séquentiel à 85 MB/s, ça nous donne la limite supérieure. Puis un premier test permet de vérifier que /dev/zero peut générer un flux important et ne sera pas le goulet d'étranglement (on s'en doutait, mais l'adminsys défie tant de théories...). Le second test mesure le débit entrant de gzip. J'avoue être étonné de constater qu'il peut compresser un flux entrant équivalent à ce que je peux obtenir en lecture depuis les disques (84 ~ 85 MB/s), je m'attendais à des performances supérieures. Le compromis espace/temps est peut être plus subtil qu'il n'y paraît, d'où ce billet qui m'oblige à me poser des questions.

Comme gzip et bzip2 ont des implémentations "parallélisées" robustes et disponibles, je me suis proposé de les bencher. Pour le logiciel, tout est issu de Debian 6.0 (architecture amd64) :

  • guest Xen 4.0 (pas de steal, il s'agit du seul domU)
  • gzip 1.3.12
  • pigz 2.1.6
  • bzip2 1.0.5
  • pbzip2 1.1.1

Un script exécute une série de tarballs avec diverses options de compression et mesure le temps passé, le débit moyen en lecture et en écriture. Un jeux de donnée issu d'un site de prod est utilisé, il contient des data divers (SQL, MongoDB, code, logs, etc). Il est évident que suivant le taux de compression obtenu les performances peuvent varier, donc je fournis le script pour que vous puissiez facilement tester sur vos propres data.

Il y a deux séries de tests. La première série (A) écrit le tarball compressé sur le filesystem source : c'est une situation non optimale mais c'est aussi la plus courante (surtout pour les développeurs, qui sont mes utilisateurs finaux). Les économies d'écriture obtenues par la compression peuvent faire gagner beaucoup de temps. La seconde série de tests (B) suppose que la destination du tarball n'est pas le goulet d'étranglement (accès vers un jeu de disques différent, filer, SAN, etc).

~# ./compression-bench /var /tmp/out
Source inodes             : 95580
Source disk usage (bytes) : 3414117296

   Command                                   Time(s)   In(MiB/s)  Out(MiB/s)  Comp.ratio
A1 tar -c  /var >/tmp/out                     187.17        17.3        17.5      100.6%
A2 tar -cz /var >/tmp/out                     180.65        18.0         2.2       12.2%
A3 tar -cj /var >/tmp/out                     494.00         6.5          .7       10.7%
A4 tar -c  /var |pigz -p1 >/tmp/out           196.32        16.5         2.0       12.2%
A5 tar -c  /var |pigz -p2 >/tmp/out           149.84        21.7         2.6       12.2%
A6 tar -c  /var |pigz -p4 >/tmp/out           139.54        23.3         2.8       12.2%
A7 tar -c  /var |pbzip2 -p1 >/tmp/out         419.94         7.7          .8       10.9%
A8 tar -c  /var |pbzip2 -p2 >/tmp/out         249.02        13.0         1.4       10.9%
A9 tar -c  /var |pbzip2 -p4 >/tmp/out         186.39        17.4         1.9       10.9%
--
B1 tar -c  /var                               128.09        25.4        25.5      100.6%
B2 tar -cz /var                               174.43        18.6         2.2       12.2%
B3 tar -cj /var                               490.22         6.6          .7       10.7%
B4 tar -c  /var |pigz -p1                     191.67        16.9         2.0       12.2%
B5 tar -c  /var |pigz -p2                     141.14        23.0         2.8       12.2%
B6 tar -c  /var |pigz -p4                     132.54        24.5         3.0       12.2%
B7 tar -c  /var |pbzip2 -p1                   414.04         7.8          .8       10.9%
B8 tar -c  /var |pbzip2 -p2                   242.16        13.4         1.4       10.9%
B9 tar -c  /var |pbzip2 -p4                   180.66        18.0         1.9       10.9%

Le temps de référence sera constitué par les 128 secondes pour la simple lecture complète des données (test B1), il sera impossible de faire mieux. Le test A1 montre que la concurrence des lectures et écritures fait chuter ce temps à 187 secondes. Si on rajoute un simple gzip (test A2), le gain de temps est ridicule avec 180 secondes : le goulet d'étranglement semble être au niveau des I/O puisque le débit observé (18 MiB/s) est bien inférieur à ce que peux traiter un seul CPU.

Puis pigz rentre en jeu : avec un seul thread il est hélas un peu plus lent que gzip et c'est bêta (196 secondes, test A4), mais dès 2 threads on passe à 150 secondes (test A5), puis enfin 140 secondes avec 4 threads : juste 9% plus lent que sans sans écriture du tarball (test A6) ! pigz utilisant automatiquement le nombre de CPUs disponibles, je peux donc simplement remplacer toute invocation de gzip par pigz et... profit.

La série B donne des résultats plus attendus, quoique le test B2 est curieux : une simple compression gzip ralentit globalement le tarball. Je n'ai pas creusé mais je suppose que le "pipe" a un buffer un peu court et la compression crée des pauses côté I/O dans tar (toute analyse moins pifométrique est bienvenue !). Ce qui m'a encouragé à considérer deux choses:

  • préférer compresser côté destination, par exemple : tar -c /foo | ssh filer 'pigz >foo.tar.gz' (ou via nc car vous allez me rétorquer que SSH va aussi être un goulet d'étranglement niveau CPU, ça peut faire un autre billet...)

  • finalement utiliser les CPUs de ces filers ou serveurs de backups qui n'en avait qu'un usage fort modeste : plus je compresse, plus je peux exécuter d'opérations en parallèle car j'économise des I/O. Je ne suis pas allé jusqu'au filesystem compressé mais pourquoi pas...

Au passage, bzip2 devient presque attractif : en général il est tellement plus lent que gzip en compression que je le réserve aux cas où le gain de place (ou de BP pour de gros mirroirs) est vraiment souhaitable. Mais pbzip2 sur 4 coeurs est aussi rapide que gzip sur 1 coeur (test A9, 186 secondes). Ca peut relancer la donne...


Partager cet article :