Почему образ Alpine Docker более чем на 50% медленнее, чем образ Ubuntu?

35

Я заметил, что мое приложение на Python работает намного медленнее, python:2-alpine3.6чем без Docker в Ubuntu. Я предложил две небольшие команды тестирования, и между двумя операционными системами заметна огромная разница, как при запуске их на сервере Ubuntu, так и при использовании Docker для 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

Я также попробовал следующий «бенчмарк», который не использует 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

Что может быть причиной этой разницы?

Underyx
источник
1
@ Смотри еще раз: отсчет времени начинается после установки bash, внутри запущенной оболочки bash
Underyx

Ответы:

45

Я выполнил тот же тест, что и вы, используя только 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

в результате разница более 2 секунд:

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

Alpine использует реализацию libc(базовую системную библиотеку), отличную от проекта musl ( зеркальный URL ). Есть много различий между этими библиотеками . В результате каждая библиотека может работать лучше в определенных случаях использования.

Вот разница между этими командами выше . Вывод начинает отличаться от строки 269. Конечно, в памяти есть разные адреса, но в остальном он очень похож. Большая часть времени, очевидно, тратится на ожидание pythonкоманды.

После установки straceв оба контейнера мы можем получить более интересную трассировку (я сократил количество итераций в тесте до 10).

Например, glibcзагружает библиотеки следующим образом (строка 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

Тот же код в 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

Я не говорю, что это ключевое отличие, но сокращение числа операций ввода-вывода в основных библиотеках может повысить производительность. Из сравнения видно, что выполнение одного и того же кода Python может привести к немного другим системным вызовам. Вероятно, самое важное можно сделать при оптимизации производительности цикла. Я недостаточно квалифицирован, чтобы судить, вызвана ли проблема производительности выделением памяти или какой-либо другой инструкцией.

  • glibc с 10 итерациями:

    write(1, "0.032388824969530106\n", 210.032388824969530106)
    
  • musl с 10 итерациями:

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

muslмедленнее на 0,0028254222124814987 секунд. Поскольку различие увеличивается с увеличением количества итераций, я предполагаю, что разница заключается в распределении памяти для объектов JSON.

Если мы сократим эталонный тест исключительно до импорта, jsonмы заметим, что разница не так уж велика:

$ 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

Загрузка библиотек Python выглядит сопоставимо. Генерация list()производит большую разницу:

$ 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

Очевидно, что наиболее дорогой является операция json.dumps(), которая может указывать на различия в распределении памяти между этими библиотеками.

Еще раз посмотрим на эталонный тест , muslкоторый немного медленнее распределяет память:

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

Я не уверен, что подразумевается под «большим распределением», но muslон почти в 2 раза медленнее, что может стать значительным, если вы повторяете такие операции тысячи или миллионы раз.

Tombart
источник
12
Всего несколько исправлений. musl - это не собственная реализация Alpine glibc. 1st musl - это (не) реализация glibc, а другая реализация libc в соответствии со стандартом POSIX. Второй MUSL не компании Alpine собственного дела, это автономный, несвязанный проект и MUSL не используется только в Alpine.
Якуб Йирутка
учитывая, что musl libc выглядит лучше на основе стандартов *, не говоря уже о более новой реализации, почему в этих случаях он проигрывает glibc? * ср. wiki.musl-libc.org/functional-differences-from-glibc.html
Лес
Является ли разница в 0,0028 секунды статистически значимой? Относительное отклонение составляет всего 0,0013%, и вы берете 10 образцов. Каково было (оценочное) стандартное отклонение для этих 10 прогонов (или даже разницы макс-мин)?
Питер Мортенсен
@PeterMortensen По вопросам, связанным с результатами тестов, вы должны обратиться к коду Eta Labs: etalabs.net/libc-bench.html Например, стресс-тест malloc повторяется 100 000 раз. Результаты могут сильно зависеть от версии библиотеки, версии GCC и используемого процессора, и это лишь некоторые из аспектов.
Томбарт