Perché l'immagine Alpine Docker è più lenta del 50% rispetto all'immagine Ubuntu?


35

Ho notato che la mia applicazione Python è molto più lenta durante l'esecuzione python:2-alpine3.6rispetto a quando non viene eseguita senza Docker su Ubuntu. Ho trovato due piccoli comandi benchmark e c'è un'enorme differenza tra i due sistemi operativi, sia quando li eseguo su un server Ubuntu, sia quando utilizzo Docker per 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

Ho anche provato il seguente 'benchmark', che non usa 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

Cosa potrebbe causare questa differenza?


1
@Seth guarda di nuovo: i tempi iniziano dopo l'installazione di bash, all'interno della shell lanciata bash
Underyx,

Risposte:


45

Ho eseguito lo stesso benchmark di te, usando solo 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

con conseguente differenza di più di 2 secondi:

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

Alpine sta utilizzando un'implementazione diversa di libc(libreria del sistema di base) dal progetto musl ( URL mirror ). Ci sono molte differenze tra queste librerie . Di conseguenza, ogni libreria potrebbe funzionare meglio in alcuni casi d'uso.

Ecco una differenza diff tra questi comandi sopra . L'output inizia a differire dalla riga 269. Naturalmente ci sono diversi indirizzi in memoria, ma per il resto è molto simile. La maggior parte del tempo è ovviamente trascorso in attesa che il pythoncomando finisca.

Dopo l'installazione stracein entrambi i contenitori, possiamo ottenere una traccia più interessante (ho ridotto il numero di iterazioni nel benchmark a 10).

Ad esempio, glibcsta caricando le librerie nel modo seguente (riga 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

Lo stesso codice 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

Non sto dicendo che questa sia la differenza fondamentale, ma ridurre il numero di operazioni I / O nelle librerie core potrebbe contribuire a migliorare le prestazioni. Dal diff si può vedere che l'esecuzione dello stesso codice Python potrebbe portare a chiamate di sistema leggermente diverse. Probabilmente il più importante potrebbe essere fatto nell'ottimizzazione delle prestazioni del loop. Non sono abbastanza qualificato per giudicare se il problema di prestazioni è causato dall'allocazione di memoria o da qualche altra istruzione.

  • glibc con 10 iterazioni:

    write(1, "0.032388824969530106\n", 210.032388824969530106)
    
  • musl con 10 iterazioni:

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

muslè più lento di 0,0028254222124814987 secondi. Man mano che la differenza aumenta con il numero di iterazioni, suppongo che la differenza sia nell'allocazione di memoria degli oggetti JSON.

Se riduciamo il benchmark all'importazione unica json, notiamo che la differenza non è così grande:

$ 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

Il caricamento delle librerie Python sembra comparabile. La generazione list()produce una differenza maggiore:

$ 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

Ovviamente è l'operazione più costosa json.dumps(), che potrebbe indicare differenze nell'allocazione di memoria tra quelle librerie.

Guardando di nuovo al benchmark , muslè davvero leggermente più lenta nell'allocazione della memoria:

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

Non sono sicuro di cosa si intenda per "grande allocazione", ma muslè quasi 2 volte più lento, il che potrebbe diventare significativo quando si ripetono tali operazioni migliaia o milioni di volte.


12
Solo poche correzioni. musl non è l' implementazione propria di Alpine di glibc. Il 1 ° musl non è una (ri) implementazione di glibc, ma una diversa implementazione di libc secondo lo standard POSIX. Il 2 ° musl non è una cosa propria di Alpine , è un progetto autonomo e indipendente e il musl non viene usato solo in Alpine.
Jakub Jirutka,

dato che musl libc sembra un migliore basato su più standard *, per non parlare della più recente implementazione perché in questi casi sembra sottoperformare glibc? * cfr. wiki.musl-libc.org/functional-differences-from-glibc.html
Foresta

La differenza di 0,0028 secondi è statisticamente significativa? La deviazione relativa è solo dello 0,0013% e si stanno prendendo 10 campioni. Qual è stata la deviazione standard (stimata) per quelle 10 corse (o anche la differenza max-min)?
Peter Mortensen,

@PeterMortensen Per domande relative ai risultati dei benchmark, fare riferimento al codice Eta Labs: etalabs.net/libc-bench.html Ad esempio, lo stress test malloc viene ripetuto 100k volte. I risultati potrebbero dipendere fortemente dalla versione della libreria, dalla versione GCC e dalla CPU utilizzata, solo per citarne alcuni aspetti.
Tombart
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.