Por que a imagem do Alpine Docker é 50% mais lenta que a imagem do Ubuntu?

35

Percebi que meu aplicativo Python é muito mais lento ao executá-lo do python:2-alpine3.6que executá-lo sem o Docker no Ubuntu. Eu vim com dois pequenos comandos de benchmark e há uma enorme diferença visível entre os dois sistemas operacionais, tanto quando os estou executando em um servidor Ubuntu quanto quando estou usando o 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

Eu também tentei o seguinte 'benchmark', que não 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

O que poderia estar causando essa diferença?

Underyx
fonte
1
Olhar @Seth novamente: o tempo começa após o bash está instalado, dentro do shell bash lançado
Underyx

Respostas:

45

Eu executei o mesmo benchmark que você, usando apenas o 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 em mais de 2 segundos de diferença:

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

A Alpine está usando uma implementação diferente de libc(biblioteca do sistema base) do projeto musl ( URL de espelho ). Existem muitas diferenças entre essas bibliotecas . Como resultado, cada biblioteca pode ter um desempenho melhor em determinados casos de uso.

Aqui está uma diferença entre os comandos acima . A saída começa a diferir da linha 269. É claro que existem endereços diferentes na memória, mas, caso contrário, é muito semelhante. Obviamente, a maior parte do tempo é gasta esperando o pythoncomando terminar.

Após a instalação stracenos dois contêineres, podemos obter um rastreamento mais interessante (reduzi o número de iterações no benchmark para 10).

Por exemplo, glibcestá carregando bibliotecas da seguinte maneira (linha 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

O mesmo código em 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

Não estou dizendo que essa é a principal diferença, mas reduzir o número de operações de E / S nas bibliotecas principais pode contribuir para um melhor desempenho. Pelo diff, você pode ver que a execução do mesmo código Python pode levar a chamadas de sistema ligeiramente diferentes. Provavelmente o mais importante poderia ser feito na otimização do desempenho do loop. Não estou qualificado o suficiente para julgar se o problema de desempenho é causado pela alocação de memória ou por alguma outra instrução.

  • glibc com 10 iterações:

    write(1, "0.032388824969530106\n", 210.032388824969530106)
    
  • musl com 10 iterações:

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

muslé mais lento em 0,0028254222124814987 segundos. À medida que a diferença aumenta com o número de iterações, eu presumo que a diferença esteja na alocação de memória dos objetos JSON.

Se reduzirmos o benchmark para a importação exclusiva json, notamos que a diferença não é tão 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

Carregar bibliotecas Python parece comparável. Gerar list()produz uma diferença maior:

$ 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, a operação mais cara é json.dumps(), o que pode apontar para diferenças na alocação de memória entre essas bibliotecas.

Olhando novamente para o benchmark , muslé realmente um pouco mais lento na alocação de memória:

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

Não sei ao certo o que significa "grande alocação", mas muslé quase duas vezes mais lento, o que pode se tornar significativo quando você repete essas operações milhares ou milhões de vezes.

Tombart
fonte
12
Apenas algumas correções. MUSL não é da Alpine própria implementação da glibc. O 1º musl não é uma (re) implementação do glibc, mas uma implementação diferente do libc de acordo com o padrão POSIX. 2º MUSL não é da Alpine própria coisa, é um autônomo, o projeto não relacionado e MUSL não é usado apenas no Alpine.
Jakub Jirutka
dado que o musl libc parece um melhor padrão baseado em *, sem mencionar a implementação mais recente, por que parece ter um desempenho inferior ao glibc nesses casos? * cf. wiki.musl-libc.org/functional-differences-from-glibc.html
Forest
A diferença de 0,0028 segundos é estatisticamente significativa? O desvio relativo é de apenas 0,0013% e você está colhendo 10 amostras. Qual foi o desvio padrão (estimado) para essas 10 corridas (ou mesmo a diferença max-min)?
Peter Mortensen
@PeterMortensen Para perguntas sobre resultados de benchmarks, consulte o código Eta Labs: etalabs.net/libc-bench.html Por exemplo, o teste de estresse malloc é repetido 100k vezes. Os resultados podem ser fortemente dependentes da versão da biblioteca, versão do GCC e CPU usada, apenas para citar alguns aspectos.
Tombart