web-dev-qa-db-fra.com

Pourquoi l'image Alpine Docker est-elle 50% plus lente que l'image Ubuntu?

J'ai remarqué que mon application Python est beaucoup plus lente sous python:2-Alpine3.6 que sous Docker sous Ubuntu. J'ai proposé deux petites commandes de benchmark et il y a une énorme différence visible entre les deux systèmes d'exploitation, à la fois lorsque je les exécute sur un serveur Ubuntu et lorsque j'utilise Docker pour Mac.

$ BENCHMARK="import timeit; print(timeit.timeit('import json; json.dumps(list(range(10000)))', number=5000))"
$ docker run python:2-Alpine3.6 python -c $BENCHMARK
7.6094589233
$ docker run python:2-slim python -c $BENCHMARK
4.3410820961
$ docker run python:3-Alpine3.6 python -c $BENCHMARK
7.0276606959
$ docker run python:3-slim python -c $BENCHMARK
5.6621271420

J'ai aussi essayé le 'benchmark' suivant, qui n'utilise pas Python:

$ docker run -ti ubuntu bash
root@6b633e9197cc:/# time $(i=0; while (( i < 9999999 )); do (( i ++
)); done)

real    0m39.053s
user    0m39.050s
sys     0m0.000s
$ docker run -ti Alpine sh
/ # apk add --no-cache bash > /dev/null
/ # bash
bash-4.3# time $(i=0; while (( i < 9999999 )); do (( i ++ )); done)

real    1m4.277s
user    1m4.290s
sys     0m0.000s

Qu'est-ce qui pourrait causer cette différence?

34
Underyx

J'ai exécuté le même test que vous, en utilisant uniquement Python 3:

$ docker run python:3-Alpine3.6 python --version
Python 3.6.2
$ docker run python:3-slim python --version
Python 3.6.2

résultant en plus de 2 secondes de différence:

$ docker run python:3-slim python -c "$BENCHMARK"
3.6475560404360294
$ docker run python:3-Alpine3.6 python -c "$BENCHMARK"
5.834922112524509

Alpine utilise une implémentation différente de libc (bibliothèque système de base) à partir du projet musl ( URL miroir ). Il y a beaucoup de différences entre ces bibliothèques . En conséquence, chaque bibliothèque pourrait mieux fonctionner dans certains cas d'utilisation.

Voici un diff strace entre les commandes ci-dessus . La sortie commence à différer de la ligne 269. Bien entendu, il existe différentes adresses en mémoire, mais sinon, elles sont très similaires. Il est évident que la plupart du temps est passé à attendre que la commande python se termine.

Après avoir installé strace dans les deux conteneurs, nous pouvons obtenir une trace plus intéressante (j'ai réduit le nombre d'itérations dans le repère à 10).

Par exemple, glibc charge les bibliothèques de la manière suivante (ligne 182):

openat(AT_FDCWD, "/usr/local/lib/python3.6", O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC) = 3
getdents(3, /* 205 entries */, 32768)   = 6824
getdents(3, /* 0 entries */, 32768)     = 0

Le même code dans musl:

open("/usr/local/lib/python3.6", O_RDONLY|O_DIRECTORY|O_CLOEXEC) = 3
fcntl(3, F_SETFD, FD_CLOEXEC)           = 0
getdents64(3, /* 62 entries */, 2048)   = 2040
getdents64(3, /* 61 entries */, 2048)   = 2024
getdents64(3, /* 60 entries */, 2048)   = 2032
getdents64(3, /* 22 entries */, 2048)   = 728
getdents64(3, /* 0 entries */, 2048)    = 0

Je ne dis pas que c'est la principale différence, mais réduire le nombre d'opérations d'E/S dans les bibliothèques principales pourrait contribuer à de meilleures performances. D'après le diff, vous pouvez voir que l'exécution du même code Python peut entraîner des appels système légèrement différents. Le plus important pourrait probablement être fait pour optimiser les performances de la boucle. Je ne suis pas assez qualifié pour savoir si le problème de performances est causé par l'allocation de mémoire ou par une autre instruction.

  • glibc avec 10 itérations:

    write(1, "0.032388824969530106\n", 210.032388824969530106)
    
  • musl avec 10 itérations:

    write(1, "0.035214247182011604\n", 210.035214247182011604)
    

musl est plus lent de 0.0028254222124814987 secondes. À mesure que la différence grandit avec le nombre d'itérations, je suppose que la différence réside dans l'allocation en mémoire des objets JSON.

Si nous réduisons la référence à la seule importation json, nous remarquons que la différence n'est pas si énorme:

$ BENCHMARK="import timeit; print(timeit.timeit('import json;', number=5000))"
$ docker run python:3-slim python -c "$BENCHMARK"
0.03683806210756302
$ docker run python:3-Alpine3.6 python -c "$BENCHMARK"
0.038280246779322624

Charger des bibliothèques Python semble comparable. La génération de list() produit une plus grande différence:

$ BENCHMARK="import timeit; print(timeit.timeit('list(range(10000))', number=5000))"
$ docker run python:3-slim python -c "$BENCHMARK"
0.5666235145181417
$ docker run python:3-Alpine3.6 python -c "$BENCHMARK"
0.6885563563555479

De toute évidence, l'opération la plus chère est json.dumps(), ce qui peut indiquer des différences d'allocation de mémoire entre ces bibliothèques.

En regardant à nouveau le repère , musl est vraiment un peu plus lent dans l'allocation de mémoire:

                          musl  | glibc
-----------------------+--------+--------+
Tiny allocation & free |  0.005 | 0.002  |
-----------------------+--------+--------+
Big allocation & free  |  0.027 | 0.016  |
-----------------------+--------+--------+

Je ne sais pas ce que l'on entend par "grosse allocation", mais musl est presque 2 fois plus lent, ce qui peut devenir important lorsque vous répétez de telles opérations des milliers ou des millions de fois.

42
Tombart