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

35

J'ai remarqué que mon application Python est beaucoup plus lente lors de son exécution python:2-alpine3.6que de son exécution sans Docker sur Ubuntu. J'ai trouvé deux petites commandes de référence 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 également 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?

Underyx
la source
1
@Seth regarde à nouveau: le chronométrage commence après l'installation de bash, à l'intérieur du shell bash lancé
Underyx

Réponses:

45

J'ai exécuté le même benchmark 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

entraînant une différence de plus de 2 secondes:

$ 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 du système de base) du projet musl ( URL miroir ). Il existe de nombreuses différences entre ces bibliothèques . Par conséquent, chaque bibliothèque peut être plus performante dans certains cas d'utilisation.

Voici une différence entre ces commandes ci-dessus . La sortie commence à différer de la ligne 269. Bien sûr, il y a différentes adresses en mémoire, mais sinon c'est très similaire. La plupart du temps est évidemment passé à attendre la fin de la pythoncommande.

Après l'installation stracedans les deux conteneurs, nous pouvons obtenir une trace plus intéressante (j'ai réduit le nombre d'itérations dans le benchmark à 10).

Par exemple, glibccharge 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 la réduction du nombre d'opérations d'E / S dans les bibliothèques principales pourrait contribuer à de meilleures performances. Du diff, vous pouvez voir que l'exécution du même code Python peut conduire à 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 suffisamment qualifié pour juger si le problème de performances est causé par l'allocation de mémoire ou 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)
    

muslest plus lent de 0,0028254222124814987 secondes. Comme la différence augmente avec le nombre d'itérations, je suppose que la différence réside dans l'allocation de mémoire des objets JSON.

Si nous réduisons la référence à l'importation uniquement, jsonnous 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

Le chargement des bibliothèques Python semble comparable. Générer list()produit une différence plus importante:

$ 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

Évidemment, l'opération la plus coûteuse est json.dumps(), ce qui pourrait indiquer des différences d'allocation de mémoire entre ces bibliothèques.

En regardant à nouveau le benchmark , l' muslallocation de mémoire est vraiment légèrement plus lente:

                          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 elle muslest presque 2 fois plus lente, ce qui peut devenir significatif lorsque vous répétez de telles opérations des milliers ou des millions de fois.

Tombart
la source
12
Juste quelques corrections. musl n'est pas la propre implémentation de la glibc par Alpine . 1st musl n'est pas une (re) implémentation de glibc, mais une implémentation différente de libc selon la norme POSIX. 2ème MUSL n'est pas Alpine propre chose, il est un autonome, un projet indépendant et MUSL n'est pas utilisé seulement dans Alpine.
Jakub Jirutka
étant donné que la libl musl semble être une meilleure base de normes *, sans parler d'une implémentation plus récente, pourquoi semble-t-elle sous-performer la glibc dans ces cas? * cf. wiki.musl-libc.org/functional-differences-from-glibc.html
Forest
La différence de 0,0028 seconde est-elle statistiquement significative? L'écart relatif n'est que de 0,0013% et vous prenez 10 échantillons. Quel était l'écart type (estimé) pour ces 10 essais (ou même la différence max-min)?
Peter Mortensen
@PeterMortensen Pour les questions concernant les résultats de référence, vous devez vous référer au code Eta Labs: etalabs.net/libc-bench.html Par exemple, le test de résistance au malloc est répété 100 000 fois. Les résultats pourraient dépendre fortement de la version de la bibliothèque, de la version de GCC et du processeur utilisé, pour n'en nommer que quelques aspects.
Tombart