Dlaczego obraz Alpine Docker jest o ponad 50% wolniejszy niż obraz Ubuntu?

35

Zauważyłem, że moja aplikacja Python działa o wiele wolniej python:2-alpine3.6niż na niej bez Dockera na Ubuntu. Wymyśliłem dwa małe polecenia testu porównawczego i między tymi dwoma systemami operacyjnymi widoczna jest ogromna różnica, zarówno podczas uruchamiania ich na serwerze Ubuntu, jak i podczas korzystania z Docker na komputery 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

Próbowałem także następującego „testu porównawczego”, który nie używa Pythona:

$ 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

Co może być przyczyną tej różnicy?

Underyx
źródło
1
@Zobacz jeszcze raz: czas zaczyna się po zainstalowaniu bash, wewnątrz uruchomionej powłoki bash
Underyx

Odpowiedzi:

45

Uruchomiłem ten sam test co ty, używając tylko Pythona 3:

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

co powoduje różnicę przekraczającą 2 sekundy:

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

Alpine używa innej implementacji libc(podstawowej biblioteki systemowej) niż projekt musl ( lustrzany URL ). Istnieje wiele różnic między tymi bibliotekami . W rezultacie każda biblioteka może działać lepiej w niektórych przypadkach użycia.

Oto różnica między tymi poleceniami powyżej . Wyjście zaczyna się różnić od wiersza 269. Oczywiście w pamięci są różne adresy, ale poza tym jest bardzo podobne. Większość czasu jest oczywiście poświęcana na oczekiwanie na zakończenie pythonpolecenia.

Po zainstalowaniu stracew obu kontenerach możemy uzyskać bardziej interesujący ślad (zmniejszyłem liczbę iteracji w teście do 10).

Na przykład glibcładuje biblioteki w następujący sposób (wiersz 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

Ten sam kod w 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

Nie twierdzę, że jest to kluczowa różnica, ale zmniejszenie liczby operacji we / wy w bibliotekach podstawowych może przyczynić się do lepszej wydajności. Z porównania widać, że wykonanie tego samego kodu Pythona może prowadzić do nieco innych wywołań systemowych. Prawdopodobnie najważniejszą rzeczą może być optymalizacja wydajności pętli. Nie mam wystarczających kwalifikacji, aby ocenić, czy problem z wydajnością jest spowodowany alokacją pamięci lub innymi instrukcjami.

  • glibc z 10 iteracjami:

    write(1, "0.032388824969530106\n", 210.032388824969530106)
    
  • musl z 10 iteracjami:

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

musljest wolniejszy o 0,0028254222124814987 sekund. Ponieważ różnica rośnie wraz z liczbą iteracji, zakładam, że różnica polega na alokacji pamięci obiektów JSON.

Jeśli ograniczymy test do samego importu json, zauważymy, że różnica nie jest aż tak duża:

$ 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

Ładowanie bibliotek Pythona wygląda porównywalnie. Generowanie list()powoduje większą różnicę:

$ 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

Oczywiście jest to najdroższa operacja json.dumps(), która może wskazywać na różnice w alokacji pamięci między tymi bibliotekami.

Patrząc znów na benchmarku , musljest naprawdę nieznacznie wolniejszy w alokacji pamięci:

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

Nie jestem pewien, co należy rozumieć przez „duży przydział”, ale musljest prawie 2 razy wolniejszy, co może stać się znaczące, gdy powtórzy się takie operacje tysiące lub miliony razy.

Tombart
źródło
12
Tylko kilka poprawek. musl nie jest własną implementacją glibc przez Alpine . 1st musl nie jest (re) implementacją glibc, ale inną implementacją libc dla standardu POSIX. 2-ty MUSL nie jest Alpine własna sprawa, to samodzielna, niezwiązane projekt i MUSL nie jest używany tylko w Alpine.
Jakub Jirutka
biorąc pod uwagę, że musl libc wydaje się lepszym rozwiązaniem opartym na standardach *, nie wspominając już o nowszej implementacji, dlaczego wydaje się, że w tych przypadkach wydaje się gorzej niż w przypadku glibc? * por. wiki.musl-libc.org/functional-differences-from-glibc.html
Las
Czy różnica wynosząca 0,0028 sekundy jest statystycznie istotna? Względne odchylenie wynosi tylko 0,0013% i pobierasz 10 próbek. Jakie było (szacunkowe) odchylenie standardowe dla tych 10 przebiegów (a nawet różnica maks./min.)?
Peter Mortensen
@PeterMortensen W przypadku pytań dotyczących wyników testu porównawczego należy zapoznać się z kodem Eta Labs: etalabs.net/libc-bench.html Np. Test warunków skrajnych malloc powtarza się 100 razy. Wyniki mogą być silnie zależne od wersji biblioteki, wersji GCC i zastosowanego procesora, żeby wymienić tylko kilka aspektów.
Tombart