¿Por qué la imagen de Alpine Docker es un 50% más lenta que la imagen de Ubuntu?

35

Noté que mi aplicación Python es mucho más lenta cuando la python:2-alpine3.6ejecuto sin Docker en Ubuntu. Se me ocurrieron dos pequeños comandos de referencia y hay una gran diferencia visible entre los dos sistemas operativos, tanto cuando los estoy ejecutando en un servidor Ubuntu como cuando estoy usando Docker para 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

También probé el siguiente 'punto de referencia', que no 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

¿Qué podría estar causando esta diferencia?

Underyx
fuente
1
@Seth mira de nuevo: el tiempo comienza después de instalar bash, dentro del shell de lanzamiento lanzado
Underyx

Respuestas:

45

Ejecuté el mismo punto de referencia que usted, 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

resultando en más de 2 segundos de diferencia:

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

Alpine está utilizando una implementación diferente de libc(biblioteca del sistema base) del proyecto musl ( URL espejo ). Hay muchas diferencias entre esas bibliotecas . Como resultado, cada biblioteca podría funcionar mejor en ciertos casos de uso.

Aquí hay una diferencia entre los comandos anteriores . La salida comienza a diferir de la línea 269. Por supuesto, hay diferentes direcciones en la memoria, pero por lo demás es muy similar. Obviamente, la mayor parte del tiempo se pasa esperando pythona que termine el comando.

Después de instalar straceen ambos contenedores, podemos obtener una traza más interesante (he reducido el número de iteraciones en el punto de referencia a 10).

Por ejemplo, glibcestá cargando bibliotecas de la siguiente manera (línea 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

El mismo código en 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

No digo que esta sea la diferencia clave, pero reducir la cantidad de operaciones de E / S en las bibliotecas principales podría contribuir a un mejor rendimiento. Desde el diff puede ver que ejecutar el mismo código de Python puede conducir a llamadas de sistema ligeramente diferentes. Probablemente lo más importante se podría hacer para optimizar el rendimiento del bucle. No estoy lo suficientemente calificado para juzgar si el problema de rendimiento es causado por la asignación de memoria o alguna otra instrucción.

  • glibc con 10 iteraciones:

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

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

musles más lento en 0.0028254222124814987 segundos. A medida que la diferencia aumenta con el número de iteraciones, supongo que la diferencia está en la asignación de memoria de los objetos JSON.

Si reducimos el punto de referencia a solo importar json, notamos que la diferencia no es tan 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

La carga de las bibliotecas de Python parece comparable. Generar list()produce una mayor diferencia:

$ 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

Obviamente, la operación más costosa es la json.dumps()que podría señalar diferencias en la asignación de memoria entre esas bibliotecas.

Mirando nuevamente al punto de referencia , la muslasignación de memoria es realmente un poco más lenta:

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

No estoy seguro de lo que se entiende por "gran asignación", pero musles casi 2 veces más lento, lo que puede llegar a ser significativo cuando repite tales operaciones miles o millones de veces.

Tombart
fuente
12
Solo algunas correcciones. musl no es la implementación propia de Alpine de glibc. 1st musl no es una (re) implementación de glibc, sino una implementación diferente de libc según el estándar POSIX. Segundo MUSL no es de Alpine propia cosa, es un proyecto independiente, sin relación y MUSL no se usa solo en Alpine.
Jakub Jirutka
dado que musl libc parece una mejor base más basada en estándares *, sin mencionar una implementación más reciente, ¿por qué parece tener un rendimiento inferior a glibc en estos casos? * cf. wiki.musl-libc.org/functional-differences-from-glibc.html
Forest
¿Es la diferencia de 0.0028 segundos estadísticamente significativa? La desviación relativa es solo 0.0013% y está tomando 10 muestras. ¿Cuál fue la desviación estándar (estimada) para esas 10 carreras (o incluso la diferencia máxima-mínima)?
Peter Mortensen
@PeterMortensen Para preguntas sobre los resultados de referencia, debe consultar el código Eta Labs: etalabs.net/libc-bench.html Por ejemplo, la prueba de estrés de Malloc se repite 100k veces. Los resultados podrían depender mucho de la versión de la biblioteca, la versión de GCC y la CPU utilizada, solo por nombrar algunos aspectos.
Tombart