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

21

Eu notei que meu aplicativo Python é muito mais lento quando executado em python:2-alpine3.6 do que executá-lo sem o Docker no Ubuntu. Eu criei dois pequenos comandos de benchmark e há uma grande diferença visível entre os dois sistemas operacionais, tanto quando eu os executo 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?

    
por Underyx 15.06.2017 / 17:23

1 resposta

25

Eu executei o mesmo benchmark que você usou 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 básica do sistema) de musl . Existem muitas diferenças entre essas bibliotecas . Como resultado, cada biblioteca pode ter um melhor desempenho em certos casos de uso.

Aqui está uma comparação de straces entre os comandos acima . A saída começa a diferir da linha 269. Claro que existem endereços diferentes na memória, mas de outra forma é muito semelhante. A maior parte do tempo é obviamente gasta esperando o comando python terminar.

Depois de instalar strace em ambos os contêineres, podemos obter um rastreamento mais interessante (reduzimos o número de iterações no benchmark para 10).

Por exemplo, glibc está 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

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 E / S nas bibliotecas principais pode contribuir para um melhor desempenho. A partir do diff, você pode ver que executar o 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. Eu não estou qualificado o suficiente para julgar se o problema de desempenho é causado por alocação de memória ou 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 suponho que a diferença está na alocação de memória dos objetos JSON.

Se reduzirmos o valor de referência apenas para importar json , notamos que a diferença não é tão grande assim:

$ 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 maior diferença:

$ 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 a referência , 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 2 × mais lento, o que pode se tornar significativo quando você repetir essas operações milhares ou milhões de vezes.

    
por 25.07.2017 / 16:33