Warum ist das Alpine Docker-Image mehr als 50% langsamer als das Ubuntu-Image?

35

Mir ist aufgefallen, dass meine Python-Anwendung beim Ausführen viel langsamer ist als python:2-alpine3.6ohne Docker unter Ubuntu. Ich habe zwei kleine Benchmark-Befehle gefunden, und es gibt einen großen Unterschied zwischen den beiden Betriebssystemen, sowohl wenn ich sie auf einem Ubuntu-Server ausführe als auch wenn ich Docker für Mac verwende.

$ 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

Ich habe auch den folgenden 'Benchmark' ausprobiert, der Python nicht verwendet:

$ 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

Was könnte diesen Unterschied verursachen?

Underyx
quelle
1
@Seth schau nochmal: Das Timing startet nach der Installation von bash in der gestarteten bash-Shell
Underyx 20.07.17

Antworten:

45

Ich habe den gleichen Benchmark wie Sie ausgeführt und nur Python 3 verwendet:

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

was zu mehr als 2 Sekunden Unterschied führt:

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

Alpine verwendet eine andere Implementierung von libc( Basissystembibliothek ) als das musl- Projekt ( Spiegel-URL ). Es gibt viele Unterschiede zwischen diesen Bibliotheken . Infolgedessen kann jede Bibliothek in bestimmten Anwendungsfällen eine bessere Leistung erbringen.

Hier ist ein Unterschied zwischen diesen Befehlen oben . Die Ausgabe beginnt sich von Zeile 269 zu unterscheiden. Natürlich gibt es verschiedene Adressen im Speicher, aber ansonsten ist es sehr ähnlich. Die meiste Zeit wird offensichtlich damit verbracht, auf das pythonEnde des Befehls zu warten .

Nach der Installation stracein beiden Containern erhalten wir eine interessantere Ablaufverfolgung (ich habe die Anzahl der Iterationen im Benchmark auf 10 reduziert).

Beispielsweise glibcist das Laden von Bibliotheken in der folgenden Weise (Leitung 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

Der gleiche Code in 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

Ich sage nicht, dass dies der Hauptunterschied ist, aber die Reduzierung der Anzahl der E / A-Vorgänge in Kernbibliotheken könnte zu einer besseren Leistung beitragen. Aus dem Diff können Sie ersehen, dass das Ausführen des gleichen Python-Codes zu geringfügig unterschiedlichen Systemaufrufen führen kann. Wahrscheinlich könnte das Wichtigste bei der Optimierung der Schleifenleistung getan werden. Ich bin nicht qualifiziert genug, um beurteilen zu können, ob das Leistungsproblem durch Speicherzuweisung oder eine andere Anweisung verursacht wird.

  • glibc mit 10 Iterationen:

    write(1, "0.032388824969530106\n", 210.032388824969530106)
    
  • musl mit 10 Iterationen:

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

muslist um 0.0028254222124814987 Sekunden langsamer. Da der Unterschied mit der Anzahl der Iterationen zunimmt, würde ich annehmen, dass der Unterschied in der Speicherzuordnung von JSON-Objekten liegt.

Wenn wir den Benchmark auf den reinen Import reduzieren, stellen jsonwir fest, dass der Unterschied nicht so groß ist:

$ 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

Das Laden von Python-Bibliotheken sieht vergleichbar aus. Generieren list()erzeugt größere Unterschiede:

$ 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

Offensichtlich ist die teuerste Operation json.dumps(), die auf Unterschiede in der Speicherzuordnung zwischen diesen Bibliotheken hindeuten könnte.

Wenn Sie sich den Benchmark noch einmal ansehen , muslist die Speicherzuweisung wirklich etwas langsamer:

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

Ich bin mir nicht sicher, was mit "große Zuweisung" gemeint ist, aber es muslist fast 2 × langsamer, was bedeutsam werden kann, wenn Sie solche Vorgänge Tausende oder Millionen Mal wiederholen.

Tombart
quelle
12
Nur ein paar Korrekturen. musl ist keine eigene Implementierung von glibc. 1st musl ist keine (Neu-) Implementierung von glibc, sondern eine andere Implementierung von libc nach POSIX-Standard. 2nd Musl ist nicht die eigene Sache von Alpine , es ist ein eigenständiges, nicht verwandtes Projekt und Musl wird nicht nur in Alpine verwendet.
Jakub Jirutka
Angesichts der Tatsache, dass musl libc auf mehr Standards * basiert, und der Tatsache, dass es eine neuere Implementierung gibt, warum scheint es in diesen Fällen glibc zu übertreffen? * vgl. wiki.musl-libc.org/functional-differences-from-glibc.html
Forest
Ist der Unterschied von 0.0028 Sekunden statistisch signifikant? Die relative Abweichung beträgt nur 0,0013% und Sie entnehmen 10 Proben. Was war die (geschätzte) Standardabweichung für diese 10 Läufe (oder sogar die Max-Min-Differenz)?
Peter Mortensen
@PeterMortensen Bei Fragen zu Benchmark-Ergebnissen sollten Sie sich an den Code von Eta Labs wenden : etalabs.net/libc-bench.html ZB wird der Malloc- Stresstest 100k-mal wiederholt. Die Ergebnisse können stark von der Bibliotheksversion, der GCC-Version und der verwendeten CPU abhängen, um nur einige Aspekte zu nennen.
Tombart